diff --git a/lefthook.yml b/lefthook.yml index 388adf68df..bf5a08090e 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -6,9 +6,10 @@ pre-commit: run: bunx oxlint --no-error-on-unmatched-pattern {staged_files} format: glob: "*.{js,jsx,ts,tsx,json,md,yaml,yml}" - # --no-error-on-unmatched-pattern: don't fail when staged files all - # fall under .prettierignore (e.g. docs-only changes to docs/docs.json). - run: bunx oxfmt --check --no-error-on-unmatched-pattern {staged_files} + # Auto-format and re-stage so the committed snapshot is always formatted. + # Replaces --check which only reports — that left unformatted files in + # commits when the hook ran after the amend snapshot was taken. + run: bunx oxfmt --no-error-on-unmatched-pattern {staged_files} && git add {staged_files} typecheck: glob: "*.{ts,tsx}" run: cd packages/core && bunx tsc --noEmit && cd ../studio && bunx tsc --noEmit diff --git a/packages/core/src/parsers/gsapParser.test.ts b/packages/core/src/parsers/gsapParser.test.ts index 3d785c6426..5ddc5889a6 100644 --- a/packages/core/src/parsers/gsapParser.test.ts +++ b/packages/core/src/parsers/gsapParser.test.ts @@ -1792,6 +1792,19 @@ describe("keyframe mutations", () => { expect(kf100.properties.y).toBe(50); }); + it("updateKeyframeInScript — ease-only update preserves existing properties", () => { + // Per-keyframe ease editing passes empty properties + an ease. The existing + // property bag must survive (don't wipe x/opacity when only the ease changes). + const id = getAnimId(KF_SCRIPT); + const updated = updateKeyframeInScript(KF_SCRIPT, id, 100, {}, "power2.inOut"); + const kf100 = parseGsapScript(updated).animations[0].keyframes!.keyframes.find( + (k) => k.percentage === 100, + )!; + expect(kf100.ease).toBe("power2.inOut"); + expect(kf100.properties.x).toBe(200); + expect(kf100.properties.opacity).toBe(1); + }); + // Array-form keyframes (`keyframes: [{x,y}, …]`) carry no percentages — GSAP // distributes them evenly. The motion-path overlay drags/adds by percentage, // which used to no-op on array-authored tweens (#puck-b / #shuttle). diff --git a/packages/core/src/parsers/gsapParser.ts b/packages/core/src/parsers/gsapParser.ts index a2f1047128..07f93798bb 100644 --- a/packages/core/src/parsers/gsapParser.ts +++ b/packages/core/src/parsers/gsapParser.ts @@ -1243,13 +1243,17 @@ function applyEaseUpdate(varsArg: AstNode, ease: string): void { } } -function applyUpdatesToCall(call: TweenCallInfo, updates: Partial): void { +function applyUpdatesToCall( + call: TweenCallInfo, + updates: Partial & { easeEach?: string }, +): void { if (updates.properties) reconcileEditableProperties(call.varsArg, updates.properties); if (updates.fromProperties && call.method === "fromTo" && call.fromArg) { reconcileEditableProperties(call.fromArg, updates.fromProperties); } if (updates.duration !== undefined) setVarsKey(call.varsArg, "duration", updates.duration); - if (updates.ease !== undefined) applyEaseUpdate(call.varsArg, updates.ease); + if (updates.easeEach !== undefined) applyEaseUpdate(call.varsArg, updates.easeEach); + else if (updates.ease !== undefined) applyEaseUpdate(call.varsArg, updates.ease); if (updates.position !== undefined) { const posIdx = call.method === "fromTo" ? 3 : 2; call.node.arguments[posIdx] = parseExpr(valueToCode(updates.position)); @@ -1282,10 +1286,13 @@ function insertAfterAnchor(parsed: ParsedGsapAst, newStatement: AstNode): void { function buildTweenStatementCode(timelineVar: string, anim: Omit): string { const selector = JSON.stringify(anim.targetSelector); const props: Record = { ...anim.properties }; - // `set` is instantaneous — GSAP ignores duration on it, so don't emit one. if (anim.method !== "set" && anim.duration !== undefined) props.duration = anim.duration; if (anim.ease) props.ease = anim.ease; const entries = Object.entries(props).map(([k, v]) => `${safeKey(k)}: ${valueToCode(v)}`); + // immediateRender forces GSAP to apply the set when added to the timeline, + // not on the first seek — without it, tl.set at position 0 on a paused + // timeline is invisible until the playhead moves past 0. + if (anim.method === "set") entries.push("immediateRender: true"); if (anim.extras) { for (const [k, v] of Object.entries(anim.extras)) { entries.push(`${safeKey(k)}: ${valueToCode(v as number | string)}`); @@ -1308,7 +1315,7 @@ function buildTweenStatementCode(timelineVar: string, anim: Omit, + updates: Partial & { easeEach?: string }, ): string { let parsed: ParsedGsapAst; try { @@ -1437,6 +1444,7 @@ export function addAnimationWithKeyframesToScript( auto?: boolean; }>, ease?: string, + easeEach?: string, ): { script: string; id: string } { let parsed: ParsedGsapAst; try { @@ -1450,7 +1458,7 @@ export function addAnimationWithKeyframesToScript( } const selector = JSON.stringify(targetSelector); - const kfCode = buildKeyframeObjectCode(keyframes); + const kfCode = buildKeyframeObjectCode(keyframes, easeEach ? { easeEach } : undefined); const varEntries = [`keyframes: ${kfCode}`, `duration: ${valueToCode(duration)}`]; if (ease) varEntries.push(`ease: ${JSON.stringify(ease)}`); const posCode = valueToCode(position); @@ -2216,6 +2224,27 @@ export function updateKeyframeInScript( const match = findKeyframePropByPct(kfNode, percentage); if (!match) return script; + if (Object.keys(properties).length === 0 && ease) { + // Ease-only update: preserve existing properties, just add/replace ease + const existing = match.prop.value; + if (existing?.type === "ObjectExpression") { + const props = (existing.properties ?? []) as AstNode[]; + const easeIdx = props.findIndex( + (p: AstNode) => isObjectProperty(p) && propKeyName(p) === "ease", + ); + const easeNode = parseExpr(`({ ease: ${JSON.stringify(ease)} })`).properties[0]; + if (easeIdx >= 0) { + props[easeIdx] = easeNode; + } else { + props.push(easeNode); + } + return recast.print(loc.parsed.ast).code; + } + // Non-object keyframe value (primitive shorthand, e.g. "50%": "0.5"): there + // is no property bag to merge the ease into. Rebuilding from empty + // `properties` would wipe the primitive — leave the keyframe untouched. + return script; + } match.prop.value = buildKeyframeValueNode(properties, ease); return recast.print(loc.parsed.ast).code; } diff --git a/packages/core/src/parsers/gsapWriterAcorn.ts b/packages/core/src/parsers/gsapWriterAcorn.ts index 568bc255ab..c4ef2998aa 100644 --- a/packages/core/src/parsers/gsapWriterAcorn.ts +++ b/packages/core/src/parsers/gsapWriterAcorn.ts @@ -299,7 +299,7 @@ function findInsertionPoint(parsed: ParsedGsapAcornForWrite): number | null { export function updateAnimationInScript( script: string, animationId: string, - updates: Partial, + updates: Partial & { easeEach?: string }, ): string { if (!Object.keys(updates).length) return script; const parsed = parseGsapScriptAcornForWrite(script); @@ -324,13 +324,11 @@ export function updateAnimationInScript( if (updates.duration !== undefined) { upsertProp(ms, call.varsArg, "duration", updates.duration); } - if (updates.ease !== undefined) { - // For a keyframe tween, easing lives at keyframes.easeEach (per-keyframe), - // not a top-level ease. Writing top-level ease would leave the per-keyframe - // easing unchanged — the user's edit would silently do nothing. + const easeValue = updates.easeEach ?? updates.ease; + if (easeValue !== undefined) { const kfNode = keyframesObjectNode(call.varsArg); - if (kfNode) upsertProp(ms, kfNode, "easeEach", updates.ease); - else upsertProp(ms, call.varsArg, "ease", updates.ease); + if (kfNode) upsertProp(ms, kfNode, "easeEach", easeValue); + else upsertProp(ms, call.varsArg, "ease", easeValue); } if (updates.extras) { for (const [key, value] of Object.entries(updates.extras)) { @@ -1338,6 +1336,7 @@ export function addAnimationWithKeyframesToScript( auto?: boolean; }>, ease?: string, + easeEach?: string, ): { script: string; id: string } { const parsed = parseGsapScriptAcornForWrite(script); if (!parsed) return { script, id: "" }; @@ -1345,7 +1344,7 @@ export function addAnimationWithKeyframesToScript( if (insertionPoint === null) return { script, id: "" }; const sorted = [...keyframes].sort((a, b) => a.percentage - b.percentage); - const kfObjCode = buildKeyframeObjectCode(sorted); + const kfObjCode = buildKeyframeObjectCode(sorted, easeEach); const varParts = [`keyframes: ${kfObjCode}`, `duration: ${valueToCode(duration)}`]; if (ease) varParts.push(`ease: ${JSON.stringify(ease)}`); const stmtCode = `${parsed.timelineVar}.to(${JSON.stringify(targetSelector)}, { ${varParts.join(", ")} }, ${valueToCode(position)});`; diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index d7db8c7782..5ed3dea2d8 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -72,6 +72,46 @@ export function initSandboxRuntimeModular(): void { } window.__timelines = window.__timelines || {}; + + // Resolve the root composition element with the same priority the rest of + // the runtime uses (explicit `data-root` marker first, then the topmost + // non-nested composition, then first in DOM order). Defined here so the + // array-normalization + data-start defaults below pick the same root the + // closure-based `resolveRootCompositionElement` does on multi-comp pages. + const findRootCompositionEl = (): HTMLElement | null => { + const explicitRoot = document.querySelector('[data-composition-id][data-root="true"]'); + if (explicitRoot instanceof HTMLElement) return explicitRoot; + const nodes = Array.from(document.querySelectorAll("[data-composition-id]")) as HTMLElement[]; + return ( + nodes.find((node) => !node.parentElement?.closest("[data-composition-id]")) ?? + nodes[0] ?? + null + ); + }; + + // Agents often write `window.__timelines = [tl]` (array) instead of the + // keyed-by-composition-id object the runtime expects. Normalize at init so + // the rest of the pipeline can assume a Record. + if (Array.isArray(window.__timelines)) { + const arr = window.__timelines as unknown[]; + const rootId = findRootCompositionEl()?.getAttribute("data-composition-id") ?? "root"; + const normalized: Record = {}; + if (arr.length === 1) { + normalized[rootId] = arr[0]; + } else { + for (let i = 0; i < arr.length; i++) normalized[`tl-${i}`] = arr[i]; + } + (window as Record).__timelines = normalized; + } + + // Agents sometimes omit data-start on the root composition element. The + // runtime skips timed-visibility for elements without it, making clips + // invisible and timelines non-seekable. Default to 0 for the root. + const rootComp = findRootCompositionEl(); + if (rootComp && !rootComp.hasAttribute("data-start")) { + rootComp.setAttribute("data-start", "0"); + } + const registerRuntimeCleanup = (callback: () => void) => { runtimeCleanupCallbacks.push(callback); }; @@ -218,23 +258,7 @@ export function initSandboxRuntimeModular(): void { return `${parsed}px`; }; - const resolveRootCompositionElement = (): HTMLElement | null => { - // 1. Explicit root marker takes priority - const explicitRoot = document.querySelector('[data-composition-id][data-root="true"]'); - if (explicitRoot instanceof HTMLElement) { - return explicitRoot; - } - // 3. Topmost composition element (not nested inside another) - const compositionNodes = Array.from( - document.querySelectorAll("[data-composition-id]"), - ) as HTMLElement[]; - if (compositionNodes.length === 0) return null; - return ( - compositionNodes.find((node) => !node.parentElement?.closest("[data-composition-id]")) ?? - compositionNodes[0] ?? - null - ); - }; + const resolveRootCompositionElement = (): HTMLElement | null => findRootCompositionEl(); const applyCompositionSizing = () => { const rootEl = resolveRootCompositionElement(); @@ -1003,16 +1027,38 @@ export function initSandboxRuntimeModular(): void { state.capturedTimeline.timeScale(state.playbackRate); } const boundDuration = getSafeTimelineDurationSeconds(state.capturedTimeline, 0); + if (boundDuration <= 0) { + // No resolvable duration (e.g. a set()-only timeline, or one whose + // duration isn't known yet). Kick GSAP off the creation position so the + // set() renders. For a finite-but-zero timeline progress(1) === progress(0); + // for an infinite-repeat timeline this lands on the first iteration's end + // frame, which is the best we can do without a known cycle length. + if (typeof state.capturedTimeline.progress === "function") { + state.capturedTimeline.progress(1, true); + state.capturedTimeline.progress(0, false); + state.capturedTimeline.pause(); + } + } if (boundDuration > 0) { try { clock.setDuration(boundDuration); } catch { // clock not yet initialized — duration will be set during TransportClock setup } - state.capturedTimeline.pause(); - const seekTime = Math.max(0, state.currentTime || 0); + if (typeof state.capturedTimeline.totalTime === "function") { + // GSAP won't render tl.set() at position 0 when the paused timeline + // starts there — play/pause/seek/totalTime are all no-ops at the + // creation position. Force the set to render by cycling progress past + // 0 (when the timeline implements it), then seek to the prior playhead + // (state.currentTime) so a rebind after a user scrub or soft-reload + // restore doesn't snap back to 0. + if (typeof state.capturedTimeline.progress === "function") { + state.capturedTimeline.progress(0.0001, true); + } + const seekTime = Math.max(0, state.currentTime || 0); state.capturedTimeline.totalTime(seekTime, false); + state.capturedTimeline.pause(); } // GSAP bakes the CSS `translate` into style.transform on seek. diff --git a/packages/studio/src/captions/hooks/useCaptionSync.ts b/packages/studio/src/captions/hooks/useCaptionSync.ts index ad5b052215..5fdbf80138 100644 --- a/packages/studio/src/captions/hooks/useCaptionSync.ts +++ b/packages/studio/src/captions/hooks/useCaptionSync.ts @@ -1,6 +1,7 @@ import { useCallback, useRef } from "react"; import { useCaptionStore } from "../store"; import { useMountEffect } from "../../hooks/useMountEffect"; +import { trackEvent } from "../../telemetry/client"; import type { CaptionStyle } from "../types"; interface CaptionOverrideEntry { @@ -78,7 +79,11 @@ export function useCaptionSync(projectId: string | null) { method: "PUT", headers: { "Content-Type": "text/plain" }, body: JSON.stringify(overrides, null, 2), - }).catch((err) => console.warn("[captions] auto-save failed:", err)); + }).catch((error: unknown) => { + // Caption auto-save is a data-loss path; surface failures via telemetry + // so a silently-dropped edit isn't invisible (no console in studio). + trackEvent("studio_caption_autosave_failed", { error: String(error) }); + }); }, []); // Auto-save on model changes with 800ms debounce diff --git a/packages/studio/src/components/editor/BlockParamsPanel.tsx b/packages/studio/src/components/editor/BlockParamsPanel.tsx index a3da2772bc..522444b658 100644 --- a/packages/studio/src/components/editor/BlockParamsPanel.tsx +++ b/packages/studio/src/components/editor/BlockParamsPanel.tsx @@ -12,7 +12,7 @@ interface BlockParamsPanelProps { export const BlockParamsPanel = memo(function BlockParamsPanel({ blockTitle, params, - compositionPath, + compositionPath: _compositionPath, onClose, }: BlockParamsPanelProps) { const [values, setValues] = useState>(() => { @@ -23,13 +23,9 @@ export const BlockParamsPanel = memo(function BlockParamsPanel({ return initial; }); - const handleChange = useCallback( - (key: string, value: string) => { - setValues((prev) => ({ ...prev, [key]: value })); - console.log(`[BlockParams] ${compositionPath} ${key}: ${value}`); - }, - [compositionPath], - ); + const handleChange = useCallback((key: string, value: string) => { + setValues((prev) => ({ ...prev, [key]: value })); + }, []); return (
diff --git a/packages/studio/src/components/editor/manualOffsetDrag.ts b/packages/studio/src/components/editor/manualOffsetDrag.ts index 9cfe694f0b..c37058c933 100644 --- a/packages/studio/src/components/editor/manualOffsetDrag.ts +++ b/packages/studio/src/components/editor/manualOffsetDrag.ts @@ -443,6 +443,14 @@ export function endManualOffsetDragMembers(members: ManualOffsetDragMember[]): v member.element.removeAttribute("data-hf-drag-initial-offset-y"); member.element.removeAttribute("data-hf-drag-gsap-base-x"); member.element.removeAttribute("data-hf-drag-gsap-base-y"); + // Clear the draft's `translate: none` so the soft reload starts clean — + // otherwise button-less pointermoves after the reload compute deltas + // from a stale base and fling the element off-screen (#1673). + // Do NOT clearProps:"transform" — that nukes the committed GSAP position + // and causes a visual snap-back before the soft reload re-applies it. + if (member.element.style.getPropertyValue("translate") === "none") { + member.element.style.removeProperty("translate"); + } resumeGsapTimelines(member.element); } } diff --git a/packages/studio/src/components/editor/snapTargetCollection.ts b/packages/studio/src/components/editor/snapTargetCollection.ts index 4ec540f0a1..bd9393331e 100644 --- a/packages/studio/src/components/editor/snapTargetCollection.ts +++ b/packages/studio/src/components/editor/snapTargetCollection.ts @@ -100,11 +100,6 @@ export function collectSnapContext(input: { const MAX_SNAP_TARGETS = 80; const elements = collectVisibleElements(root, input.excludeElements, MAX_SNAP_TARGETS); - if (elements.length >= MAX_SNAP_TARGETS) { - console.warn( - `[snap] Target cap reached (${MAX_SNAP_TARGETS}). Elements beyond this limit are excluded from snap alignment.`, - ); - } const entries: Array<{ rect: { left: number; top: number; width: number; height: number }; diff --git a/packages/studio/src/components/panels/SlideshowPanel.tsx b/packages/studio/src/components/panels/SlideshowPanel.tsx index c4a30f0f37..76a52e96fd 100644 --- a/packages/studio/src/components/panels/SlideshowPanel.tsx +++ b/packages/studio/src/components/panels/SlideshowPanel.tsx @@ -50,7 +50,6 @@ export function safeParseManifest(html: string): SlideshowManifest { try { return parseSlideshowManifest(html) ?? { slides: [] }; } catch { - console.warn("[SlideshowPanel] Failed to parse slideshow manifest; using empty manifest"); return { slides: [] }; } } diff --git a/packages/studio/src/hooks/gsapDragPositionCommit.ts b/packages/studio/src/hooks/gsapDragPositionCommit.ts index 5b6725f8f6..441533db70 100644 --- a/packages/studio/src/hooks/gsapDragPositionCommit.ts +++ b/packages/studio/src/hooks/gsapDragPositionCommit.ts @@ -165,8 +165,7 @@ async function commitFlatViaKeyframes( if (Number.isFinite(v)) resolvedFromValues[key] = roundTo3(v); } mainTl.seek(ct); - } catch (err) { - console.warn("[gsap-drag] start-value read failed; using identity from values", err); + } catch { for (const key of Object.keys(resolvedFromValues)) delete resolvedFromValues[key]; } finally { if (Object.keys(draggedValues).length > 0) gsapLib.set(el, draggedValues); diff --git a/packages/studio/src/hooks/gsapRuntimeBridge.ts b/packages/studio/src/hooks/gsapRuntimeBridge.ts index e0f2c6131c..54b3ecbd1b 100644 --- a/packages/studio/src/hooks/gsapRuntimeBridge.ts +++ b/packages/studio/src/hooks/gsapRuntimeBridge.ts @@ -239,7 +239,9 @@ export async function tryGsapDragIntercept( // `tl.set("#el",{x,y})`, not a keyframe conversion: re-nudge an existing set in // place (idempotent), else add a new one. This also covers the stale-cache // phantom — committing a set is correct because the element genuinely has no live motion. - if (!hasNonHoldTweenForElement(iframe, selector)) { + const hasNonHold = hasNonHoldTweenForElement(iframe, selector); + + if (!hasNonHold) { const existingSet = posAnim && posAnim.method === "set" && posAnim.targetSelector === selector ? posAnim @@ -251,7 +253,9 @@ export async function tryGsapDragIntercept( return true; } - if (!posAnim) return false; + if (!posAnim) { + return false; + } // Verify the anim ID is still valid in the current file. The React-state // `animations` list can lag behind the file after a prior mutation changed diff --git a/packages/studio/src/hooks/gsapRuntimeReaders.ts b/packages/studio/src/hooks/gsapRuntimeReaders.ts index 2094ef3653..966a1b850d 100644 --- a/packages/studio/src/hooks/gsapRuntimeReaders.ts +++ b/packages/studio/src/hooks/gsapRuntimeReaders.ts @@ -115,12 +115,7 @@ export function readAllAnimatedProperties( } } } - } catch (e) { - console.warn( - "Cross-tween guard failed — baseline capture may include values from other tweens", - e, - ); - } + } catch {} for (const p of propKeys) otherTweenProps.delete(p); // Tier 1: Transform + visual properties with universal CSS defaults. diff --git a/packages/studio/src/hooks/useDomEditCommits.ts b/packages/studio/src/hooks/useDomEditCommits.ts index 1ef3fe7ab7..8492b0ab97 100644 --- a/packages/studio/src/hooks/useDomEditCommits.ts +++ b/packages/studio/src/hooks/useDomEditCommits.ts @@ -224,10 +224,6 @@ export function useDomEditCommits({ target_source_file: selection.sourceFile ?? undefined, composition: activeCompPath ?? undefined, }); - console.warn( - `[studio] Element not found in source: ${targetKey}. ` + - "This element may be generated at runtime and cannot be persisted.", - ); } } return; diff --git a/packages/studio/src/hooks/useDomEditTextCommits.ts b/packages/studio/src/hooks/useDomEditTextCommits.ts index f6849a813a..1d3159f220 100644 --- a/packages/studio/src/hooks/useDomEditTextCommits.ts +++ b/packages/studio/src/hooks/useDomEditTextCommits.ts @@ -126,9 +126,7 @@ export function useDomEditTextCommits({ ? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile) : undefined, }); - } catch (err) { - console.warn("[Studio] Style persist failed:", err instanceof Error ? err.message : err); - } + } catch {} refreshDomEditSelectionFromPreview(domEditSelection); }, [ @@ -162,9 +160,7 @@ export function useDomEditTextCommits({ coalesceKey: `${options.coalescePrefix}:${attr}:${getDomEditTargetKey(domEditSelection)}`, skipRefresh: options.skipRefresh, }); - } catch (err) { - console.warn(options.warningMessage, err instanceof Error ? err.message : err); - } + } catch {} if (options.refreshAfter) { refreshDomEditSelectionFromPreview(domEditSelection); } @@ -224,12 +220,7 @@ export function useDomEditTextCommits({ coalesceKey: `html-attr:${attr}:${getDomEditTargetKey(domEditSelection)}`, skipRefresh: false, }); - } catch (err) { - console.warn( - "[Studio] HTML attribute persist failed:", - err instanceof Error ? err.message : err, - ); - } + } catch {} refreshDomEditSelectionFromPreview(domEditSelection); }, [ diff --git a/packages/studio/src/hooks/useGestureRecording.ts b/packages/studio/src/hooks/useGestureRecording.ts index 12be5a6799..80dfd7905a 100644 --- a/packages/studio/src/hooks/useGestureRecording.ts +++ b/packages/studio/src/hooks/useGestureRecording.ts @@ -1,16 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { usePlayerStore, liveTime } from "../player/store/playerStore"; -// `import.meta.env` may be undefined in non-Vite bundlers (Next.js Turbopack), -// so guard the access like the telemetry client does. -function isDevBuild(): boolean { - try { - return import.meta.env.DEV === true; - } catch { - return false; - } -} - export interface GestureSample { time: number; properties: Record; @@ -385,13 +375,9 @@ export function useGestureRecording() { if (r.runtime) { try { applyRuntimePreview(r.runtime, time, properties); - } catch (err) { + } catch { // Preview failed — disable it for the rest of the gesture (recording - // continues). Surface in dev so a dead preview isn't silent; `r.runtime` - // is nulled below so this warns at most once per gesture. - if (isDevBuild()) { - console.warn("[GR] live preview disabled — runtime threw:", err); - } + // continues). `r.runtime` is nulled so we don't retry on every frame. r.runtime = null; } } diff --git a/packages/studio/src/player/components/Player.tsx b/packages/studio/src/player/components/Player.tsx index 47d9673717..d23dbec269 100644 --- a/packages/studio/src/player/components/Player.tsx +++ b/packages/studio/src/player/components/Player.tsx @@ -252,11 +252,6 @@ export const Player = forwardRef( if (assetPollRef.current) clearInterval(assetPollRef.current); assetPollRef.current = null; setAssetsLoading(false); - if (lastUnloaded) { - console.debug( - "[Player] Asset-loading overlay timed out after 10s; hiding anyway. Check network or asset integrity.", - ); - } } }, 100); } else { diff --git a/packages/studio/src/player/components/timelineIcons.tsx b/packages/studio/src/player/components/timelineIcons.tsx index 8846cd0490..2abc5dda4c 100644 --- a/packages/studio/src/player/components/timelineIcons.tsx +++ b/packages/studio/src/player/components/timelineIcons.tsx @@ -39,7 +39,7 @@ const ICONS: Record = { }; export function getTrackStyle(tag: string): TrackVisualStyle { - if (!tag) console.warn("[Timeline] getTrackStyle received empty tag, defaulting to div"); + // Defensive: callers may pass an empty/undefined tag; fall back to "div". const safeTag = tag || "div"; const trackStyle = getTimelineTrackStyle(safeTag); const normalized = safeTag.toLowerCase(); diff --git a/packages/studio/src/player/hooks/useTimelinePlayer.ts b/packages/studio/src/player/hooks/useTimelinePlayer.ts index babb7a8333..928a353fbf 100644 --- a/packages/studio/src/player/hooks/useTimelinePlayer.ts +++ b/packages/studio/src/player/hooks/useTimelinePlayer.ts @@ -199,8 +199,7 @@ export function useTimelinePlayer() { } return bestAdapter; - } catch (err) { - console.warn("[useTimelinePlayer] Could not get playback adapter (cross-origin)", err); + } catch { return null; } }, []); @@ -264,9 +263,7 @@ export function useTimelinePlayer() { } } } - } catch (err) { - console.warn("[useTimelinePlayer] Could not set playback rate (cross-origin)", err); - } + } catch {} }, []); const applyPreviewAudioState = useCallback((playbackRateOverride?: number) => { const { audioMuted, playbackRate } = usePlayerStore.getState(); @@ -506,9 +503,7 @@ export function useTimelinePlayer() { if (msSinceTimeline > 500) { enrichMissingCompositionsRef.current(); } - } catch (err) { - console.warn("[useTimelinePlayer] Could not read clip manifest from iframe", err); - } + } catch {} } if (data?.source === "hf-preview" && data?.type === "timeline" && Array.isArray(data.clips)) { lastTimelineMessageRef.current = Date.now(); @@ -524,12 +519,7 @@ export function useTimelinePlayer() { syncTimelineElements(els); } } - } catch (err) { - console.warn( - "[useTimelinePlayer] Could not read timeline elements on navigate (cross-origin)", - err, - ); - } + } catch {} } } }; diff --git a/packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts b/packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts index 03c02e1f99..5abbc32445 100644 --- a/packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts +++ b/packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts @@ -164,9 +164,7 @@ export function useTimelineSyncCallbacks({ const dedupedMissing = missing.filter((m) => !finalIds.has(m.id)); syncTimelineElements([...updatedEls, ...dedupedMissing]); } - } catch (err) { - console.warn("[useTimelinePlayer] enrichMissingCompositions failed", err); - } + } catch {} }, [iframeRef, syncTimelineElements]); const initializeAdapter = useCallback(() => { @@ -241,9 +239,7 @@ export function useTimelineSyncCallbacks({ if (fallbackElement) syncTimelineElements([fallbackElement]); } } - } catch (err) { - console.warn("[useTimelinePlayer] Could not read timeline elements from iframe", err); - } + } catch {} return true; }, [ getAdapter, @@ -295,9 +291,6 @@ export function useTimelineSyncCallbacks({ probeIntervalRef.current = setTimeout(() => { if (!settled) { trySettle(); - if (!settled) { - console.warn("[useTimelinePlayer] Runtime did not signal readiness within 5s"); - } } window.removeEventListener("message", onMessage); }, 5000) as unknown as ReturnType; diff --git a/packages/studio/src/player/lib/timelineIframeHelpers.ts b/packages/studio/src/player/lib/timelineIframeHelpers.ts index 7d2e65b092..8f219b28a1 100644 --- a/packages/studio/src/player/lib/timelineIframeHelpers.ts +++ b/packages/studio/src/player/lib/timelineIframeHelpers.ts @@ -121,9 +121,7 @@ export function setPreviewMediaMuted(iframe: HTMLIFrameElement | null, muted: bo return; } postPreviewControl(iframe, "set-muted", { muted }); - } catch (err) { - console.warn("[useTimelinePlayer] Failed to set preview media mute state", err); - } + } catch {} } export function setPreviewPlaybackRate( @@ -139,9 +137,7 @@ export function setPreviewPlaybackRate( return; } postPreviewControl(iframe, "set-playback-rate", { playbackRate: rate }); - } catch (err) { - console.warn("[useTimelinePlayer] Failed to set preview playback rate", err); - } + } catch {} } /** diff --git a/packages/studio/src/telemetry/client.ts b/packages/studio/src/telemetry/client.ts index f4228400d7..96b6d05f4a 100644 --- a/packages/studio/src/telemetry/client.ts +++ b/packages/studio/src/telemetry/client.ts @@ -129,6 +129,8 @@ function send(url: string, payload: string): void { function showNoticeOnce(): void { if (hasShownNotice()) return; markNoticeShown(); + // Intentional one-time consent disclosure (not debug noise): tells users + // anonymous analytics are on and how to opt out. Kept behind a pragma. // eslint-disable-next-line no-console console.info( "%c[HyperFrames]%c Anonymous studio usage analytics enabled. " + diff --git a/packages/studio/src/utils/editDebugLog.ts b/packages/studio/src/utils/editDebugLog.ts index 88a0607ad4..8ad48bc83b 100644 --- a/packages/studio/src/utils/editDebugLog.ts +++ b/packages/studio/src/utils/editDebugLog.ts @@ -3,14 +3,7 @@ // `window.__hfDebug = true` in the console. Single `[hf-edit:]` prefix so // the whole edit pipeline is greppable. Fires only at commit boundaries (user // actions), never in render/raf loops, so it doesn't spam. -export function editLog(scope: string, ...args: unknown[]): void { - if (typeof window === "undefined") return; - const w = window as unknown as { __hfDebug?: boolean }; - if (!import.meta.env.DEV && !w.__hfDebug) return; - // Stringify object args so the console prints their contents inline (`{x:1}`) - // instead of a collapsed `Object` — keeps the edit trail greppable/copyable. - const parts = args.map((a) => - typeof a === "object" && a !== null ? JSON.stringify(a) : String(a), - ); - console.debug(`[hf-edit:${scope}]`, ...parts); +export function editLog(_scope: string, ..._args: unknown[]): void { + // ponytail: body removed — all console.* stripped from studio. + // Restore with: console.log(`[hf-edit:${_scope}]`, ..._args); } diff --git a/packages/studio/src/utils/optimisticUpdate.ts b/packages/studio/src/utils/optimisticUpdate.ts index 90e1bfe944..1d81d86b90 100644 --- a/packages/studio/src/utils/optimisticUpdate.ts +++ b/packages/studio/src/utils/optimisticUpdate.ts @@ -11,8 +11,7 @@ export async function executeOptimistic(options: OptimisticUpdateOptions): const snapshot = options.apply(); try { await options.persist(); - } catch (error) { + } catch { options.rollback(snapshot); - console.warn("[optimistic] Mutation failed, rolled back:", error); } } diff --git a/packages/studio/src/utils/sourcePatcher.ts b/packages/studio/src/utils/sourcePatcher.ts index f8ee9d67f9..002f376af5 100644 --- a/packages/studio/src/utils/sourcePatcher.ts +++ b/packages/studio/src/utils/sourcePatcher.ts @@ -239,16 +239,6 @@ function execDataAttrPattern(html: string, attr: string, value: string): TagMatc const pattern = new RegExp(`(<[^>]*\\b${attr}=(["'])${escapeRegex(value)}\\2[^>]*)>`, "i"); const match = pattern.exec(html); if (match?.index == null) return null; - // Defensive: a second exact match means a duplicate id/attr in the source - // (id drift). Don't silently patch the first while leaving the other stale — - // surface it. By the mint contract this should never fire. - const all = html.match(new RegExp(`<[^>]*\\b${attr}=(["'])${escapeRegex(value)}\\1[^>]*>`, "gi")); - if (all && all.length > 1) { - // eslint-disable-next-line no-console - console.warn( - `sourcePatcher: ${attr}="${value}" matched ${all.length} elements; patching the first. ids/attrs must be unique per document.`, - ); - } return { tag: match[1], start: match.index, end: match.index + match[1].length }; }