mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
cc055f318d59623741fffa96e816ed68a635c2ef
291
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
264f2f06c8 |
Merge pull request #826 from func25/selection-scrub-freeze
fix(studio): keep preview animations active after selection scrub |
||
|
|
0f2a705259 |
fix(studio): preserve playback state on Jump-to-in/out shortcuts (#842)
When the user has the timeline playing and presses A (Jump to in-point)
or E (Jump to out-point), the seek seeks to the marker as expected but
also pauses the playback. The reporter (and the natural UX) expects
playback to keep going from the marker.
Root cause sits in two layers:
1. The `seek` callback in `useTimelinePlayer.ts` unconditionally calls
`setIsPlaying(false)` and `stopRAFLoop()` whenever the store reports
playing. That path is shared with timeline clicks, LayersPanel
navigation, and frame stepping — flipping the default would change
behavior the rest of the app expects.
2. `wrapTimeline` (the GSAP-timeline-backed adapter) calls `tl.pause()`
before `tl.seek(t)`, so even if the callback above stopped pausing,
GSAP-driven compositions would still get paused inside the adapter.
The fix is opt-in at both layers:
- Extend `PlaybackAdapter.seek` with `options?: { keepPlaying?: boolean }`.
Default is omitted/false, preserving existing behavior for every
caller that doesn't pass the option.
- `wrapTimeline.seek` skips the implicit `tl.pause()` when keepPlaying
is set. `createStaticSeekPlaybackAdapter` accepts the new signature
but is a no-op for the flag (it never paused internally).
- `useTimelinePlayer` seek callback grows the same option and forwards
it to adapter.seek(time, options). The reset block (stopRAFLoop,
setIsPlaying(false), shuttle refs) is gated behind !options.keepPlaying.
- Reverse shuttle is always stopped on seek (the RAF reverse tick
cannot survive a seek), so keepPlaying is overridden when the
shuttle was running backward. Documented with an inline comment.
- usePlaybackKeyboard updates its seek param type to match and passes
{ keepPlaying: true } on the A and E handlers only. Frame stepping
(Arrow keys, J/L with K held) keeps the default.
Tests (happy-dom):
- useTimelinePlayer.seek.test.ts covers the callback in three cases:
default seek clears isPlaying, seek with keepPlaying preserves
isPlaying=true, and the option from paused state stays paused.
- playbackAdapter.test.ts (new) covers wrapTimeline: default seek
pauses the GSAP timeline, keepPlaying: true skips the pause,
keepPlaying: false is the explicit default.
Closes part of #834 (sub-bug #2). Sub-bug #1 (playhead should loop
to in-point when exceeding out-point) lives in the RAF tick and is
left for a follow-up PR.
Co-authored-by: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com>
|
||
|
|
eac8808425 |
fix(studio): use e.key for playback shortcuts so non-QWERTY layouts work
The 7 letter shortcuts (J/K/L/I/O/A/E) in usePlaybackKeyboard were gated on `e.code === "Key*"`, which is the physical key position on a US-QWERTY layout. On AZERTY (and other layouts) the physical "KeyA" slot produces e.key="q", so "Jump to in-point" and the rest of the letter shortcuts either fired on the wrong character or not at all. Switch the 7 letter shortcuts to compare `e.key.toLowerCase()` and rename `pressedCodesRef` → `pressedKeysRef` so the K-hold combo (K+J / K+L for frame stepping) is also keyed off the typed character. `Space` and `Arrow*` keep using `e.code` since those codes are layout-independent. Adds a happy-dom test covering QWERTY happy path, AZERTY (physical KeyQ produces e.key="a" → in-point seek fires), AZERTY contrapositive (physical KeyA producing e.key="q" no longer triggers in-point), Shift+I clears in-point, K-hold combo for frame stepping, K release returning the set to clean state, and Space passthrough. Addresses bug #3 in #834. Bugs #1 (loop at out-point) and #2 (Jump to in-point forcing pause) live outside this hook (player loop and adapter `seek` respectively) and are left for follow-up PRs. |
||
|
|
e59089bf75 | fix(studio): persist studio state in project URLs (#836) | ||
|
|
d6543a08e1 | fix(studio): keep preview animations active after selection scrub | ||
|
|
2a2a88ce1f |
fix(studio): center vertical composition thumbnails in sidebar (#820)
* feat(studio): add pasteboard background to preview viewport Adds bg-neutral-800 to the preview viewport so the area outside the canvas is visually distinct from the composition content — consistent with professional video editors (Premiere, DaVinci, Figma). * feat(studio): pasteboard background and canvas outline around preview - NLEPreview: viewport gets bg-neutral-700 (#404040) as the pasteboard color surrounding the canvas — distinct from the app chrome (#0a0a0a) - Player wrapper: drop bg-black so the pasteboard shows around the canvas (loading overlays still cover the area with bg-black during load) - Player: set host background to transparent via inline style (overrides :host { background: #000 } in shadow DOM), and inject a style rule into the open shadow root so .hfp-container has overflow:visible and the canvas iframe gets a thin white ring + soft drop-shadow — making the canvas boundary legible against the pasteboard * fix(studio): center vertical composition thumbnails in sidebar Portrait (and other non-16:9) compositions were pinning to the top-left of the 80x45 thumbnail slot because transform-origin was '0 0'. Compute the centering offsets from the scaled dimensions and apply them as left/top so any aspect ratio renders centred in the slot. |
||
|
|
34f5fc05f3 |
feat(studio): add pasteboard background to preview viewport (#819)
* feat(studio): add pasteboard background to preview viewport Adds bg-neutral-800 to the preview viewport so the area outside the canvas is visually distinct from the composition content — consistent with professional video editors (Premiere, DaVinci, Figma). * feat(studio): pasteboard background and canvas outline around preview - NLEPreview: viewport gets bg-neutral-700 (#404040) as the pasteboard color surrounding the canvas — distinct from the app chrome (#0a0a0a) - Player wrapper: drop bg-black so the pasteboard shows around the canvas (loading overlays still cover the area with bg-black during load) - Player: set host background to transparent via inline style (overrides :host { background: #000 } in shadow DOM), and inject a style rule into the open shadow root so .hfp-container has overflow:visible and the canvas iframe gets a thin white ring + soft drop-shadow — making the canvas boundary legible against the pasteboard |
||
|
|
1caeb28658 |
feat(studio): header logo, playbar cleanup, and I/O work-area markers (#811)
* feat(studio): header logo, playbar cleanup, and I/O work-area markers
- Add Hyperframes icon mark to the studio header (left of project name)
- Remove m:ss toggle button — click the timecode directly to switch modes
- Remove frame jump input from controls bar — moved into ⌨ shortcuts panel
- Replace Loop text button with a repeat icon
- Collapse J/K/L shortcut badges into a single ⌨ icon that opens a panel
- Shortcuts panel: Jump to frame, Work area I/O display, shortcuts reference
- Implement I/O work-area markers (closes #807):
- I / Shift+I: set / clear in-point at playhead
- O / Shift+O: set / clear out-point at playhead
- A: jump to in-point (or start); E: jump to out-point (or end)
- Loop respects in/out boundaries for both forward and backward playback
- Teal work-area band + tick markers rendered on the seek bar
* fix(studio): guard against inverted in/out work-area points in loop ticks
If the user sets out-point before in-point (outPoint < inPoint), rawLoopStart
>= rawLoopEnd caused the loop guard to fire immediately on every tick, creating
a tight infinite seek loop. Both the forward RAF tick and the reverse RAF tick
now fall back to the full composition range when the work area is invalid.
* fix(studio): address work-area edge cases from review
- setInPoint/setOutPoint now cross-clear the opposite marker when setting one
would produce an inverted range (in >= out), preventing the invalid state
rather than correcting it at tick time
- Forward tick no longer gates on !adapter.isPlaying() — outPoint crossing
fires even while the adapter is running; explicitly pauses on the non-loop
path so playback stops at out-point rather than sailing to dur
- play() end-of-stream reset seeks to inPoint (if set) instead of hardcoded 0
* feat(studio): use full Hyperframes wordmark logo in header
Replace the standalone icon mark with the complete logo from logo-dark.svg
(icon mark + Hyperframes wordmark), with all black text fills inverted to
white for the dark header background. Project name is shown next to the logo
separated by a middot.
* Revert "feat(studio): use full Hyperframes wordmark logo in header"
This reverts commit
|
||
|
|
c3f70c91db |
fix(studio): restore saved positions on page refresh (#801)
* fix(studio): restore saved positions on page refresh studio-manual-edits.json was correctly persisted to disk but never read back into memory on bootstrap. On every page refresh, studioManualEditManifestRef started empty, so handleLoad applied an empty manifest and all saved positions/sizes/rotations were silently discarded. applyStudioManualEditsToPreview now reads from disk whenever the in-memory manifest is empty. The existing readRevision guard prevents overwriting an in-flight optimistic edit if a position change races with the disk read. * fix(studio): close delete-all race and apply same bootstrap to motion manifest Two follow-up fixes from review: 1. Replace edits.length === 0 with an explicit manifestBootstrappedRef boolean. The old condition was true in two distinct states: never-bootstrapped AND user-deleted-all-edits. Because the delete-all disk write is async-queued, there was a window where applyStudioManualEditsToPreview could read stale disk content and resurrect just-deleted positions. The boolean flag is set on the first apply and reset on project switch, cleanly separating the two states. 2. applyStudioMotionToPreview had the identical bug: GSAP motion edits were also lost on page refresh. Applied the same motionBootstrappedRef pattern. |
||
|
|
246a1911b1 |
fix(studio): auto-reconnect when preview server is not running (#802)
* fix(studio): auto-reconnect when preview server is not running When the preview server is not reachable (tab reload after server died, or opening the URL before running npm run dev), the Studio was silently swallowing the fetch error and rendering an infinite pulsing dot with no recovery path. Users had no idea what happened. Instead of showing an error and asking the user to act, the Studio now polls /api/projects every 2 seconds and automatically transitions into the full editor the moment the server becomes available — no manual reload required. Also fixes how agents are instructed about the dev server: CLAUDE.md and AGENTS.md listed `npm run dev` as a one-liner comment identical to other commands, giving no indication it blocks until stopped. Agents (including Claude Code) were running it in foreground, timing out after ~2 minutes, and silently killing the server. Added an explicit note that it must be started as a background process. * fix(studio): auto-reconnect when preview server is not running Two issues combined to produce the "reloading the tab kills the whole" experience for users running with an AI agent: 1. Agents silently killed the server — CLAUDE.md/AGENTS.md listed npm run dev with no indication it blocks. Agents ran it in foreground, the Bash tool timed out, and the process died. Added an explicit run_in_background instruction. 2. Studio had no recovery path — fetch errors were swallowed, leaving a permanent pulsing dot with no way out. Now the Studio polls every 2s and auto-transitions the moment the server responds. Also fixes the bookmark-reload case: the hash path previously bailed out before pinging the server, so a dead server + saved URL produced a blank editor instead of the waiting state. The server is now always contacted first, regardless of whether a hash project ID is present. Timer cleanup (cancelled flag + clearTimeout) prevents setState on unmounted components under StrictMode dev re-mounts. Extracted into useServerConnection hook to keep App.tsx under the 500 LOC limit. |
||
|
|
039c2b4402 | fix(studio): keep layer selection seek within clip range | ||
|
|
2bea226ea5 | fix(studio): only seek layers on selection (#790) | ||
|
|
2e6022972b |
fix(studio): add preview zoom controls (#761)
* fix(studio): add smooth preview zoom with pinch/Ctrl+scroll - Scale iframe content from inside (contentDocument.documentElement) instead of scaling the parent div, avoiding compositor re-rasterization on every zoom frame — critical for smooth zoom on high-refresh displays (240Hz) - Document-level capture-phase wheel handler bypasses the DomEditOverlay - Center-based zoom (no pan drift from pointer-anchored formulas) - Transient HUD shows zoom % briefly, no persistent UI controls - Double-click preview area to reset zoom to fit - Drag-to-pan when zoomed past 100% - Momentum scroll suppression after pinch gesture (400ms cooldown) - Delta clamping (MAX_DELTA=10) prevents overshooting on fast gestures - toDomPrecision rounds transform values to 4 decimals (matches tldraw) - Zoom state persisted to localStorage with 200ms debounce - Exposes --preview-zoom CSS custom property for overlay coordinate mapping - Fix infinite render loop in NLELayout (onIframeRef → refreshPreviewDocumentVersion) * fix(studio): use CSS zoom instead of transform scale for preview zoom CSS transform: scale() on a div containing an iframe causes compositor cross-layer sync issues that produce visible frame tearing on high-refresh displays (240Hz ProMotion). CSS zoom property changes the actual rendered size without compositor layer synchronization, eliminating the jumping. - Replace transform: scale(Z) with zoom: Z on the stage div - Keep transform: translate() for panning (compositor-friendly, no iframe) - Overlays work correctly since getBoundingClientRect() includes zoom - Remove will-change, transition hacks, pointer-events toggles * fix(ci): use apt-get for ffmpeg in preview-regression workflow The FedericoCarboni/setup-ffmpeg action downloads from an external URL that has been persistently unreachable, causing CI failures. Switch to apt-get install which uses Ubuntu's package repos (same as ci.yml and player-perf.yml). * fix(studio): clear zoom timers on NLEPreview unmount settleTimerRef, hudTimerRef, and retiringTimerRef could fire after component unmount. Add cleanup effect to prevent stale callbacks. * feat(studio): persist sidebar, timeline, and playback speed across reloads Wire up studioUiPreferences for the three remaining UI states requested in #752: left sidebar collapsed, timeline visibility, and playback rate. All three now survive page reloads using the same localStorage key as preview zoom. |
||
|
|
10fb968583 | fix(studio): show inspector selection bounds after click (#789) | ||
|
|
236eabe248 |
fix: prevent SourceEditor recreation on every content change (#624)
The mountEditor callback had content in its dependency array and was used as a React ref callback. Every content change (keystroke) gave mountEditor a new identity, causing React to destroy and recreate the entire CodeMirror editor — losing cursor position, undo history, and focus. Remove content from the dependency array and use a separate useEffect to push external content updates to the existing editor via dispatch(). The editor is now only recreated when filePath, language, or readOnly change. Co-authored-by: Test User <test@example.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
6fe970651d |
feat(studio): add Layers panel as new inspector tab (#784)
* feat(studio): add Layers panel as new inspector tab Adds a dedicated Layers tab alongside Design and Renders in the right panel inspector. The panel shows the full composition element tree with collapsible hierarchy — clicking a layer selects it without navigating away from the tree view. Closes #783 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(studio): add visual element previews to Layers panel Each layer row now shows a small color/content preview thumbnail: - Text elements show a snippet of their content in the actual font color - Container elements show their background color as a colored swatch - Image elements show a tiny thumbnail of the image - Media elements show an icon indicator Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(studio): add hover-to-highlight and auto-seek to Layers panel Replace tiny preview thumbnails with hover highlighting — hovering a layer row highlights the element in the preview canvas. Clicking a layer auto-seeks the playhead to that element's start time in the timeline. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): layers panel tab order, autoseek, and renders toolbar overflow - Reorder inspector tabs to Design → Layers → Renders - Fix autoseek: walk DOM ancestors when selected element has no direct timeline match, so clicking a child like S2 Heading correctly seeks to the start of its parent scene - Fix Renders toolbar overflow: add flex-wrap to the export controls header so selects and the Export button wrap instead of clipping * fix(studio): seek to midpoint of element duration in layers panel autoseek * fix(studio): layers panel seek now drives adapter.seek via requestSeek signal setCurrentTime() only updated the store — adapter.seek() and liveTime.notify() were never called so the iframe never moved. Add requestedSeekTime to the player store; useTimelinePlayer subscribes and calls the real seek() path when it fires. * feat(studio): hover over a layer auto-seeks to element midpoint (300ms debounce) * feat(studio): add collapsible sections to Design panel Section component now supports collapse/expand with a chevron toggle. Text, Layout, and Fill sections stay expanded by default. Less-used sections (Flex, Radius, Stroke, Effects, Clip, Transparency) start collapsed to reduce scrolling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): remove stale selectedTimelineElement usages dropped in rebase * fix(studio): stabilize resetErrors in useConsoleErrorCapture to break render loop resetErrors was a new function object on every render. handlePreviewIframeRef had it as a dep, so it also changed every render. NLELayout's useEffect watching onIframeRef would re-fire, calling setPreviewIframe again, which re-ran useConsoleErrorCapture with the new iframe — infinite loop. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
51f968ec87 | fix(studio): keep preview selection overlay visible for selected elements (#777) | ||
|
|
91bdffffe6 |
fix(ci): scope LOC check to studio, split useTimelinePlayer + hyperframes-player under 500 LOC (#750)
* refactor: split useTimelinePlayer.ts and hyperframes-player.ts into focused modules (<500 LOC each) * fix(ci): scope 500 LOC check to packages/studio, add allowlist for grandfathered files * feat(cli): Linux ARM64 support — auto-install Chromium on DGX Spark / GB10 / Jetson Chrome Headless Shell has no Linux ARM64 binary. On arm64 Linux: - Detects the platform automatically - Tries to auto-install system Chromium via apt-get (works on Ubuntu/Debian ARM) - Falls back to clear manual instructions with exact commands - 'hyperframes browser ensure' guides through the setup interactively - After setup, all render commands work without any flags * fix(ci): disable Windows Defender real-time monitoring to prevent EPERM builds Path exclusions are insufficient — Defender re-scans new files created during bun install before the exclusion takes effect. Disable real-time monitoring for the entire job duration instead (standard CI practice). * refactor(studio): split all files >500 LOC + extract useToast, delete allowlist All 11 large files split into focused modules under 500 LOC. App.tsx extracted toast logic into useToast hook (493 LOC now). .filesize-allowlist deleted — no longer needed. * fix: remove unused imports from split files, extract useToast from App.tsx App.tsx: 504 → 493 lines (toast logic extracted to useToast hook) timelineDOM.ts: remove unused imports from re-export pattern MotionPanel.tsx: remove unused clampStudioCustomEasePoints import studioMotionOps.ts: remove unused StudioGsapMotionDirection import * fix(ci): use Set-MpPreference to fully disable Windows Defender (both jobs) * fix(producer): use node --experimental-strip-types instead of tsx for build:fonts Eliminates the tsx binary dependency that Windows Defender locks during bun install, causing EPERM errors. Node 22.6+ strips TypeScript types natively with no external binary. * chore: remove .filesize-allowlist — App.tsx is now 493 lines (<500) * fix(ci): disable Windows Defender before checkout to prevent all EPERM races * fix(producer): skip build:fonts if fontData.generated.ts already exists The generated file is tracked in git, so CI doesn't need to regenerate it. This avoids @fontsource/inter node_modules access on Windows which triggers EPERM from Defender scanning during bun install. |
||
|
|
59e1d0787f |
fix(studio): prevent composition switch loop on sub-composition navigation (#754)
Circular state update between activeCompPath and compositionStack caused the preview to flicker when navigating to sub-compositions and scrubbing. The cycle: activeCompPath change → useEffect updates compositionStack → onCompositionChange fires → setActiveCompPath + refreshPreviewDocumentVersion → re-render → effect re-evaluates → repeat. Fixed by guarding both sides: onCompositionChange skips if path unchanged, updateCompositionStack skips notification if top-of-stack ID unchanged. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
38efe168e2 |
refactor(studio): contexts, PropertyPanel split, duration fix, perf (#748)
* feat(studio): add manual DOM editing inspector (#466) * fix: stabilize studio preview and runtime sync * fix: pass selector through timeline thumbnails * feat: add studio timeline editing * fix: disambiguate timeline edit targets * fix: stop timeline auto-scroll in fit mode * feat: use percentage-based timeline zoom * fix: sync timeline playhead on zoom changes * fix: reset timeline scroll when returning to fit * feat(studio): add manual DOM editing inspector * docs: update studio manual dom editing guide * feat(studio): add image asset picker for fills * feat(studio): add inline image uploads for fills * fix(studio): use real file input for image fill uploads * fix(studio): restore toast plumbing after rebase * fix(studio): explain in-app upload limitation * fix(studio): reuse asset-tab upload pattern in fills * feat(studio): refine manual design inspector * fix(studio): polish manual design inspector * fix(studio): keep color picker in viewport * fix(studio): clarify color picker selection * docs: update manual DOM editing guide * fix(studio): keep gradient color picker open * fix(studio): scope text color to text layers * fix(studio): add agent fallback for immovable layers * fix(studio): address manual editing review feedback * fix(studio): make local font selection reliable * fix(studio): improve dom picking and thumbnails * fix(studio): copy absolute paths in agent prompts * fix(studio): prevent timeline track cutoff * fix: copy Studio agent prompts in Safari * fix(studio): hold canvas movement from inspector * feat(studio): add persistent undo redo (#537) Studio manual editing and timeline editing mutate project files directly, but those edits had no reliable undo/redo path. Before releasing manual editing, users need a way to recover from visual property changes, source-editor saves, timeline moves/resizes/deletes, and timeline asset drops. The history also needs to survive a page refresh. A refresh should not erase the only way back from a bad manual edit. - Adds a persistent per-project edit-history model for file snapshots. - Stores undo/redo stacks in IndexedDB so history survives Studio refreshes. - Records source editor saves, manual DOM edits, and timeline mutations. - Adds toolbar undo/redo buttons with standard keyboard shortcuts: `Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`, and `Ctrl+Y`. - Validates current file hashes before applying undo/redo so external file changes do not silently overwrite newer content. - Keeps history available in memory if IndexedDB persistence fails during a session. - Adds focused unit coverage for the pure history model, storage adapter, controller/hook behavior, and project-file save helper. Studio previously treated every editor mutation as an immediate file write. Manual DOM editing, timeline updates, and source-editor saves each had separate write paths, so there was no common transaction boundary where Studio could capture the file contents before and after an edit. Undo/redo needed to sit above those write paths as a file-level transaction system: capture changed files before saving, write the new contents, persist the history entry by project, then apply undo/redo only when the current file content still matches the expected snapshot. - `bun --filter @hyperframes/studio test src/utils/editHistory.test.ts src/utils/editHistoryStorage.test.ts src/hooks/usePersistentEditHistory.test.ts src/utils/studioFileHistory.test.ts` -> 4 files pass, 15 tests pass - `bun --filter @hyperframes/studio test` -> 26 files pass, 289 tests pass - `bun --filter @hyperframes/studio typecheck` - `bunx oxlint packages/studio/src/App.tsx packages/studio/src/icons/SystemIcons.tsx packages/studio/src/hooks/usePersistentEditHistory.ts packages/studio/src/hooks/usePersistentEditHistory.test.ts packages/studio/src/utils/editHistory.ts packages/studio/src/utils/editHistory.test.ts packages/studio/src/utils/editHistoryStorage.ts packages/studio/src/utils/editHistoryStorage.test.ts packages/studio/src/utils/studioFileHistory.ts packages/studio/src/utils/studioFileHistory.test.ts` -> 0 warnings, 0 errors - `bunx oxfmt --check packages/studio/src/App.tsx packages/studio/src/icons/SystemIcons.tsx packages/studio/src/hooks/usePersistentEditHistory.ts packages/studio/src/hooks/usePersistentEditHistory.test.ts packages/studio/src/utils/editHistory.ts packages/studio/src/utils/editHistory.test.ts packages/studio/src/utils/editHistoryStorage.ts packages/studio/src/utils/editHistoryStorage.test.ts packages/studio/src/utils/studioFileHistory.ts packages/studio/src/utils/studioFileHistory.test.ts` - `git diff --check` - `bun run --filter @hyperframes/core build:hyperframes-runtime` before commit hook, because the clean worktree needed the ignored runtime-inline artifact for typecheck - Lefthook pre-commit -> lint, format, typecheck pass - Lefthook commit-msg -> commitlint pass - Started Studio locally at `http://127.0.0.1:5190/#project/undo-redo-sample`. - Used `agent-browser` to select a preview element in the Inspector and change `#hero-card` from `left: 220px` to `left: 260px`. - Refreshed Studio and verified Undo stayed enabled. - Clicked Undo and verified the project file returned to `left: 220px`; clicked Redo and verified the inline `left: 260px` returned. - Used `agent-browser` to drag the `side-card` timeline clip, refreshed Studio, then verified Undo restored the previous timeline attributes and Redo reapplied the timeline move. - Recorded the tested undo/redo flow with `agent-browser`: `qa-artifacts/studio-undo-redo-2026-04-28/studio-undo-redo-flow.webm`. - Local screenshots and recordings are kept under `qa-artifacts/studio-undo-redo-2026-04-28/` and are intentionally not committed. - The scratch Studio project used for browser proof is local-only under `packages/studio/data/projects/undo-redo-sample/` and is intentionally not committed. - The PR intentionally excludes the earlier PRD/TDD planning notes under `docs/superpowers/`; those remain local-only per request. * fix: align Studio capture with preview (#595) Studio frame capture could fail for projects mounted outside the repo when the project id came from an encoded hash route. A project like `Notion Showcase` loaded as `#project/Notion%20Showcase`, but the capture URL encoded that already-encoded value again, producing `/api/projects/Notion%2520Showcase/...` and a 404. While validating the fix by seeking through the preview, capture also diverged from the visible player for nested compositions because the thumbnail route sought raw timelines instead of the same player seek path used by Studio preview. - Decodes project ids when reading Studio `#project/...` routes and centralizes project hash/API path construction. - Keeps API URLs encoded exactly once, including project names with spaces, literal `%`, reserved characters, and unicode. - Updates Studio thumbnail capture to prefer `window.__player.seek(t)` and only fall back to raw timeline seeking for standalone pages. - Preserves explicit `t=0` thumbnail requests instead of falling back to `0.5` seconds. - Adds preview-regression CI coverage for Studio routing, frame capture URL construction, thumbnail seeking, and core thumbnail seek parsing. Studio treated the hash route segment as the canonical project id even when the browser had already percent-encoded it. `buildFrameCaptureUrl` then encoded that string again, so a decoded project directory name and the capture API path no longer matched. The preview/capture mismatch was a separate seek-path issue: the visible Studio preview seeks through the HyperFrames player, which maps global time into nested composition time. The capture route bypassed that layer and paused all registered timelines at the same global time. The zero-second capture case came from parsing `t` with a truthiness fallback, so `parseFloat("0") || 0.5` became `0.5`. - `bun run --cwd packages/studio test -- vite.thumbnail.test.ts src/utils/projectRouting.test.ts src/utils/frameCapture.test.ts` - `bun run --cwd packages/core test -- src/studio-api/routes/thumbnail.test.ts` - `bunx oxfmt --check .github/workflows/preview-regression.yml packages/studio/vite.thumbnail.ts packages/studio/vite.thumbnail.test.ts packages/studio/vite.config.ts packages/studio/src/utils/projectRouting.ts packages/studio/src/utils/projectRouting.test.ts packages/studio/src/utils/frameCapture.ts packages/studio/src/App.tsx packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/routes/thumbnail.test.ts` - `bunx oxlint .github/workflows/preview-regression.yml packages/studio/vite.thumbnail.ts packages/studio/vite.thumbnail.test.ts packages/studio/vite.config.ts packages/studio/src/utils/projectRouting.ts packages/studio/src/utils/projectRouting.test.ts packages/studio/src/utils/frameCapture.ts packages/studio/src/App.tsx packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/routes/thumbnail.test.ts` - `bun run --cwd packages/studio typecheck` - `bun run --cwd packages/core build:hyperframes-runtime` - `bun run --cwd packages/core typecheck` - `git diff --check` Pre-commit also reran lint, format, and typecheck successfully for the committed files. Using `agent-browser`, I mounted `/Users/miguel07code/Downloads/Notion Showcase` into Studio's project data and opened: ```text http://127.0.0.1:5197/#project/Notion%20Showcase ``` Before the fix, Capture requested `/api/projects/Notion%2520Showcase/thumbnail/index.html?...` and Studio showed `Capture failed`. After the fix, I sought the preview to `0s`, `2s`, `10s`, and `18s`, captured each frame, and compared the visible preview crop against the capture output. The capture URLs all used `Notion%20Showcase`, not `Notion%2520Showcase`, and no failure toast appeared. Mean pixel diffs for preview vs capture were: - `0s`: `0.0` - `2s`: `0.8641` - `10s`: `0.3496` - `18s`: `0.2309` The small non-zero diffs are raster/antialias-level differences after resizing the capture to the preview crop dimensions. - Browser screenshots, comparison sheets, network logs, and the `agent-browser` recording are local-only under `qa-artifacts/capture-button/` and are not committed. - The local Notion Showcase project mount is an ignored symlink under `packages/studio/data/projects/` and is not committed. - Thumbnail cache versions were bumped so stale captures generated with the old seek behavior are not reused. * feat: persist studio manual edits via manifest * fix(studio): stabilize manual edit manifest rendering * fix(studio): allow master canvas layer selection * fix(studio): scale master edits in source coordinates * fix(studio): reapply manual edits during playback * fix(studio): keep rotation edit base stable * feat(studio): highlight hovered canvas target * fix(studio): drag hovered canvas targets immediately * fix(studio): rotate manual edits around center * fix(studio): keep rotate handle aligned while dragging * fix(studio): allow small rotation adjustments * fix(studio): match rotate handle size to resize handle * fix(studio): connect rotate handle line to selection * feat(studio): reset selected manual edits * fix(studio): route inspector geometry through manual edits * feat: add studio group repositioning * fix: preserve studio group selections * fix: seed additive studio selection groups * fix: select studio groups on pointerdown * fix: harden studio group overlay events * fix: address studio manual edit review feedback * fix: apply nested manual edits in drilled previews * fix: commit drag offsets from gesture math * fix: persist manual preview edits on refresh * fix: harden manual edit refresh apply * fix: share manual edit render runtime * chore: release v0.5.0-alpha.15 * feat(core): add studio animation preview APIs * feat(studio): add alpha editor layer inspector * chore: release v0.6.0-alpha.1 * feat(studio): enable inspector panels by default * fix(studio): keep motion panel opt-in * chore: release v0.6.0-alpha.2 * feat: auto-open timeline clip layers * feat: show composition loading in studio * feat: disable Studio timeline while composition loads * chore: ignore .claude directory * chore: release v0.6.0-alpha.3 * feat(studio): simplify inspector selection ux * fix(studio): keep notion preview playback moving * fix(studio): handle raster inspector clicks * fix(studio): stale selection, rotation control, design panel polish Fixes and improvements based on power-user testing feedback: 1. Fix stale selection after style edits — handleDomStyleCommit now calls refreshDomEditSelectionFromPreview after persisting, matching every other commit handler. Without this, the PropertyPanel showed frozen computedStyles after color/radius/shadow edits, making it look like editing "didn't work." Also adds error handling around the persist call. 2. Add rotation field to the Design panel Layout section — reads the current rotation angle from the manual edit manifest and commits via the existing handleDomRotationCommit handler. 3. Enable motion panel by default — STUDIO_MOTION_PANEL_ENABLED now defaults to true so the Motion tab is discoverable without env vars. 4. Color controls only when element has color — fill color section now only shows when the element has an explicit non-transparent background-color. Text color shows only when the element has a color style. Prevents showing color pickers on elements where color edits have no visible effect. 5. Exclude canvas from selection — added "canvas" to DOM_LAYER_IGNORED_TAGS so canvas elements are not selectable in the preview or listed in the layer panel. 6. Multi-selection feedback — shows "N elements selected" with guidance instead of the generic empty state when multiple elements are selected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): prevent browser launch timeout from crashing dev server The shared Puppeteer browser pool in getSharedBrowser() could throw a 30s TimeoutError during launch. This error propagated as an uncaught rejection and killed the vite process, even though generateThumbnail had its own try/catch — the browser launch promise rejected outside that scope. Now getSharedBrowser itself catches launch failures and returns null, so thumbnails degrade gracefully instead of crashing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): revert motion panel default to false Motion panel stays opt-in via env var per product direction. Only the Design panel is enabled by default. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): prevent read-only property crash in manual edit wrappers The seek/play/applyAfter wrapper functions in manualEdits.ts crashed with "Cannot set property X which has only a getter" when the player or timeline objects define seek/play as getter-only properties. This prevented ALL manual edits (position, rotation, size) from persisting to disk — the error thrown during applyCurrentStudioManualEditsToPreview aborted the save queue. Wrapped all three property assignments in try/catch so wrapping gracefully degrades when the target object is non-configurable. Verified: position edit (X=42px) now persists to .hyperframes/studio-manual-edits.json and survives page refresh. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: alpha preview e2e fixes — exports, init templates, EPIPE crash Three bugs found via automated e2e testing of the v0.6.0-alpha preview: 1. core: add missing package.json export specifiers for studio-api/manual-edits-render-script and studio-api/studio-motion-render-script — the alpha.3 npm publish failed because the studio build could not resolve these sub-paths. 2. cli: fix init --example creating empty projects — tsup leaves empty template directories in dist/ during the build, causing existsSync(templateDir) to return true and skip the remote fetch fallback. Now checks for index.html inside the dir instead. 3. engine: fix unhandled EPIPE crash in streaming encoder — ffmpeg stdin/stdout had no error handlers, so a write after the ffmpeg process exits throws an uncaught error that crashes the process. Verified with 8 consecutive e2e iterations (424 test runs, 0 flaky). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): thumbnail crash, feature defaults, multi-select UX, fps selector Power-user audit fixes for the alpha studio: - vite.config.ts: wrap thumbnail generation in try/catch so Puppeteer TimeoutError doesn't crash the entire vite dev server as an uncaught rejection. Close the page on error to prevent browser session leaks. - manualEditingAvailability.ts: enable motion panel and manual canvas drag editing by default (were both false, undiscoverable without knowing the env vars). - PropertyPanel.tsx: show "N elements selected" feedback when multiple elements are selected instead of the generic "Select an element" empty state. - RenderQueue.tsx + App.tsx: add FPS selector (24/30/60) to the render export bar instead of hardcoding 30fps. Pass the user's choice through to startRender. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: release v0.6.0-alpha.4 * fix(runtime): update clock duration when root timeline is late-bound Compositions with external sub-compositions (like apple-presentation with 7 slides) load child compositions via fetch(). The root GSAP timeline is only bound after all external compositions finish loading, but the TransportClock duration was only set during initial setup. When bindRootTimelineIfAvailable runs after the external compositions load, it captures the root timeline but never updates the clock. player.getDuration() continues returning 0, so the player's probe interval never fires the 'ready' event, and the Studio shows "Loading composition" indefinitely. Now bindRootTimelineIfAvailable updates clock.setDuration when the root timeline is late-bound. Guarded with try/catch for the early call site where clock is not yet initialized (temporal dead zone). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): block element selection while composition is loading Prevent users from selecting elements in the preview while the composition is still loading (showing "Loading composition" overlay). Selection and hover highlighting are suppressed until the player fires the ready event. Also reverts motion panel and manual drag editing defaults to false — these were accidentally set to true during the PR #693 merge. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: release v0.6.0-alpha.5 * chore: release v0.6.0-alpha.6 * fix(runtime): remove per-tick timeline.pause() that causes audio stutter The seekRuntimeTimeline helper added timeline.pause() before every totalTime() seek. During transport-driven playback, this runs 60 times per second, causing GSAP to cascade pause events to media elements on every frame. The result: audio plays/stops/plays/stops in a stutter pattern. The captured root timeline is already paused once in player.play() — the TransportClock drives it via totalTime(t) which keeps it paused. The extra per-tick pause() was redundant for the root timeline but actively harmful for media sync. Fix: restore the original inline seek for the captured timeline (totalTime without pause), keep seekRuntimeTimeline with pause() only for standalone child timelines where explicit pause control is needed. Also fixes rebase artifact: missing PropertyPanel props in App.tsx. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: release v0.6.0-alpha.7 * fix(studio): restore text field handlers lost in rebase Restores handleDomAddTextField and handleDomRemoveTextField that were dropped when resolving App.tsx conflicts during the main→next rebase. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: release v0.6.0-alpha.8 * fix(runtime): comprehensive audio stutter fix Three changes that together caused audio play/stop/play/stop stutter during transport-driven playback: 1. seekRuntimeTimeline called timeline.pause() before every totalTime() seek, 60x per second. GSAP cascades pause to media elements on every frame. Fix: restore original inline seek for the captured timeline (totalTime without pause). The timeline is already paused once in player.play(). seekRuntimeTimeline with pause() remains only for standalone child timelines. 2. player.play() removed the !tl guard, allowing play without a captured timeline. But getSafeTimelineDurationSeconds(null) returns 0, so the clock has no duration → immediately reaches end → stops → restarts. Fix: when no timeline provides duration, fall back to the root composition element's data-duration attribute. 3. Audio source attachment added networkState guard that could cause the clock to flicker between audio-source and monotonic timing on transient media states. Fix: keep !rawEl.error guard (prevents errored audio from freezing the clock) but drop the networkState check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(runtime): skip drift corrections on playing video elements Seeking a playing video resets the browser's decoder pipeline, causing a ~150ms freeze while it re-buffers. During that freeze the monotonic clock advances, drift grows, and strict sync fires another seek — creating a perpetual stutter loop (176 seek events / 8s observed on the apple-presentation composition). Skip strict and force drift corrections for playing video elements; only hard sync (>0.5s catastrophic drift) warrants the decoder-reset cost. Audio elements are unaffected and retain the full correction tiers. Also propagate the asset-loading overlay state to the timeline so controls are disabled during "Preparing preview assets", matching the existing behavior for the initial composition loading overlay. * chore: release v0.6.0-alpha.9 * feat(studio): consolidate keyboard shortcuts into single handler Move all window-level keyboard shortcuts from 4 separate files into one `handleAppKeyDown` listener in App.tsx: - Shift+T: toggle timeline (was App.tsx, separate useMountEffect) - Cmd/Ctrl+Z: undo (was App.tsx, separate useEffect) - Cmd/Ctrl+Shift+Z: redo (was App.tsx, separate useEffect) - Cmd/Ctrl+1: sidebar Compositions tab (was LeftSidebar.tsx) - Cmd/Ctrl+2: sidebar Assets tab (was LeftSidebar.tsx) - Delete/Backspace: remove selected element (was Timeline.tsx) LeftSidebar exposes a ref handle for tab switching. Timeline watches selectedElement becoming null to clean up popover/range UI state. History hotkey kept as named function for iframe forwarding. Playback shortcuts (Space, J/K/L, arrows) and caption nudge remain in their component hooks — tightly coupled to component state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): sidebar tab overflow + hot-reload double-refresh 1. Sidebar tabs: use equal 1fr columns, shorter "Comps" label, truncate on overflow, tighter padding. Fixes tabs clipping outside the rounded pill at narrow sidebar widths. 2. Hot reload: set domEditSaveTimestampRef before every save-then-refresh path (source editor, timeline move/resize/delete, asset drop). The file-change watcher already checks this timestamp and suppresses echoed events — but source editor saves and timeline operations weren't setting it, causing a double refreshKey increment that could leave the player in a non-playable state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): delete key removes preview-selected elements The consolidated keyboard handler only checked selectedElementId (timeline clips). When a user selected a child element in the preview via the inspector, selectedElementId was null because the element didn't correspond to a top-level timeline clip, so Delete/Backspace did nothing. Add handleDomEditElementDelete that removes the element referenced by the current domEditSelection via the remove-element mutation API. The Delete key handler now falls through from timeline selection to DOM edit selection. * fix(studio): remove unused deleteInFlightRef from Timeline Leftover from moving Delete handling to the consolidated keyboard handler in App.tsx. Also suppress pre-existing exhaustive-deps warning on the intentional every-render selection-change watcher. * fix(studio): forward all keyboard shortcuts to preview iframe The consolidated handleAppKeyDown was only added to the parent window. When focus was inside the preview iframe (after clicking an element), keydown events didn't reach the parent, so Delete and other shortcuts didn't fire. Replace the per-function iframe forwarding (handleTimelineToggleHotkey only) with the full app-level handler via a ref-stable wrapper. All app shortcuts (Delete, Undo/Redo, Shift+T, Cmd+1/2) now work from within the preview iframe. * fix(core): search inside <template> content when removing elements linkedom's document.querySelectorAll does not traverse <template> content. Elements in template-based compositions (like .title-word, .bullet-text) were invisible to the removal logic, so delete returned changed: false and the element survived the reload. Fall back to template.querySelectorAll when the document-level query returns no matches. Uses template.querySelectorAll directly (not template.content.querySelectorAll) because removing from the content DocumentFragment doesn't update the serialized output. * fix(studio): suppress loading overlay on hot-reload Only show the composition loading overlay on the first iframe load. Hot-reloads (source editor save, timeline edits, element delete) no longer flash the full-screen loading state. * fix(studio): reorder design panel, fix stroke height, rename Blending - Move Text section to the top of the panel (before Layout) - Remove Selection Colors section - Rename "Blending" to "Transparency" - Fix stroke Width/Style height mismatch by making SelectField use inline label layout matching MetricField * fix(studio): prevent panel scroll when wheel-adjusting metric inputs React registers onWheel passively, so preventDefault had no effect on the parent scroll container. Replace with a native wheel listener (passive: false) that blocks both default scroll and propagation. * chore: release v0.6.0-alpha.10 * chore: release v0.6.0-alpha.11 * fix(studio): clean next alpha inspector artifacts * chore: release v0.6.0-alpha.12 * fix(studio,player,core): eliminate double audio and manifest polling loop (#722) Three bugs that compound in Studio preview: 1. **Double audio on pause/resume**: syncRuntimeMedia played audio through the HTML <audio> element while WebAudioTransport simultaneously played the same source through AudioBufferSourceNode. Fixed by passing webAudio.isActive() as outputMuted so HTML elements stay muted when Web Audio owns playback. Also removed the priorMuted restore in stopAll() which raced with the next play cycle. 2. **Manifest polling loop**: applyStudioManualEditsToPreview and applyStudioMotionToPreview unconditionally fetched from disk on every call, even without forceFromDisk. The runtime posts state messages every frame via postMessage, triggering React re-renders that re-invoked these functions ~60x/second. Fixed by returning early when no disk read is requested, and using refs instead of callbacks in useEffect deps. 3. **Parent proxy double-play**: the player web component created parent-frame audio proxies even when the runtime bridge was available, causing two audio sources on autoplay-blocked promotion. Fixed by skipping proxy creation when _hasRuntimeBridge returns true, and synchronously muting iframe media on promotion to close the async race window. Also fixes pre-existing ResolutionPreset type missing square variants. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): improve font picker and text property controls (#736) - Line height and letter-spacing: convert from free-text to select with presets - Font style: remove oblique (browser falls back to italic), keep normal/italic - Font weight: detect available weights via document.fonts.check(), add labels - Font source: local fonts matching Google catalog tagged as Google - Font list: balanced per-source caps prevent any source from being cut off - Sort order: Google fonts rank before Local so curated fonts appear first Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): inspector visibility, undo/redo blinking, and preview caching Inspector picks invisible elements when an ancestor has GSAP-set opacity: 0 because CSS opacity is not inherited — getComputedStyle on the child still returns 1. Walk the ancestor chain in the picker, domEditing, and overlay visibility checks to catch this. Also: - Containers with all-invisible children are no longer selectable - Selection/hover overlay hides during playback and while loading - Undo/redo no longer double-refreshes (echo suppression for all file writes) - Undo/redo reloads iframe in-place instead of recreating the Player, preserving shader transition cache - Preview routes return ETag + Cache-Control headers; composition HTML uses project signature for conditional 304, binary assets use mtime+size - Loading overlay deferred 400ms so cached loads never flash it * fix(studio): remove timeline inspector buttons, enable manual dragging Remove the eye icon (inspector) and image icon (thumbnail toggle) from timeline clips. The timeline layer inspector feature and all supporting code is removed. Enable manual dragging in the preview by default. Add scrub-to-drag on X/Y/W/H fields in the design panel. Hide the Radius section when the element has no visible background. Fix pre-existing ResolutionPreset type for square presets. * chore: release v0.6.0-alpha.13 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): add rotation field, inline element drag, fix manifest load regression (#743) - Add rotation (R) field to geometry row (X, Y, W, H, R) in property panel. Goes through manifest via handleDomRotationCommit, resettable with Reset Edits. - Auto-promote display:inline elements to inline-block when dragged so translate works on inline spans. - Fix regression from polling fix: iframe load now passes readFromDiskFirst to load manifest from disk, so Reset Edits finds existing entries. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(studio): decompose App.tsx monolith (4297 → 567 lines) (#741) * refactor(studio): decompose App.tsx from 4297 to 567 lines Break the monolithic StudioApp component into focused modules: Hooks (12 new): - usePanelLayout: resizable/collapsible panel state - useFileManager: file tree, CRUD, uploads, derived lists - useManifestPersistence: manual edit + motion manifest save queue - useTimelineEditing: clip move/resize/delete/drop handlers - useDomEditSession: DOM selection, style/text commits, preview interaction - useAppHotkeys: keyboard shortcuts, undo/redo, iframe hotkey sync - useCaptionDetection: auto-detect caption compositions - useRenderClipContent: timeline clip thumbnail rendering - useConsoleErrorCapture: preview iframe console error capture - useFrameCapture: frame capture download flow - useLintModal: lint execution and modal state - useCompositionDimensions: stage-size message listener Components (6 new): - AskAgentModal: agent prompt modal - StudioHeader: toolbar with undo/redo, capture, inspector toggle - StudioLeftSidebar: file tree + code editor (handles collapsed state) - StudioPreviewArea: NLELayout + overlays + caption timeline - StudioRightPanel: Design/Motion/Renders tab panel - TimelineToolbar: zoom controls + timeline toggle Utilities (4 new): - studioHelpers: types, path helpers, DOM utilities - studioPreviewHelpers: preview pointer/player interaction - domEditHelpers: selection group algebra - studioFontHelpers: font injection + @font-face management Also removes dead timeline layer inspector code (eye icon, thumbnail toggle, layer panel) that was disabled behind a feature flag. * feat(studio): add Layer (z-index) field to design panel Adds a scrub-enabled "Layer" field below the W/H inputs in the Layout section. Available for all elements regardless of style editing capability since z-index is fundamental to composition stacking order. * docs: architecture spec for studio domain contexts, hook split, and file-size lint * docs: implementation plan for studio contexts, hook split, and file-size lint * refactor(studio): consolidate duplicate helpers in useDomEditSession Remove ~370 lines of helper functions that were copied into the hook instead of imported. All removed functions already exist in the canonical utility files (studioHelpers, studioFontHelpers, studioPreviewHelpers, domEditHelpers). Also removes the duplicate local type definitions for RightPanelTab, AgentModalAnchorPoint, and PreviewLocalPointer, and drops now-unused imports (googleFontStylesheetUrl, importedFontFaceCss, resolveVisualDomEditSelectionTarget, DomEditViewport). Temporarily excludes useDomEditSession.ts from the 500 LOC file-size check until Tasks 3-5 split it into focused hooks. * refactor(studio): extract useDomSelection from useDomEditSession * refactor(studio): extract useAskAgentModal from useDomEditSession * refactor(studio): extract usePreviewInteraction from useDomEditSession * refactor(studio): extract useDomEditCommits, useDomEditSession now thin orchestrator Split the 897-line useDomEditSession into focused hooks: - useDomEditCommits (439 LOC): manifest commits (path offset, box size, rotation, manual edits reset, motion), persist operations, element delete, font asset resolution - useDomEditTextCommits (329 LOC): style/text/text-field commits - useDomEditSession (339 LOC): thin orchestrator wiring selection, agent modal, preview interaction, and commit hooks All files now under 500 LOC limit. Removed the temporary lefthook filesize exclusion for useDomEditSession. * feat(studio): add 4 domain contexts (PanelLayout, FileManager, DomEdit, Studio) Create context providers that wrap hook return values for prop-drilling elimination. Each context destructures and reconstructs the value inside useMemo so exhaustive-deps is satisfied and re-renders are minimized. Not yet wired into App.tsx — that comes in a follow-up. * refactor(studio): wire domain contexts, eliminate prop drilling in 4 components Wire StudioProvider, PanelLayoutProvider, FileManagerProvider, and DomEditProvider in App.tsx. Migrate StudioHeader, StudioLeftSidebar, StudioPreviewArea, and StudioRightPanel to consume contexts instead of props. Prop counts reduced: - StudioHeader: 13 -> 6 - StudioLeftSidebar: 19 -> 4 - StudioPreviewArea: 37 -> 11 - StudioRightPanel: 39 -> 3 Net: -118 lines, 108 props removed from call sites. * chore: upgrade to React 19 Upgrade react and react-dom from 18.3 to 19.2.6 across the workspace. Add resolutions/overrides in root package.json to prevent peer dependency pins (e.g. @phosphor-icons/react) from pulling React 18. Regenerate bun.lock. This enables the React 19 context syntax (<Context value={...}>) used by the new domain contexts. * fix(studio): refresh preview after z-index change so stacking updates visually * fix(studio): remove duplicate duration override causing oscillation The timeline message handler set the duration twice: once via processTimelineMessage and once via a raw durationInFrames override. When drilled into a sub-composition, these could disagree, causing the duration to oscillate after element deletion. * fix(studio): use in-place iframe reload after clip delete, remove confirm dialogs Two changes to fix duration oscillation after deleting a timeline clip: 1. Replace setRefreshKey (full Player remount) with in-place iframe.contentWindow.location.reload() after deleting a clip. The full remount triggered a chaotic re-probing cycle with multiple duration sources (adapter, manifest, postMessage) fighting each other, causing the timeline to oscillate between durations. In-place reload preserves the Player web component and its state. 2. Remove window.confirm dialogs from both timeline clip delete and DOM element delete. Undo is available so the confirmation adds friction without value. * chore: gitignore docs/superpowers * feat(studio): add favicon * perf(studio): skip no-op state updates in timeline sync syncTimelineElements was called 60+ times per page load, each time triggering setElements/setDuration/setTimelineReady even when nothing changed. This caused massive re-render churn and memory usage. Add early-return guards to skip updates when values haven't changed. Also fixes the duration oscillation after element delete. * refactor(studio): split PropertyPanel.tsx (3126 LOC) into 8 focused modules The monolithic PropertyPanel.tsx exceeded the 500 LOC filesize limit. Split into cohesive modules by responsibility: - propertyPanelHelpers.ts (401) — pure utility functions, shared types/constants - propertyPanelPrimitives.tsx (357) — CommitField, MetricField, DetailField, SliderControl, SegmentedControl, SelectField, Section - propertyPanelColor.tsx (371) — ColorField, ColorSlider - propertyPanelFill.tsx (421) — ImageFillField, GradientField, asset path helpers - propertyPanelFont.tsx (455) — FontFamilyField + font catalog helpers - propertyPanelSections.tsx (453) — TextSection, TextFieldEditor, text controls - propertyPanelStyleSections.tsx (411) — StyleSections (stroke, effects, clip, fill) - PropertyPanel.tsx (347) — main component, LayerTree, re-exports for consumers All re-exports from PropertyPanel.tsx preserved for backwards compatibility. No behavioral changes — pure structural split. * fix(studio): use in-place iframe reload for all timeline operations Replace setRefreshKey with in-place iframe reload for move, resize, and asset drop — matching delete which was already fixed. Prevents the Player remount probe cycle that causes duration oscillation. * perf(studio): replace 5s polling loop with event-driven adapter init The Player's onIframeLoad used a setInterval polling loop (25 attempts × 200ms = 5 seconds) to detect when the runtime's __player/__timeline globals appeared. Each poll that missed triggered wasted work, and multiple duration sources fighting during the probe cycle caused oscillation bugs. Replace with event-driven initialization: 1. Fast path: try initializeAdapter() immediately (works for in-place reloads where the adapter is already present) 2. If not ready, listen for the runtime's "state"/"timeline" postMessage signals and initialize on the first one 3. Single 5s timeout as safety net (replaces 25 interval ticks) This eliminates the polling overhead, reduces setDuration/setElements calls to exactly 1 per load, and makes the Player responsive within one frame of the runtime being ready instead of up to 200ms later. * fix(studio): prevent duration oscillation after element delete Two fixes for the duration display oscillating between sub-composition and master durations after deleting an element in the preview: 1. Clear store elements before iframe reload in handleDomEditElementDelete. Without this, stale pre-delete elements remain in the store and cause mergeTimelineElementsPreservingDowngrades to alternate between REPLACE and PRESERVE modes as the element count fluctuates. 2. Add 500ms cooldown on enrichMissingCompositions after timeline messages. The "state" handler was calling enrichMissingCompositions every ~80ms, which added extra elements from GSAP timelines. These fought with the authoritative element list from "timeline" messages (~333ms), creating a feedback loop where element count oscillated and triggered alternating merge strategies with different durations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): single reloadPreview as source of truth for preview refresh Create reloadPreview() in App.tsx that encapsulates the correct behavior (in-place iframe reload with setRefreshKey fallback). Pass it as the sole refresh mechanism to hooks, removing direct setRefreshKey access from useTimelineEditing and useDomEditCommits. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(studio): decompose App.tsx from 4297 to 567 lines Break the monolithic StudioApp component into focused modules: Hooks (12 new): - usePanelLayout: resizable/collapsible panel state - useFileManager: file tree, CRUD, uploads, derived lists - useManifestPersistence: manual edit + motion manifest save queue - useTimelineEditing: clip move/resize/delete/drop handlers - useDomEditSession: DOM selection, style/text commits, preview interaction - useAppHotkeys: keyboard shortcuts, undo/redo, iframe hotkey sync - useCaptionDetection: auto-detect caption compositions - useRenderClipContent: timeline clip thumbnail rendering - useConsoleErrorCapture: preview iframe console error capture - useFrameCapture: frame capture download flow - useLintModal: lint execution and modal state - useCompositionDimensions: stage-size message listener Components (6 new): - AskAgentModal: agent prompt modal - StudioHeader: toolbar with undo/redo, capture, inspector toggle - StudioLeftSidebar: file tree + code editor (handles collapsed state) - StudioPreviewArea: NLELayout + overlays + caption timeline - StudioRightPanel: Design/Motion/Renders tab panel - TimelineToolbar: zoom controls + timeline toggle Utilities (4 new): - studioHelpers: types, path helpers, DOM utilities - studioPreviewHelpers: preview pointer/player interaction - domEditHelpers: selection group algebra - studioFontHelpers: font injection + @font-face management Also removes dead timeline layer inspector code (eye icon, thumbnail toggle, layer panel) that was disabled behind a feature flag. * docs: architecture spec for studio domain contexts, hook split, and file-size lint * docs: implementation plan for studio contexts, hook split, and file-size lint * refactor(studio): consolidate duplicate helpers in useDomEditSession Remove ~370 lines of helper functions that were copied into the hook instead of imported. All removed functions already exist in the canonical utility files (studioHelpers, studioFontHelpers, studioPreviewHelpers, domEditHelpers). Also removes the duplicate local type definitions for RightPanelTab, AgentModalAnchorPoint, and PreviewLocalPointer, and drops now-unused imports (googleFontStylesheetUrl, importedFontFaceCss, resolveVisualDomEditSelectionTarget, DomEditViewport). Temporarily excludes useDomEditSession.ts from the 500 LOC file-size check until Tasks 3-5 split it into focused hooks. * refactor(studio): extract useDomSelection from useDomEditSession * refactor(studio): extract useAskAgentModal from useDomEditSession * refactor(studio): extract usePreviewInteraction from useDomEditSession * refactor(studio): extract useDomEditCommits, useDomEditSession now thin orchestrator Split the 897-line useDomEditSession into focused hooks: - useDomEditCommits (439 LOC): manifest commits (path offset, box size, rotation, manual edits reset, motion), persist operations, element delete, font asset resolution - useDomEditTextCommits (329 LOC): style/text/text-field commits - useDomEditSession (339 LOC): thin orchestrator wiring selection, agent modal, preview interaction, and commit hooks All files now under 500 LOC limit. Removed the temporary lefthook filesize exclusion for useDomEditSession. * refactor(studio): wire domain contexts, eliminate prop drilling in 4 components Wire StudioProvider, PanelLayoutProvider, FileManagerProvider, and DomEditProvider in App.tsx. Migrate StudioHeader, StudioLeftSidebar, StudioPreviewArea, and StudioRightPanel to consume contexts instead of props. Prop counts reduced: - StudioHeader: 13 -> 6 - StudioLeftSidebar: 19 -> 4 - StudioPreviewArea: 37 -> 11 - StudioRightPanel: 39 -> 3 Net: -118 lines, 108 props removed from call sites. * fix(studio): refresh preview after z-index change so stacking updates visually * fix(studio): remove duplicate duration override causing oscillation The timeline message handler set the duration twice: once via processTimelineMessage and once via a raw durationInFrames override. When drilled into a sub-composition, these could disagree, causing the duration to oscillate after element deletion. * fix(studio): use in-place iframe reload after clip delete, remove confirm dialogs Two changes to fix duration oscillation after deleting a timeline clip: 1. Replace setRefreshKey (full Player remount) with in-place iframe.contentWindow.location.reload() after deleting a clip. The full remount triggered a chaotic re-probing cycle with multiple duration sources (adapter, manifest, postMessage) fighting each other, causing the timeline to oscillate between durations. In-place reload preserves the Player web component and its state. 2. Remove window.confirm dialogs from both timeline clip delete and DOM element delete. Undo is available so the confirmation adds friction without value. * chore: gitignore docs/superpowers * perf(studio): skip no-op state updates in timeline sync syncTimelineElements was called 60+ times per page load, each time triggering setElements/setDuration/setTimelineReady even when nothing changed. This caused massive re-render churn and memory usage. Add early-return guards to skip updates when values haven't changed. Also fixes the duration oscillation after element delete. * refactor(studio): split PropertyPanel.tsx (3126 LOC) into 8 focused modules The monolithic PropertyPanel.tsx exceeded the 500 LOC filesize limit. Split into cohesive modules by responsibility: - propertyPanelHelpers.ts (401) — pure utility functions, shared types/constants - propertyPanelPrimitives.tsx (357) — CommitField, MetricField, DetailField, SliderControl, SegmentedControl, SelectField, Section - propertyPanelColor.tsx (371) — ColorField, ColorSlider - propertyPanelFill.tsx (421) — ImageFillField, GradientField, asset path helpers - propertyPanelFont.tsx (455) — FontFamilyField + font catalog helpers - propertyPanelSections.tsx (453) — TextSection, TextFieldEditor, text controls - propertyPanelStyleSections.tsx (411) — StyleSections (stroke, effects, clip, fill) - PropertyPanel.tsx (347) — main component, LayerTree, re-exports for consumers All re-exports from PropertyPanel.tsx preserved for backwards compatibility. No behavioral changes — pure structural split. * fix(studio): use in-place iframe reload for all timeline operations Replace setRefreshKey with in-place iframe reload for move, resize, and asset drop — matching delete which was already fixed. Prevents the Player remount probe cycle that causes duration oscillation. * perf(studio): replace 5s polling loop with event-driven adapter init The Player's onIframeLoad used a setInterval polling loop (25 attempts × 200ms = 5 seconds) to detect when the runtime's __player/__timeline globals appeared. Each poll that missed triggered wasted work, and multiple duration sources fighting during the probe cycle caused oscillation bugs. Replace with event-driven initialization: 1. Fast path: try initializeAdapter() immediately (works for in-place reloads where the adapter is already present) 2. If not ready, listen for the runtime's "state"/"timeline" postMessage signals and initialize on the first one 3. Single 5s timeout as safety net (replaces 25 interval ticks) This eliminates the polling overhead, reduces setDuration/setElements calls to exactly 1 per load, and makes the Player responsive within one frame of the runtime being ready instead of up to 200ms later. * fix(studio): prevent duration oscillation after element delete Two fixes for the duration display oscillating between sub-composition and master durations after deleting an element in the preview: 1. Clear store elements before iframe reload in handleDomEditElementDelete. Without this, stale pre-delete elements remain in the store and cause mergeTimelineElementsPreservingDowngrades to alternate between REPLACE and PRESERVE modes as the element count fluctuates. 2. Add 500ms cooldown on enrichMissingCompositions after timeline messages. The "state" handler was calling enrichMissingCompositions every ~80ms, which added extra elements from GSAP timelines. These fought with the authoritative element list from "timeline" messages (~333ms), creating a feedback loop where element count oscillated and triggered alternating merge strategies with different durations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): single reloadPreview as source of truth for preview refresh Create reloadPreview() in App.tsx that encapsulates the correct behavior (in-place iframe reload with setRefreshKey fallback). Pass it as the sole refresh mechanism to hooks, removing direct setRefreshKey access from useTimelineEditing and useDomEditCommits. * fix: resolve lint errors from rebase (unused imports, duplicate declarations) * fix: prefix unused probeResult variable * fix: restore renderOrchestrator.ts from origin/next (rebase conflict artifact) * fix: resolve rebase conflicts by using main's producer and next's studio/player * fix: restore rebase-conflicted files from origin/next * fix: use 'load' instead of 'networkidle0' for Puppeteer waitUntil (type compatibility) * fix: restore webAudioTransport.ts from main (test compatibility) --------- Co-authored-by: Vance Ingalls <vance@heygen.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
15f9fb711a |
fix(studio): skip deferred media in hasUnloadedAssets check
Elements with preload!='auto' are intentionally deferred by the media preloader and should not block the loading overlay. |
||
|
|
a94ca4f5d3 |
fix(studio): adapt sidebar thumbnail container to composition aspect ratio (#728)
Portrait compositions (1080x1920) rendered pillarboxed inside a fixed 80x45px landscape container, wasting space with black bars on both sides. The thumbnail container now derives its dimensions from the composition's stage size: landscape gets 80px wide, portrait gets 45px tall. The preview scale calculation uses matching card dimensions so the iframe fills the container without letterboxing. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
aa715ca8a0 |
feat(core,studio,cli): add square + square-4k canvas resolutions
The four existing presets only cover 16:9 (landscape) and 9:16 (portrait) aspect ratios. A 1080×1080 square comp had nowhere to land at any scale: "Auto" rendered at the comp's authored 1080×1080, and picking 1080p or 4K mapped to a landscape/portrait preset whose aspect ratio mismatched, which the producer's resolveDeviceScaleFactor validator rejects with "does not match the aspect ratio of the composition". Add `square` (1080×1080) and `square-4k` (2160×2160) to CANVAS_DIMENSIONS in core. The existing `keyof typeof CANVAS_DIMENSIONS` derivation extends the `CanvasResolution` union and `VALID_CANVAS_RESOLUTIONS` array automatically, so the producer's validator, the render API route, and the CLI `--resolution` flag pick the new presets up without further changes. - core: extend CANVAS_DIMENSIONS, RESOLUTION_ALIASES, and the htmlParser to recognize `data-resolution="square|square-4k"` and to infer square from equal width/height (vs. the prior "square defaults to portrait" tie-breaker). - studio: extend the local ResolutionPreset / CANVAS_DIMENSIONS mirrors; collapse isPortraitComp into a 3-way `compAspect` helper so resolveResolution returns the square preset for square comps. - cli: update --resolution help text on `init` and `render` to mention the new presets. - tests: add square cases to renderOrchestrator's resolveDeviceScaleFactor suite (returns 1 for square→square, 2 for square→square-4k, rejects landscape preset on square comp), update the htmlParser test that previously pinned the "square→portrait" tiebreaker. |
||
|
|
976ceabedc |
refactor(studio): simplify dropdown helpers + use stage-size message for dims
Cleanup from the /simplify pass on PR #715. - App.tsx: subscribe to the runtime's `stage-size` message (which carries authoritative width/height post-applyCompositionSizing) instead of re-parsing data-width/data-height from the iframe DOM. Drops the cross-origin try/catch, querySelector, and parseInt logic, and fires once per comp load instead of on every state/timeline tick. - App.tsx: import CompositionDimensions from RenderQueue instead of inlining the shape. - RenderQueue.tsx: replace scaleLabel() with a SCALE_LABEL record, inline the one-call formatDims helper, and trim the type comment to the WHY. |
||
|
|
b22252ee8b |
feat(studio): collapse render resolution dropdown to Auto / 1080p / 4K
Orientation is a property of the composition, not a user choice — the backend's portrait/landscape presets are tied to the comp's authored aspect ratio. Letting users pick "1080p portrait" for a landscape composition just produces a wrong-aspect render. The dropdown now exposes three scale choices (Auto / 1080p / 4K) and maps to the correct portrait/landscape preset based on the active composition's data-width / data-height. Native <select title> tooltips are unreliable across browsers, so the resolved dimensions render inline in each option label (e.g. "1080p · 1920×1080") — always visible, no hover needed. App.tsx tracks the active comp's dimensions by listening for the existing hf-preview state/timeline postMessages (same source the caption-detection logic uses) and passes them to RenderQueue. The useRenderQueue / backend contract is unchanged: RenderQueue still emits "landscape" | "portrait" | "landscape-4k" | "portrait-4k" | "auto". |
||
|
|
2b2281d490 | refactor: dedupe resolution presets and clean up 4k stack | ||
|
|
bc02b8810c | fix(studio): exhaustively type RESOLUTION_OPTIONS and document layout coupling | ||
|
|
25edba803a | feat(studio): add resolution selector to render export bar | ||
|
|
a7b308b667 |
feat: cache shader transition preview frames (#634)
* feat: cache shader transition preview frames * fix: move shader transition loading to player |
||
|
|
49d4dbbadb |
fix: use JSON.stringify for caption text escaping in generated JS
The generated JavaScript used manual single-quote escaping (replace(/\\/g, '\\\\').replace(/'/g, \"'\\\\''\")) which missed newlines, carriage returns, and other special characters. Captions containing line breaks would produce syntactically broken JS. Use JSON.stringify which handles all JS string special characters correctly, including newlines, unicode, and control characters. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
26b8e2a985 |
Revert "feat: Persist Studio manual edits via manifest (#593)"
This reverts commit
|
||
|
|
8d83d4f132 |
fix: make caption overrides refresh-safe (#609)
## Summary This stacked PR makes caption overrides refresh-safe. Caption edits are still saved to `caption-overrides.json`, but override targets are now stable across preview refreshes and regenerated caption HTML. ## Architecture - **Stable word identity**: generated caption HTML preserves optional transcript word IDs in the `TRANSCRIPT` array and emits those IDs on word spans. - **Parser continuity**: the caption parser preserves existing transcript word `id` fields instead of regenerating index-only identity. - **Override loading**: Studio loads saved overrides by `wordId` first, with the existing `wordIndex` fallback kept for older overrides. - **Idempotent runtime wrapping**: transform overrides reuse an existing `data-caption-wrapper="true"` wrapper instead of nesting wrappers on every refresh. - **Animation compatibility**: overrides still wrap the word so inner word-level GSAP animation can continue to target the original span. ## User Impact Users can edit caption word position, scale, rotation, color, opacity, font size, font weight, and font family, then refresh without overrides drifting to the wrong word or accumulating nested wrappers. ## Main Files - `packages/core/src/runtime/captionOverrides.ts` - `packages/studio/src/captions/generator.ts` - `packages/studio/src/captions/parser.ts` - `packages/studio/src/captions/hooks/useCaptionSync.ts` ## Test Plan ```bash volta run --node 22.20.0 bun run --filter @hyperframes/core test -- src/runtime/captionOverrides.test.ts volta run --node 22.20.0 packages/studio/node_modules/.bin/vitest run --root packages/studio --config /dev/null src/captions/parser.test.ts src/captions/generator.test.ts volta run --node 22.20.0 bun run --filter @hyperframes/core typecheck volta run --node 22.20.0 bun run --filter @hyperframes/studio typecheck volta run --node 22.20.0 bunx oxlint <changed files> volta run --node 22.20.0 bunx oxfmt --check <changed files> git diff --check ``` |
||
|
|
d0abe90a82 |
feat: Persist Studio manual edits via manifest (#593)
## Summary Studio manual geometry edits now persist as a project-local manifest instead of being baked into composition source on each gesture. The manifest lives at: ```text .hyperframes/studio-manual-edits.json ``` It is the source of truth for manual drag, resize, rotation, inspector geometry edits, group moves, and selected-layer reset. ## Architecture - **Manifest-backed edits**: each edit stores a kind (`path-offset`, `box-size`, `rotation`), a source-scoped target, and the edit values. - **Source-scoped resolution**: targets include `sourceFile`, `id`, `selector`, and `selectorIndex`, so duplicate selectors in nested compositions resolve against the owning source file. - **Additive CSS layer**: move uses CSS `translate`, resize writes stable dimensions/flex sizing, and rotation uses CSS `rotate` over the authored base. - **Shared replay runtime**: Studio preview, thumbnails, frame capture, producer renders, and CLI Studio renders/thumbnails all use the same core manual-edit render script. - **Animation-safe replay**: Studio reapplies the manual layer after load, refresh, timeline seeks, player operations, playback frames, thumbnail seeks, and render seeks instead of rewriting GSAP timelines. - **History and handoff**: the manifest is a normal project file, so undo/redo and agent edits can preserve, modify, or remove manual visual edits explicitly. ## User Impact Users can move, resize, rotate, group-move, and reset supported layers from the canvas or inspector, then refresh, capture thumbnails/screenshots, play animated compositions, and render videos without manual edits drifting away from the edited state. ## Main Files - `packages/studio/src/components/editor/manualEdits.ts` - `packages/studio/src/components/editor/DomEditOverlay.tsx` - `packages/studio/src/components/editor/PropertyPanel.tsx` - `packages/studio/src/App.tsx` - `packages/core/src/studio-api/helpers/manualEditsRenderScript.ts` - `packages/studio/vite.config.ts` - `packages/cli/src/server/studioServer.ts` - `packages/core/src/compiler/htmlBundler.ts` - `packages/producer/src/services/htmlCompiler.ts` - `packages/core/src/studio-api/routes/thumbnail.ts` - `packages/producer/src/services/fileServer.ts` - `packages/producer/src/services/renderOrchestrator.ts` ## Test Plan ```bash volta run --node 22.20.0 bun run build volta run --node 22.20.0 bun run --filter @hyperframes/core test -- src/studio-api/helpers/manualEditsRenderScript.test.ts volta run --node 22.20.0 bun run --filter @hyperframes/core typecheck volta run --node 22.20.0 bun run --filter @hyperframes/studio typecheck volta run --node 22.20.0 bun run --filter @hyperframes/cli typecheck volta run --node 22.20.0 bunx oxlint <changed files> volta run --node 22.20.0 bunx oxfmt --check <changed files> git diff --check ``` |
||
|
|
04bd56a7ae |
fix: align Studio capture with preview (#595)
## Problem
Studio frame capture could fail for projects mounted outside the repo when the project id came from an encoded hash route. A project like `Notion Showcase` loaded as `#project/Notion%20Showcase`, but the capture URL encoded that already-encoded value again, producing `/api/projects/Notion%2520Showcase/...` and a 404.
While validating the fix by seeking through the preview, capture also diverged from the visible player for nested compositions because the thumbnail route sought raw timelines instead of the same player seek path used by Studio preview.
## What this fixes
- Decodes project ids when reading Studio `#project/...` routes and centralizes project hash/API path construction.
- Keeps API URLs encoded exactly once, including project names with spaces, literal `%`, reserved characters, and unicode.
- Updates Studio thumbnail capture to prefer `window.__player.seek(t)` and only fall back to raw timeline seeking for standalone pages.
- Preserves explicit `t=0` thumbnail requests instead of falling back to `0.5` seconds.
- Adds preview-regression CI coverage for Studio routing, frame capture URL construction, thumbnail seeking, and core thumbnail seek parsing.
## Root cause
Studio treated the hash route segment as the canonical project id even when the browser had already percent-encoded it. `buildFrameCaptureUrl` then encoded that string again, so a decoded project directory name and the capture API path no longer matched.
The preview/capture mismatch was a separate seek-path issue: the visible Studio preview seeks through the HyperFrames player, which maps global time into nested composition time. The capture route bypassed that layer and paused all registered timelines at the same global time.
The zero-second capture case came from parsing `t` with a truthiness fallback, so `parseFloat("0") || 0.5` became `0.5`.
## Verification
### Local checks
- `bun run --cwd packages/studio test -- vite.thumbnail.test.ts src/utils/projectRouting.test.ts src/utils/frameCapture.test.ts`
- `bun run --cwd packages/core test -- src/studio-api/routes/thumbnail.test.ts`
- `bunx oxfmt --check .github/workflows/preview-regression.yml packages/studio/vite.thumbnail.ts packages/studio/vite.thumbnail.test.ts packages/studio/vite.config.ts packages/studio/src/utils/projectRouting.ts packages/studio/src/utils/projectRouting.test.ts packages/studio/src/utils/frameCapture.ts packages/studio/src/App.tsx packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/routes/thumbnail.test.ts`
- `bunx oxlint .github/workflows/preview-regression.yml packages/studio/vite.thumbnail.ts packages/studio/vite.thumbnail.test.ts packages/studio/vite.config.ts packages/studio/src/utils/projectRouting.ts packages/studio/src/utils/projectRouting.test.ts packages/studio/src/utils/frameCapture.ts packages/studio/src/App.tsx packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/routes/thumbnail.test.ts`
- `bun run --cwd packages/studio typecheck`
- `bun run --cwd packages/core build:hyperframes-runtime`
- `bun run --cwd packages/core typecheck`
- `git diff --check`
Pre-commit also reran lint, format, and typecheck successfully for the committed files.
### Browser verification
Using `agent-browser`, I mounted `/Users/miguel07code/Downloads/Notion Showcase` into Studio's project data and opened:
```text
http://127.0.0.1:5197/#project/Notion%20Showcase
```
Before the fix, Capture requested `/api/projects/Notion%2520Showcase/thumbnail/index.html?...` and Studio showed `Capture failed`.
After the fix, I sought the preview to `0s`, `2s`, `10s`, and `18s`, captured each frame, and compared the visible preview crop against the capture output. The capture URLs all used `Notion%20Showcase`, not `Notion%2520Showcase`, and no failure toast appeared.
Mean pixel diffs for preview vs capture were:
- `0s`: `0.0`
- `2s`: `0.8641`
- `10s`: `0.3496`
- `18s`: `0.2309`
The small non-zero diffs are raster/antialias-level differences after resizing the capture to the preview crop dimensions.
## Notes
- Browser screenshots, comparison sheets, network logs, and the `agent-browser` recording are local-only under `qa-artifacts/capture-button/` and are not committed.
- The local Notion Showcase project mount is an ignored symlink under `packages/studio/data/projects/` and is not committed.
- Thumbnail cache versions were bumped so stale captures generated with the old seek behavior are not reused.
|
||
|
|
15ee63c6e7 |
fix: harden CLI edge-case repros (#591)
## Problem I reproduced the selected open issue batch one by one and confirmed the reports were valid. The fixes all touch the CLI/runtime capture boundary, then the follow-up regression run exposed one over-broad runtime change in sub-composition host visibility and one CI-only baseline trap. Closes #590, #589, #588, #587, #586, and #584. ## What this fixes ### CLI/runtime edge cases - Makes the GSAP infinite-repeat lint rule ignore JavaScript comments, so literal `repeat:-1` text in comments is not flagged. - Lets the compositions CLI inspect `<template>` content, count visual-only template descendants, estimate simple GSAP durations, and suppress root `data-start` warnings in sub-composition lint mode. - Preserves runtime bootstrap scripts when body scripts are coalesced, and injects the runtime into a real `<head>` when source HTML has no head. - Keeps #589 fixed by loading and rendering template-wrapped sub-composition content, while restoring host visibility to the shorter of the authored parent clip window and the child composition live timeline. - Resolves snapshot/validate viewport size from root `data-width` / `data-height` instead of falling back to 1920x1080. - Skips fully off-frame text boxes during contrast sampling and bounds-checks ring samples so contrast output no longer emits `null:1` / `NaN:1`. - Marks muted videos as `data-has-audio="false"` in the core timing compiler, which fixes the same-src muted `<video>` + separate `<audio>` StaticGuard case. - Keeps user-authored `hf-seek` listeners reachable during capture by preventing author scripts from being merged into the runtime bootstrap path. ### Shared helper cleanup - Removes the stale producer-local timing compiler duplicate; producer compilation now consumes the core timing compiler. - Centralizes HTML document helpers in core: fragment parsing, embedded runtime stripping, head/body script injection, and early-head injection. - Centralizes the CLI layout/snapshot static HTML server. - Adds browser-safe core subpath helpers for Lottie readiness and CLI screenshot clip calculation; Studio's Vite config keeps the screenshot clip helper self-contained so clean-checkout test startup does not value-import core `.ts` source. - Replaces the engine parity-contract copy with a core re-export. - De-duplicates render-job cleanup and Studio static file-serving callbacks. ### Regression hardening - Replaces the embedded-runtime script stripping regex with a script-tag scanner that handles closing tags like `</script >`. - Escapes inline script bodies before wrapping them in `<script>` tags, so authored `</script` and `<!--` text cannot break out of the injected wrapper script. - Shares media-duration clamping between core and producer, with a 50 ms tolerance for ffprobe precision drift between local and CI media stacks. - Pins the affected style fixture SFX durations in source so style-1 and style-9 compile deterministically. - Restores the `vfr-screen-recording` video golden to the CI-stable baseline; the current CI failure showed the Linux render matches the old golden, while the locally refreshed macOS golden was the mismatch. ## Root cause The CLI paths had accumulated assumptions that held for simple direct-root landscape compositions but not for current composition patterns: DOM queries did not enter template content, snapshot/validate used a fixed viewport, runtime and author scripts shared a coalescing bucket, and timing compilation treated every video as audio-bearing unless authors manually overrode it. The style shard failures were not product regressions. Local and CI media probing disagreed on the short SFX clip duration by about 45 ms, and the compiler was clamping authored durations to the locally probed value. The shared clamp tolerance preserves explicit author/source durations for small probe precision differences while still clamping real overflows. The vfr fast-shard failure was a bad baseline refresh: CI actual frames matched the old `vfr-screen-recording` baseline at 40+ dB PSNR, but mismatched the macOS-refreshed golden at ~18-22 dB. The fix is to keep the Docker/Linux-stable video golden and only retain the deterministic compiled snapshot change. The sub-composition regression came from treating a host's authored parent window as the only visibility boundary. That made settled child overlays stay visible after their own live GSAP timeline ended. The corrected runtime behavior respects both contracts: parent clips still bound where the host can appear, and the child live timeline can end the host earlier. ## Verification ### Local checks - `bunx oxfmt --check packages/core/src/runtime/init.ts packages/core/src/runtime/init.test.ts` - `bunx oxlint packages/core/src/runtime/init.ts packages/core/src/runtime/init.test.ts` - `bun run --cwd packages/core test src/runtime/init.test.ts` - `bun run --cwd packages/cli test src/commands/compositions.test.ts src/utils/compositionViewport.test.ts` - `bun run build:hyperframes-runtime` - `bun run --cwd packages/producer test --keep-temp --sequential style-12-prod style-5-prod` - `bun run --cwd packages/producer test --sequential vfr-screen-recording hdr-hlg-regression style-7-prod` - `bun run --cwd packages/core test src/compiler/htmlCompiler.test.ts src/compiler/timingCompiler.test.ts src/index.test.ts` - `bunx oxfmt --check packages/core/src/compiler/timingCompiler.ts packages/core/src/compiler/htmlCompiler.ts packages/core/src/compiler/htmlCompiler.test.ts packages/core/src/compiler/index.ts packages/core/src/index.ts packages/core/src/index.test.ts packages/producer/src/services/htmlCompiler.ts` - `bunx oxlint packages/core/src/compiler/timingCompiler.ts packages/core/src/compiler/htmlCompiler.ts packages/core/src/compiler/htmlCompiler.test.ts packages/core/src/compiler/index.ts packages/core/src/index.ts packages/core/src/index.test.ts packages/producer/src/services/htmlCompiler.ts` - `bun run --cwd packages/core typecheck` - `bun run --cwd packages/producer typecheck` - `bun run --cwd packages/producer test --sequential style-1-prod style-9-prod` - `bun run --filter @hyperframes/studio test` with `packages/core/dist` temporarily hidden to simulate clean-checkout config loading - `git diff --check` ### CI artifact checks - Inspected failed run `25225854394` job `73969147096`: style-1 failed only on `click-sfx` `1.044898` vs `1` duration/end. - Inspected failed run `25225854394` job `73969147061`: style-9 failed only on SFX `1.044898`-based duration/end mismatches. - Inspected failed run `25225854394` job `73969147048`: `vfr-screen-recording` compilation/audio passed, visual failed after comparing against the macOS-refreshed golden. - Compared the first 10 uploaded CI vfr failure frames against the restored old baseline; minimum PSNR was `40.444705`, above the fixture threshold of `28`. ### Repro checks - `bun packages/cli/src/cli.ts lint /tmp/hf-590-repro` now passes without `gsap_infinite_repeat`. - `bun packages/cli/src/cli.ts snapshot /tmp/hf-587-repro --at 0.5 --timeout 1000` now writes a 1080x1920 PNG. - `bun packages/cli/src/cli.ts validate /tmp/hf-588-repro --timeout 500` no longer emits `null:1` / `NaN:1` contrast output. - `bun packages/cli/src/cli.ts validate /tmp/hf-586-repro --timeout 500 --contrast false` no longer emits the muted-video StaticGuard contract error. - `bun packages/cli/src/cli.ts compositions /tmp/hf-589-gsap-repro` now reports `foo 0.5s 1920x1080 1 element`. - `bun packages/cli/src/cli.ts snapshot /tmp/hf-589-gsap-repro --at 0.25 --timeout 2000` captures the expected template-backed red frame. - `bun packages/cli/src/cli.ts snapshot /tmp/hf-584-repro --at 0.5,1.5 --timeout 500` captures the expected post-seek green frame. ### Browser verification - Refreshed the local side-by-side comparison page at `qa-artifacts/pr-591-video-compare/index.html`. - Served the comparison page locally and used `agent-browser` to load `style-12-prod`, play both videos quickly to the failed window, pause, and inspect the side-by-side frame. - Browser proof screenshot: `qa-artifacts/pr-591-video-compare/browser-proof/fixed-style12-labeled.png`. - Browser proof recording: `qa-artifacts/pr-591-video-compare/browser-proof/fixed-style12.webm`. - Earlier Studio proof artifacts remain local-only: `qa-artifacts/dedupe-refactor-preview.png`, `qa-artifacts/dedupe-refactor-preview-after-play.png`, `qa-artifacts/dedupe-refactor-preview.webm`. ## Notes - Browser proof and CI diagnostic artifacts are intentionally local-only and not committed. - Studio's Vite config intentionally keeps the thumbnail clip helper inline because Vite/Vitest config startup runs through Node's loader before package source `.ts` imports are transformed. - The committed PR diff changes `vfr-screen-recording/output/compiled.html` but no longer changes `vfr-screen-recording/output/output.mp4` relative to `main`. - I attempted a local `linux/amd64` Docker validation to mirror CI, but the local Docker build was blocked by Debian package download failures. The arm64 Docker image also cannot launch the x64 Puppeteer headless shell under OrbStack. The vfr baseline decision is therefore based on the uploaded CI artifact comparison above. - I kept this validated issue batch in one PR because the fixes overlap the same CLI/runtime capture surfaces. |
||
|
|
39b3997c78 |
fix(studio): warn on anonymous timeline clips (#533)
## Problem Studio timeline editing still had two rough edges that made the latest alpha feel less polished when testing it like a video editor would: - Timeline clips for anonymous DOM nodes could surface internal fallback identities like `__node__index_*`, which made the timeline look broken instead of authored. - Elements without a stable `id` could still appear in the timeline and canvas editor, but authors did not get direct lint guidance that those elements are weaker targets for Studio and agent edits. ## What this fixes - Adds a non-blocking `studio_missing_editable_id` lint warning for timeline-visible elements that do not have an `id`. - Makes the warning point to the exact element and recommend stable, human-readable ids such as `hero-title` or `scene-1-card`. - Stops using synthetic node-index ids as runtime clip identity for anonymous DOM nodes. - Gives anonymous clips readable labels from authored metadata, composition ids, DOM ids, class names, asset filenames, text content, or a simple ordinal fallback. - Keeps those labels display-only in Studio and uses key-first identity for matching, dragging, resizing, and manifest merge preservation. - Covers the duplicate-label case where two anonymous clips both render as `Card` but still stay separate timeline entries. ## Root cause The runtime manifest used synthetic node-index ids as both identity and display fallback for timeline nodes that had no stable author-provided id. Studio then treated those internal values as user-facing clip names. The first pass improved the display label, but it also risked using that friendly label as internal identity. Two anonymous clips with the same label could then collapse into the same logical timeline element. The fix separates display labels from internal identity and prefers the timeline key whenever Studio needs to match an element. The linter also had correctness checks for render and runtime behavior, but it did not teach authors when a timeline-visible element would be harder for Studio and agents to patch reliably. That left missing ids as a silent authoring quality issue instead of actionable guidance. ## Verification ### Local checks - `bun run --cwd packages/core test -- src/lint/rules/core.test.ts src/runtime/timeline.test.ts` -> 41 tests pass - `bun run --cwd packages/studio test -- src/player/hooks/useTimelinePlayer.test.ts src/player/components/timelineTheme.test.ts` -> 23 tests pass - `bun run --cwd packages/core typecheck` - `bun run --cwd packages/studio typecheck` - `bun run --cwd packages/studio build` -> passes with the existing Vite chunk-size warning - `bunx oxlint $(git diff --name-only origin/main...HEAD)` -> 0 warnings, 0 errors - `bunx oxfmt --check $(git diff --name-only origin/main...HEAD)` - `git diff --check origin/main...HEAD` ### Browser verification - Created a scratch project at `/tmp/hf-pr533-conflict-verify` with two timed anonymous `.card` clips that both label as `Card`. - Started the local Studio dev server for `pr-533-conflict-verify`. - Used `agent-browser` to verify the timeline renders two separate `Card` clips instead of collapsing duplicate anonymous labels. - Used `agent-browser` to open the Studio lint modal and verify it shows human-readable missing-id warnings, not internal node-index labels. - Used `agent-browser` to click Play after the lint pass and confirm the timeline remains usable. - Recorded the tested Studio flow with `agent-browser`. ## Notes - Rebased onto current `main`; conflict resolution preserved both the newer mainline Studio shortcut/lint behavior and this PR's anonymous-clip identity split. - GitHub Actions are running on the rebased head. - Scratch verification files are intentionally not committed. - Local screenshots and recording from this rebase pass are under `.codex-artifacts/pr-533-conflict-rebase-2026-04-29/`. |
||
|
|
3f6907e807 |
fix: keep Studio frame stepping advancing (#573)
## Problem Closes #568. Studio preview-focused frame stepping could stop advancing after a couple of ArrowLeft/ArrowRight presses. The same integer-frame stepping path also affected the K-held J/L one-frame shuttle controls. ## What this fixes - Adds a shared `stepFrameTime` helper that advances by integer frame index instead of adding fractional seconds. - Uses that helper for preview-surface keyboard shortcuts and the focused seek slider. - Adds regression coverage for truncated runtime times like `0.0333333`, which previously stepped back onto the same frame. ## Root cause The runtime seek path quantizes requested times with `Math.floor(time * fps)`. Studio was deriving the next frame from the runtime's current seconds value, which can be a truncated decimal such as `0.0333333`. Adding `1 / 30` to that value can produce `1.999998...` frames, so floor-quantization lands back on the previous frame and repeated shortcuts appear to stop responding. ## Verification ### Local checks - `bun run --filter @hyperframes/core build:hyperframes-runtime` - `bun run --filter @hyperframes/studio test -- src/player/lib/time.test.ts src/player/hooks/useTimelinePlayer.test.ts src/player/components/PlayerControls.test.ts` - `bunx oxfmt --check packages/studio/src/player/lib/time.ts packages/studio/src/player/lib/time.test.ts packages/studio/src/player/hooks/useTimelinePlayer.ts packages/studio/src/player/components/PlayerControls.tsx` - `bunx oxlint packages/studio/src/player/lib/time.ts packages/studio/src/player/lib/time.test.ts packages/studio/src/player/hooks/useTimelinePlayer.ts packages/studio/src/player/components/PlayerControls.tsx` - `bun run --filter @hyperframes/studio typecheck` - `bun run --filter @hyperframes/studio build` - `git diff --check` - Lefthook pre-commit: lint, format, typecheck - Lefthook commit-msg: commitlint ### Browser verification - Created `/tmp/hf-studio-frame-step-repro` with a 10s GSAP animation. - Started Studio preview at `http://localhost:5191/#project/hf-studio-frame-step-repro`. - Used `agent-browser` to reproduce the original stuck behavior before the fix: repeated preview-focused `ArrowRight` keydowns were handled but runtime time stayed at `0.0333333`. - Used `agent-browser` after the fix to verify preview-focused `ArrowRight` advances 10 frames to `0.3333333`. - Used `agent-browser` to verify K-held L steps forward 5 frames to `0.1666667` and K-held J steps backward from 5 frames to `0`. - Used actual Safari 18.6 with System Events key presses to verify 10 and then 20 preview-focused ArrowRight presses continue advancing visually. ## Notes - Safari WebDriver was unavailable because Safari's "Allow remote automation" setting is disabled on this machine, so the Safari check used real Safari GUI key events instead. - Local proof artifacts are intentionally not committed: - `qa-artifacts/studio-frame-step-issue-568/chrome-after-10-arrow-right.png` - `qa-artifacts/studio-frame-step-issue-568/chrome-frame-step-flow.webm` - `qa-artifacts/studio-frame-step-issue-568/safari-after-10-arrow-right.png` - `qa-artifacts/studio-frame-step-issue-568/safari-after-20-arrow-right.png` |
||
|
|
ea3b708b12 |
feat: add Studio current-frame capture (#565)
## Problem Closes #555. Studio users could inspect the preview, but there was no first-class way to capture the current rendered frame as an image. ## What this fixes - Adds a `Capture` action to the Studio header toolbar so it does not cover the video preview. - Downloads the current composition frame as a PNG using the current player time. - Extends the existing thumbnail route and Studio/CLI thumbnail generators with an explicit PNG format path while preserving JPEG thumbnails for existing previews. - Adds URL/filename utility coverage plus thumbnail route coverage for PNG requests. ## Root cause Studio already had frame thumbnail generation, but the API path was JPEG-oriented and the editor UI only used it for previews. There was no current-frame capture affordance wired to the player state. ## Verification ### Local - `bun run --filter @hyperframes/core test src/studio-api/routes/thumbnail.test.ts` - `bun run --filter @hyperframes/studio test src/utils/frameCapture.test.ts src/player/components/PlayerControls.test.ts` - `bun run --filter @hyperframes/studio typecheck` - `bun run --filter @hyperframes/core typecheck` - `bun run --filter @hyperframes/cli typecheck` - `bunx oxlint packages/cli/src/server/studioServer.ts packages/core/src/studio-api/routes/thumbnail.test.ts packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/types.ts packages/studio/src/App.tsx packages/studio/src/icons/SystemIcons.tsx packages/studio/vite.config.ts packages/studio/src/utils/frameCapture.ts packages/studio/src/utils/frameCapture.test.ts` - `bunx oxfmt --check packages/cli/src/server/studioServer.ts packages/core/src/studio-api/routes/thumbnail.test.ts packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/types.ts packages/studio/src/App.tsx packages/studio/src/icons/SystemIcons.tsx packages/studio/vite.config.ts packages/studio/src/utils/frameCapture.ts packages/studio/src/utils/frameCapture.test.ts` - `git diff --check` ### Browser <img width="1027" height="910" alt="image" src="https://github.com/user-attachments/assets/71973af4-0279-4074-9 <img width="1026" height="902" alt="Screenshot 2026-04-29 at 16 17 22" src="https://github.com/user-attachments/assets/a32e1c19-b793-40b9-82f8-de8bbb11f123" /> 060-130839a2d419" /> |
||
|
|
47b801fbf2 |
feat: add Studio NLE playback controls (#530)
## Problem HyperFrames Studio made frame-accurate playback review slower than expected for editor-style workflows. Issue #527 called out missing loop playback, frame display/jump controls, preview-focused Space handling, frame stepping, and NLE-style J/K/L shuttle controls. ## What this fixes - Adds a persistent Studio loop toggle and makes the playback loop restart when enabled. - Adds a time/frame display toggle plus a jump-to-frame input in the player controls. - Adds frame math helpers and frame-step behavior at the Studio preview frame rate. - Expands keyboard handling so preview-focused Space toggles playback, ArrowLeft/ArrowRight step frames, Shift+Arrow steps 10 frames, and J/K/L shuttle controls work from the preview/timeline surface while ignoring form/button/slider targets. - Adds J/K/L shuttle behavior: J plays backward, K pauses, L plays forward, repeated J/L ramps 1x -> 2x -> 4x, and K-held J/L frame-steps. - Makes the preview wrapper focusable so keyboard playback shortcuts work after focusing the preview area. ## Root cause The Studio playback layer only exposed mouse scrubbing, basic play/pause, a seconds-based readout, and slider-local arrow-key nudges. The global Space shortcut was also gated to `document.body`, so it stopped working once the actual preview/editor surface had focus. Studio needed a single playback-control layer above the runtime adapter that could translate editor keyboard intent into deterministic seek/play/pause operations. ## Verification ### Local checks - `bun install` - `bun run --filter @hyperframes/core build:hyperframes-runtime` - `bunx oxfmt --check packages/studio/src/player/lib/time.ts packages/studio/src/player/lib/time.test.ts packages/studio/src/player/store/playerStore.ts packages/studio/src/player/store/playerStore.test.ts packages/studio/src/player/hooks/useTimelinePlayer.ts packages/studio/src/player/components/PlayerControls.tsx packages/studio/src/player/components/PlayerControls.test.ts packages/studio/src/components/nle/NLEPreview.tsx` - `bunx oxlint packages/studio/src/player/lib/time.ts packages/studio/src/player/lib/time.test.ts packages/studio/src/player/store/playerStore.ts packages/studio/src/player/store/playerStore.test.ts packages/studio/src/player/hooks/useTimelinePlayer.ts packages/studio/src/player/components/PlayerControls.tsx packages/studio/src/player/components/PlayerControls.test.ts packages/studio/src/components/nle/NLEPreview.tsx` - `bun run --filter @hyperframes/studio test -- src/player/lib/time.test.ts src/player/store/playerStore.test.ts src/player/components/PlayerControls.test.ts src/player/hooks/useTimelinePlayer.test.ts` -> 4 files passed, 52 tests passed - `bun run --filter @hyperframes/studio typecheck` - `bun run --filter @hyperframes/studio build` - `git diff --check` - Lefthook during commit -> lint, format, typecheck, commitlint pass ### Browser verification - Created a temp project at `/tmp/hf-studio-nle-controls` with an animated 10s GSAP timeline. - Started local Studio preview via `bun run --filter @hyperframes/cli dev -- preview /tmp/hf-studio-nle-controls` at `http://localhost:5194`. - Used `agent-browser` to verify: - loop toggle changes to active state - frame display shows `current / total` frames - jump-to-frame input moves the seek position to frame 45 / frame 150 - focused preview accepts Space play/pause - ArrowRight advances one frame from preview focus - J plays backward from frame 150 to a lower frame, then K stops - agent-browser-driven recording of the tested flow completed ## Notes - Local proof artifacts are intentionally not committed: - `qa-artifacts/studio-nle-controls/frame-controls.png` - `qa-artifacts/studio-nle-controls/playback-controls.webm` - Closes #527. |
||
|
|
5fe53a0f9c | fix: keep caption shortcuts outside component | ||
|
|
9dc17ae30d | fix: scope studio playback shortcuts | ||
|
|
a45f900af7 | feat: add Studio NLE playback controls | ||
|
|
635fca124d | feat(studio): adapt timeline ruler density | ||
|
|
7ff1734db7 | fix(studio): capture timeline pinch zoom | ||
|
|
820b07ddaa | feat(studio): support trackpad timeline pinch zoom | ||
|
|
c32db3ea0a | fix: isolate studio sub-composition previews | ||
|
|
894f6e38f1 | feat(studio): server-side waveform generation with caching | ||
|
|
5e1db03fe7 | fix: sequence multi-file timeline drops (#487) | ||
|
|
bb2e64ced1 |
fix(studio): pin user-agent color scheme to dark (#482)
The studio shell paints a #0a0a0a body but never declares `color-scheme`, so browsers render native UA chrome (scrollbars, form controls, focus rings) in the light palette and the mismatch is obvious — especially scrollbars, which appear as light tracks sitting on top of a near-black panel. Studio doesn't expose a theme toggle; the UI is dark-only. Declaring `color-scheme: dark` on `:root` lines up the browser-native surfaces with the rest of the chrome. |
||
|
|
28b84ad8de |
fix(studio): prevent NLE timeline scrollbar from clipping below viewport (#481)
* fix(studio): restore flex layout inside the NLE timeline panel Without `flex flex-col`, the toolbar and Timeline children render as block elements and Timeline's `h-full` takes the panel's entire height. The toolbar still occupies its own ~45px of flow, pushing the scroll area below the viewport and clipping the scrollbar at the bottom of the right-side panel. * fix(studio): pin toolbar height with flex-shrink-0 and trim comment Wrap the toolbar slot in a flex-shrink-0 div so Timeline back-pressure can't squeeze the toolbar below its natural height. The visible bug doesn't manifest at current sizes, but in a flex-col container both children are flex items and the toolbar has no shrink guard of its own. Trim the inline comment to a single line — the rationale belongs in the commit message, not at the call site. |