- Remove store reset from useEffect cleanup — was wiping state on dep changes
and breaking auto-save subscription
- Move panel auto-open into useEffect instead of during render
- Add fallback 500ms poll alongside postMessage listener for initial load
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace polling with a message listener for hf-preview state/timeline
events. These fire after loadExternalCompositions completes, guaranteeing
.caption-group elements exist in the DOM. Eliminates the race condition
on initial page load.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Caption edit mode now activates automatically when .caption-group elements
are found in the iframe DOM. Works from index.html (master) without
requiring drill-down into the captions sub-composition. Finds the captions
source file via data-composition-src attribute.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Caption overlay and hover states always active on captions compositions
- Clicking a word auto-opens the property panel
- Remove Edit Captions button — edit mode activates automatically
- Reset edit mode when navigating away from captions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove typography, color, opacity, stroke, and background controls.
Keep only Position (X/Y) and Transform (Scale/Rotation) — the controls
that work reliably with the wrapper-based override system.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace single "color" with activeColor (spoken) and dimColor (inactive).
Runtime hook modifies existing GSAP color tweens by detecting dim vs active
states via opacity heuristic. Property panel shows both color pickers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
No longer needed — caption overrides are a JSON data file, not HTML patches.
The runtime hook re-applies overrides on every reload, so preview refresh
during editing is safe and ensures the preview stays in sync.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace manual Save button with 800ms debounced auto-save. Writing to
caption-overrides.json is safe because preview refresh is suppressed
during caption edit mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- getWordEl skips wrapper spans and finds actual word elements inside them
- readWordBoxes handles wrapped words in position calculations
- getOrCreateWrapper detects if element IS a wrapper to prevent double-wrapping
- Remove debug logging from save
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- applyCaptionOverrides() now runs both when compositions load async AND when
already ready (standalone composition view)
- Overrides file written to project root (not compositions/) to match <base href>
- Remove debug logging
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of killing transform tweens, wrap each overridden word span in an
inline-block wrapper span. Editor transforms (position, scale, rotation)
are applied to the wrapper while the inner span keeps all GSAP entrance,
exit, and karaoke animations intact.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add optional `id` field to transcript Word interface, assigned as `w{index}` during normalization
- caption-overrides.json uses `wordId` (preferred) with `wordIndex` fallback
- Runtime hook calls gsap.killTweensOf for overridden transform properties before applying
- Studio parser carries wordId through from transcript to segments
- Add typecheck to lefthook pre-commit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New caption-overrides.json format: [{wordIndex, x, y, scale, rotation, color, ...}]
- Runtime hook in packages/core applies overrides via gsap.set() after compositions load
- Studio writes/reads caption-overrides.json instead of injecting code into HTML
- Original composition source is never modified
- Remove old parseExistingOverrides and source patching code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Canvas: move (translate), scale, and rotate handles on selected words via gsap.set()
- Property panel: real-time style updates applied directly to iframe DOM
- Save: patches original source with gsap.killTweensOf + gsap.set per edited word
- Parser: reads existing editor overrides on re-entry to preserve prior edits
- Suppress preview refresh during caption edit mode to prevent reload cycle
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements generateCaptionHtml(model) which serializes a CaptionModel
into a complete captions.html HyperFrames composition with template
wrapper, CSS per-group classes, TRANSCRIPT array, and GSAP timeline
registration via window.__timelines["captions"].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds `buildCaptionModel` to the caption parser — groups TranscriptWord
arrays into CaptionGroups (default 5 words each), creates CaptionSegments
with correct timing and groupIndex, and wires in DEFAULT_STYLE,
DEFAULT_CONTAINER, and DEFAULT_ANIMATION_SET. Covers 23 tests (13 new).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Vite dev server was loading the HyperFrames runtime IIFE from
cdn.jsdelivr.net. This caused compositions using window.__hyperframes
(e.g., fitTextFontSize) to fail when the CDN script didn't load in
the iframe context.
Add a /api/runtime.js endpoint to the Vite plugin that serves the
local built runtime, matching what studioServer.ts already does for
the CLI embedded server.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
- Add `/api/projects/:id/upload` endpoint for multipart file uploads with automatic dedup naming
- Wire `onImportFiles` from Assets tab "Import media" button through to the upload API
- Add global drag-drop overlay — drop media files **anywhere** in the studio, not just the Assets panel
- Files that already exist get `(2)`, `(3)` suffixes instead of overwriting
- Support uploading into subdirectories via `?dir=` param — dropping on a folder imports there
- Make folders draggable in the file tree + support drop-to-root
- Add `bodyLimit` middleware for early rejection of oversized payloads
- Surface skipped/failed uploads via toast notification instead of console-only
Addresses feedback: _"Wish I could upload/drag-drop assets directly in the Studio (music, images, video) like a CapCut media panel"_
## Test plan
- [x] Open studio, drag an image/video/audio file onto any part of the UI
- [x] Verify the drop overlay appears with "Drop files to import" message
- [x] Drop the file — verify it appears in the Assets tab and file tree
- [x] Drop a file with the same name — verify it gets a `(2)` suffix
- [x] Click "Import media" button in Assets tab — verify file picker works
- [x] Import multiple files at once via drag-drop
- [x] Drop a file onto a nested folder in the file tree — verify it lands in that folder
- [x] Drag a folder in the file tree and drop it on another folder or root — verify it moves
- [x] Drop a file >500MB — verify toast notification appears
- [x] Verify drag overlay doesn't get stuck when dragging over nested UI elements
## Summary
- **API**: POST (create), DELETE (delete), PATCH (rename/move), POST duplicate endpoints with null-byte sanitization
- **FileTree**: right-click context menu with New File, New Folder, Rename, Duplicate, Delete
- **Drag-and-drop**: move files between folders with visual feedback and subtree guard
- **Inline editing**: rename/create inputs with filename validation
- **Header actions**: quick New File / New Folder buttons in the FILES header
Stacks on top of `feat/studio-code-quality`.
## Test plan
- [x] Right-click file → Rename, Delete, Duplicate all work
- [x] Right-click folder → New File, New Folder, Delete work
- [x] Drag file from one folder to another
- [x] Create file with invalid name (`../foo`, `a/b`) → rejected client-side
- [x] Delete currently-edited file → editor clears
- [x] Studio build succeeds
* feat(skills): add hyperframes-cli skill for CLI workflow guidance
Adds a new skill that teaches AI agents how to use the HyperFrames CLI
(init, lint, dev, render, doctor). Previously, agents had no way to
discover the CLI — the compose-video skill only covered HTML authoring.
This led to agents searching for binaries, finding the monorepo, and
running bun run studio manually instead of using npx hyperframes dev.
Also registers the skill in init.ts so new projects get it bundled
alongside hyperframes-compose and hyperframes-captions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(cli): rename dev command to preview
The command starts a preview server — "preview" describes what users
are doing more accurately than "dev". Updates the command name, file
name, all CLI references, docs, skills, and template CLAUDE.md.
22 files updated across CLI source, docs, skills, and templates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(skills): replace stale dev reference with preview in CLI skill
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(docs): catch remaining dev references missed in rename
- testing-local-changes.mdx: two inline command examples
- troubleshooting.mdx: anchor link #dev → #preview, "dev server" → "preview server"
- cli.mdx: "dev server" → "preview server"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
The timeline is now hidden by default. A toggle button appears in both the header (alongside panel toggles) and the player controls bar. Both sync the same state and turn teal when the timeline is visible.
## Changes
- **`timelineVisible` state** in `App.tsx`, defaults to `false`
- **Header toggle**: icon button between sidebar toggle and Renders button
- **Player controls toggle**: icon button at the right end of the controls bar
- **NLELayout**: `timelineVisible` and `onToggleTimeline` props gate the timeline + resize divider
- **Player controls stay visible**: moved from inside the timeline section to inside the preview area, so hiding the timeline doesn't hide play/seek/timecode
## Behavior
| State | Preview | Player controls | Timeline |
|---|---|---|---|
| Timeline hidden (default) | Full height | ✅ Visible | Hidden |
| Timeline visible | Shorter | ✅ Visible | Shown with resize handle |
Both toggle buttons show identical teal active state (`#3CE6AC/10` bg + `#3CE6AC/30` border).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
Removes the `ProjectPicker` home screen entirely. The studio now boots directly into the editor by auto-selecting the first available project from `/api/projects`.
## Changes
- **Auto-select on load**: When no `#project/` hash is present, fetches the project list and navigates to the first one
- **Type narrowing**: Added `if (resolving || !projectId)` early return so TypeScript narrows `projectId` to `string` for all downstream props
- **Removed**: `ProjectPicker`, `ProjectCard`, `ExpandedPreviewIframe` components (~280 lines), `handleSelectProject` callback, `ProjectEntry` interface
## Why
The CLI studio always has exactly one project. The home page was an extra click with no value.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- Surfaces error message when render fails due to producer server not running
- Two cases covered: initial POST failure and SSE connection drop
- Failed jobs now show the error reason in red text in the Renders panel
## Test plan
- [x] Start studio without producer server
- [x] Click render → should show "Could not reach render server" in red
- [x] Start render then kill producer → should show "Connection lost"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- Replaces ~850 lines of inline route handlers in `vite.config.ts` with the shared `createStudioApi(adapter)` module
- Implements `StudioApiAdapter` for the Vite dev server context (SSR-loaded bundler/linter, producer HTTP proxy, Puppeteer thumbnails)
- Bridges Hono `fetch()` to Vite's Connect middleware with streaming support for SSE
Now **both** consumers (CLI + studio) use the same shared API module, ensuring feature parity.
## Test plan
- [x] `pnpm --filter @hyperframes/studio dev` starts correctly
- [x] Home page shows project grid with thumbnails
- [x] Preview plays with correct fonts/animations
- [x] Sub-composition drill-down works
- [x] Lint modal shows findings
- [x] File read/write works in code editor
- [x] Render queue works (requires producer server)
🤖 Generated with [Claude Code](https://claude.com/claude-code)