## Summary
- Adds a hover tooltip (?) next to the format dropdown in the render queue export bar
- Shows the selected format's details (codec, use case) plus a comparison with the other two formats
- Helps users pick between MP4 (general), MOV/ProRes 4444 (transparent video for editors), and WebM/VP9 (transparent for web)
## Test plan
- [x] Open studio, go to the render queue panel
- [x] Hover over the (?) icon next to the format dropdown — tooltip appears above
- [x] Switch format in the dropdown — tooltip content updates to show the selected format first
- [x] Move pointer away — tooltip dismisses
- [x] Verify tooltip doesn't clip or overflow the panel
<img width="420" height="263" alt="image" src="https://github.com/user-attachments/assets/f4bd8bf7-65ed-45ab-ac53-577b4985fa34" />
## Summary
Coordinated minor bump across all published packages. No source changes in this PR itself; it is the version stamp for everything that landed on main since v0.2.5.
## Version bumps
| Package | from | to |
|---|---|---|
| `@hyperframes/cli` | 0.2.5 | **0.3.0** |
| `@hyperframes/core` | 0.2.5 | **0.3.0** |
| `@hyperframes/engine` | 0.2.5 | **0.3.0** |
| `@hyperframes/player` | 0.2.7 | **0.3.0** |
| `@hyperframes/producer` | 0.2.5 | **0.3.0** |
| `@hyperframes/studio` | 0.2.9 | **0.3.0** |
Between 0.2.5 and this release, `player` and `studio` received several patch versions on npm as we iterated on the bundler, entry point, and SSR issues. 0.3.0 collapses that into a single coordinated minor so the ecosystem is aligned again.
## What is in v0.3.0
### `@hyperframes/player`
**Restored package entry points to the compiled `dist/` output.** 0.2.5 shipped with `"main": "./src/hyperframes-player.ts"` but the published tarball only included `dist/` via the `"files"` field. Every consumer trying to import the package failed with `Module not found: Can't resolve '@hyperframes/player'`. Entry points now point at the built JS/`.d.ts` files inside `dist/`.
**DOM-based root timeline resolution in the ready probe.** In a bundled preview, `window.__timelines` contains the master composition alongside its sub-compositions, for example:
```js
{
main: GSAPTimeline(14s),
intro: GSAPTimeline(1.5s),
'scene2-4-canvas': GSAPTimeline(12.6s),
'scene5-logo-outro': GSAPTimeline(3.2s),
}
```
The probe used to select the adapter with `keys[keys.length - 1]`. Object key ordering meant the last-registered sub-composition would win, so the `ready` event reported a sub-composition's duration (e.g. 3.2s) instead of the master's 14s. The probe now looks up the root composition id from the outermost `[data-composition-id]` element in the iframe DOM and uses its key. Falls back to the last key when no element is present, so standalone sub-composition previews keep working.
### `@hyperframes/studio`
**`useTimelinePlayer.getAdapter()` uses the same DOM-based root id lookup** as the player. Previously play, pause, seek, and duration readout were all driven by whichever sub-composition happened to register its timeline last.
**`Player.tsx` loads `@hyperframes/player` lazily.** The component used to call `import "@hyperframes/player"` at module scope, which runs the package's `customElements.define(...)` side effect during module evaluation. `HTMLElement` does not exist in the Node runtime, so any consumer page that transitively imported the studio during server rendering threw:
```
ReferenceError: HTMLElement is not defined
at module evaluation (@hyperframes/studio/src/player/components/Player.tsx)
```
The import now runs inside the mount effect via `import(...)` so it only evaluates in the browser. Added a cancellation flag and deferred cleanup so a fast unmount before the dynamic import resolves does not leak listeners or DOM nodes.
**Captions module imports stripped of `.js` extensions.** Files under `src/captions/` imported siblings as `./types.js` and `./parser.js`. That is legal ESM TypeScript but Turbopack and several other bundlers refuse to resolve those specifiers against `.ts` files inside `node_modules`, breaking any consumer build that transitively pulled in the captions module. Captions now uses extensionless imports, matching the rest of the studio codebase.
### `@hyperframes/core`, `@hyperframes/cli`, `@hyperframes/engine`, `@hyperframes/producer`
Version bump only, no source changes since 0.2.5. Kept on the same version so the ecosystem is easier to reason about.
## Impact for consumers
If you use `@hyperframes/studio` in a Next.js app:
- The play button in a bundled preview reports the correct composition duration and drives the master timeline.
- The session page no longer 500s in dev mode when the studio barrel is imported (the SSR fix).
- Turbopack builds that transitively load the captions module no longer fail on `Cannot resolve './types.js'`.
If you use `@hyperframes/player` directly:
- Consumer bundlers can resolve the package again (dist entry points restored).
- The `ready` event duration reports the master, not a sub-composition.
## After merge
Publish each package to npm with `pnpm publish` (workspace deps auto-resolve).
* fix(studio): load @hyperframes/player lazily to support SSR
Player.tsx had a bare `import "@hyperframes/player"` at module scope. The
player package registers a class that extends HTMLElement as a side effect,
and HTMLElement doesn't exist in a Node server runtime. Any consumer that
imported from @hyperframes/studio during server-side rendering (e.g. the
Next.js App Router evaluating a client component for SSR) threw
`HTMLElement is not defined`.
Move the import inside the mount effect via dynamic `import(...)` so it
only runs in the browser, and wire up a cancellation flag and deferred
cleanup so a fast unmount doesn't leak listeners or DOM nodes.
* fix(studio): remove .js extensions from captions-internal imports
The captions module imported sibling files as `./types.js` and
`./parser.js`. That's legal ESM TypeScript, but Turbopack (and other
bundlers) refuse to resolve those specifiers against .ts files when the
package is consumed from node_modules — the rest of @hyperframes/studio
uses extensionless imports for that reason.
Align captions with the rest of the codebase so the package builds
without bundler-specific configuration in consumers.
* chore: release @hyperframes/player@0.2.7 and @hyperframes/studio@0.2.9
Ships the root-timeline resolution fix (#247), the SSR-safe player load,
and the captions import cleanup.
Bundled previews register a master composition alongside its sub-compositions
in `window.__timelines`, e.g. { main, intro, scene2, scene5 }. Both the
player's probe and studio's getAdapter() were using `keys[keys.length - 1]`
to pick the adapter, which returned whichever timeline was registered last.
That made the player report the final sub-composition's duration as the
video length (e.g. 3.2s instead of the master's 14s) and play/pause/seek
targeted that sub-composition instead of the full composition.
Look up the outermost `[data-composition-id]` element in the iframe DOM
and use its id to select the right timeline. Falls back to last-key when
no element is present (standalone sub-composition previews) so drill-down
views keep working.
Also restores `main`/`import` entry points on @hyperframes/player to
point at compiled dist output (the src/ paths broke workspace consumers
that only receive the published tarball).
* fix(studio): support web-component refs in useTimelinePlayer
The studio's `useTimelinePlayer` hook returns an `iframeRef` that
consumers attach to an `<iframe>` element. When consumers wrap the
iframe in a custom element (e.g. `<hyperframes-player>`) that puts
the iframe inside its shadow DOM, every `iframeRef.current.contentWindow`
access returned `null` and `getAdapter()` silently failed — meaning
timeline seek, play, pause, and `refreshPlayer` all became no-ops.
Changes:
- Add `resolveIframe(el)` helper that returns the underlying iframe
whether the host is the iframe itself, a custom element with a
shadow-DOM iframe, or a wrapper with a descendant iframe.
- Export `resolveIframe` from the studio so consumers can pre-resolve
the iframe before assigning it to `iframeRef`.
- Internal `useTimelinePlayer` keeps the strict `HTMLIFrameElement`
ref type, so existing consumers attaching directly to an `<iframe>`
are unaffected.
Also adds:
- JSDoc on the player's `iframeElement` getter.
- "Advanced: iframe access" docs section in `packages/player/README.md`
and `docs/packages/player.mdx`.
- Type-safety lint rules in `.oxlintrc.json` and a "Type-safety
conventions" section in `CONTRIBUTING.md`.
Backward compatible — App.tsx and NLELayout.tsx continue to work
unchanged.
* chore(lint): defer no-explicit-any rule; it broke existing codebase
The new rules added 37 errors across 32 existing files — mostly
legitimate `window as any` casts at browser-global and test-mock
boundaries. Enabling them without fixing all violations breaks CI.
Revert the `.oxlintrc.json` additions and soften the CONTRIBUTING.md
wording to describe the convention without claiming lint enforcement
(that enforcement will come in a follow-up PR that fixes all sites).
* docs(quickstart): collapse prerequisites into expandable accordion
Wraps the Node.js and FFmpeg install instructions in an Accordion
component so the quickstart page is less verbose for returning users
who already have the dependencies installed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(quickstart): add prerequisite bullet list above accordion
Adds a concise bullet list (Node.js 22+, FFmpeg) above the expandable
install instructions so users can see at a glance what's needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(player): handle Infinity duration from runtime gracefully
When compositions have repeating animations without data-duration, the
runtime sends durationInFrames: Infinity. The player now ignores
non-finite duration values instead of displaying "Infinity:NaN" in the
controls. formatTime also returns "0:00" for non-finite inputs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(lint): add data-duration and Math.ceil overshoot rules
- Add root_composition_missing_data_duration warning when the root
composition element is missing data-duration, which causes the runtime
to infer Infinity for loop-inflated timelines.
- Add gsap_repeat_ceil_overshoot warning that catches
repeat: Math.ceil(d/c)-1 patterns which overshoot the intended
duration. Recommends Math.floor instead.
- Fix gsap_infinite_repeat fixHint to suggest Math.floor (not Math.ceil).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(player): wait for injected runtime before declaring ready
When the player auto-injects the runtime script (because the
composition has GSAP timelines but no runtime), it would immediately
declare ready on the next probe cycle — before the runtime script
finished loading from CDN. This caused play() to send a postMessage
that nobody received, making autoplay silently fail.
Now the probe waits for the runtime bridge (__hf or __player) to
appear before proceeding to the ready state.
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
- Upgrades `root_composition_missing_html_wrapper` from **warning** to **error** — a bare `<div data-composition-id>` as `index.html` without `<!DOCTYPE html>/<html>/<body>` causes browsers to quirks-mode, the preview server to fail, and the bundler to silently skip runtime injection
- Improves the error message to explain _why_ this is bad, and includes a snippet of the offending root element
- Skips `<template>`\-wrapped compositions (already caught by the separate `standalone_composition_wrapped_in_template` rule)
- Adds 8 tests covering the exact screenshot scenario, proper HTML, sub-compositions, plain HTML, and template wrappers
## Test plan
- [x] All 441 existing tests pass (`vitest run`)
- [x] 8 new tests for `root_composition_missing_html_wrapper` and `standalone_composition_wrapped_in_template`
- [x] TypeScript build clean (`tsc --noEmit`)
- [x] oxlint + oxfmt pass
- [x] Run `npx hyperframes lint` on a bare composition `index.html` and verify it now reports an error
The tts command was implemented (PR #201) but never added to the root-level
help display or documentation. This adds it to:
- help.ts GROUPS (AI & Integrations) so it appears in `hyperframes --help`
- docs/packages/cli.mdx with usage examples and flag reference
- CLAUDE.md "Adding CLI Commands" checklist: new steps 4-5 require adding
commands to help.ts groups and docs, preventing future omissions
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
- **Replaces the studio's hand-rolled iframe + scaling in** **`Player.tsx`** with the `<hyperframes-player>` web component, eliminating duplicated ResizeObserver, dimension detection, and stage-size message handling
- **Adds a public** **`iframeElement`** **getter** to the player web component so the studio's `useTimelinePlayer` can still access the inner iframe for clip manifest parsing, timeline probing, and DOM inspection
- **Updates player package exports** to resolve from source for workspace consumers (matching `@hyperframes/core` pattern), while npm-published consumers still get built `dist/` files
### Why a separate player package?
1. **Zero dependencies, any framework** — 12KB vanilla web component vs 940KB React+Zustand+CodeMirror studio
2. **CDN-ready** — single `<script>` tag, no build pipeline needed
3. **Embeddable by third parties** — users embed compositions in their own sites without the studio
4. **Single source of truth** — studio now uses the player instead of duplicating its scaling/detection logic
## Test plan
- [x] `pnpm --filter @hyperframes/player typecheck` passes
- [x] `pnpm --filter @hyperframes/studio typecheck` passes
- [x] `pnpm --filter @hyperframes/studio build` passes
- [x] `pnpm --filter @hyperframes/studio test` passes (2 pre-existing failures, unrelated)
- [x] E2E: Standalone player loads composition, detects 4s GSAP timeline, controls work, play/pause works
- [x] E2E: Studio preview renders via `<hyperframes-player>`, `iframeElement` bridge works, playback controls sync correctly
## Summary
- Add comprehensive README for `@hyperframes/player` covering installation, usage, full API reference (attributes, properties, methods, events), sizing, and distribution formats
- Bump version from 0.2.2 to 0.2.4 to align with monorepo release
## Test plan
- [x] Verify README renders correctly on GitHub
- [x] Confirm package.json version matches monorepo (0.2.4)
## Summary
- The Vite dev server proxied studio renders to a separate producer server (port 9847) that needed to be started manually
- When the producer wasn't running, renders silently failed — red dot, no error message, no way to know what went wrong
- Replaced the proxy with direct in-process rendering via `@hyperframes/producer` — same code path as the CLI and embedded preview mode
- Removed ~70 lines of SSE proxy streaming code, replaced with the same ~20-line in-process pattern used everywhere else
## DX improvement
**Before:** `pnpm dev` + `npx tsx packages/producer/src/public-server.ts` (two terminals, easy to forget)
**After:** `pnpm dev` (renders work immediately)
## Testing
Verified manually: open studio via `pnpm dev`, navigate to a project, click Export — renders complete with live progress updates, no separate server needed.
## Summary
- Audio (and video) sounds broken/choppy on first play in the studio preview, but works fine on second play
- Root cause: `<audio>` elements default to `preload="metadata"`, which only fetches enough data to determine duration — not enough for smooth playback. When `el.play()` fires, the browser hasn't buffered the audio data yet
- The runtime now eagerly sets `preload="auto"` and calls `load()` during init, ensuring media is fully buffered before the user clicks play
- `syncRuntimeMedia` now defers `play()` on unbuffered media by registering a `canplay` listener, instead of silently swallowing the failure
## Testing
Verified with agent-browser against a 26s narration composition (soulscape-film):
```
# After fix — audio element state at init:
preload: "auto"
readyState: 4 (HAVE_ENOUGH_DATA)
buffered: 26.07s (entire file)
duration: 26.07s
```
Audio is fully buffered before any play attempt, so first-play works identically to subsequent plays.
## Files changed
- `packages/core/src/runtime/init.ts` — set `preload="auto"` + `load()` in `bindMediaMetadataListeners`
- `packages/core/src/runtime/media.ts` — defer `play()` on unbuffered media via `canplay` listener
- `packages/core/src/runtime/media.test.ts` — updated test + added unbuffered media test case
Add a FIFO semaphore to limit concurrent renders in the producer server,
preventing Chrome CPU contention that causes beginFrame failures.
- New Semaphore utility class (packages/producer/src/utils/semaphore.ts)
- Both blocking render and SSE renderStream handlers acquire/release the semaphore
- SSE stream sends a "queued" event when request must wait
- New GET /render/queue endpoint exposes active/queued render counts
- Configurable via HandlerOptions.maxConcurrentRenders or PRODUCER_MAX_CONCURRENT_RENDERS env var (default: 2)
- New --max-concurrent-renders CLI flag (1-10)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
- When 2-3 renders run in parallel on Linux (beginFrame mode), Chrome's `HeadlessExperimental.beginFrame` fails with "Another frame is pending" due to CPU contention
- Extracts `sendBeginFrame` helper with exponential backoff retry (50ms–800ms, 5 attempts) — used by both the main capture path and the hasDamage=false fallback
- After retries exhaust, throws an actionable error instead of a raw protocol error
## Testing
### Environment
- Linux (Ubuntu 20.04), 8 cores
- `chrome-headless-shell` 146.0.7680.153 (beginFrame mode active)
- Test composition: 1920×1080, 5s duration, 30fps, 150 frames, 3 GSAP-animated elements
### Before fix (main)
Ran 3 parallel renders of the same composition simultaneously:
| Render | Result | Details |
|--------|--------|---------|
| R1 | Completed | 304 KB, 6.6s |
| R2 | **FAILED** | `Protocol error (HeadlessExperimental.beginFrame): Another frame is pending` at frame 120/150 |
| R3 | Completed | 304 KB, 6.7s |
The error is non-deterministic — it hits whichever worker loses the CDP frame contention race under CPU pressure.
### After fix (this branch)
Same 3 parallel renders:
| Render | Result | Details |
|--------|--------|---------|
| R1 | Completed | 304 KB, 7.3s |
| R2 | Completed | 304 KB, 7.3s |
| R3 | Completed | 304 KB, 7.3s |
All 3 succeeded. The slight increase in wall time (6.6s → 7.3s) is consistent with occasional retries absorbing transient contention without failing.
### Code review
- Both `beginFrame` call sites in `beginFrameCapture` (main capture path + hasDamage=false fallback) use the shared `sendBeginFrame` helper
- Backoff ceiling is 1.55s per frame (50+100+200+400+800ms), acceptable for transient contention
- beginFrame mode is Linux-only (`chrome-headless-shell` + `--enable-begin-frame-control`); macOS uses screenshot mode so the retry code path isn't exercised there
## Summary
- Parent-relative paths (e.g. `src="../file.wav"`) silently drop media from rendered MP4
- The compiler rewrites external paths to `hf-ext/` and copies files to the compiled directory, but both the audio mixer and video frame extractor only resolved against `projectDir` — never finding them
- Now checks `compiledDir` first (matching the file server's resolution order), then falls back to `projectDir`
- Fixes both `<audio>` and `<video>` elements with external paths
## Real-world context
Reported in Slack by Abhai — a TTS comparison video using `<audio src="../tts-voxcpm2.wav">` (audio file in parent directory, composition in subdirectory) rendered successfully but the output MP4 had no audio stream. The render completed without any error, silently dropping the audio.
## Testing
### Environment
- Linux (Ubuntu 20.04), ffmpeg 4.2
- Test composition: `subdir/index.html` with `<audio id="bg-audio" src="../test-audio.wav">`, WAV file at parent directory
### Before fix (main)
```
[AUDIO-DEBUG] element.src=hf-ext/tmp/hf-test-231/test-audio.wav
baseDir=/tmp/hf-test-231/subdir
[AUDIO-DEBUG] resolved srcPath=/tmp/hf-test-231/subdir/hf-ext/tmp/hf-test-231/test-audio.wav
exists=false
```
- Audio mixer tries `join(projectDir, "hf-ext/...")` → file doesn't exist at that path
- Output: **9.8 KB, video stream only** (confirmed via ffprobe)
- No error logged — audio silently dropped
### After fix (this branch)
```
[AUDIO-DEBUG] element.src=hf-ext/tmp/hf-test-231/test-audio.wav
baseDir=/tmp/hf-test-231/subdir
compiledDir=/tmp/.../compiled
[AUDIO-DEBUG] fromCompiled=/tmp/.../compiled/hf-ext/tmp/hf-test-231/test-audio.wav
exists=true
[AUDIO-DEBUG] resolved srcPath=/tmp/.../compiled/hf-ext/tmp/hf-test-231/test-audio.wav
exists=true
[AUDIO-RESULT] success=true, hasAudio=true
```
- Audio mixer checks `join(compiledDir, "hf-ext/...")` first → file found
- Output: **44.6 KB, video + audio streams** (confirmed via ffprobe)
### ffprobe comparison
| Branch | File size | Streams |
|--------|-----------|---------|
| `main` | 9.8 KB | `video (h264)` only |
| `fix` | 44.6 KB | `video (h264)` + `audio (aac)` |
### Path resolution flow
1. Compiler sees `<audio src="../test-audio.wav">`
2. Compiler resolves to absolute path, maps it to `hf-ext/tmp/.../test-audio.wav`
3. Compiler copies file to `compiled/hf-ext/tmp/.../test-audio.wav`
4. Audio mixer gets `element.src = "hf-ext/tmp/.../test-audio.wav"`
5. **main**: tries `join(projectDir, src)` → not found → silent drop
6. **fix**: tries `join(compiledDir, src)` first → found → audio mixed in
### Repro
```bash
mkdir -p /tmp/test/subdir
ffmpeg -f lavfi -i "sine=frequency=440:duration=2" /tmp/test/test-audio.wav -y
# Create subdir/index.html with <audio src="../test-audio.wav" ...>
cd /tmp/test/subdir && npx hyperframes render
ffprobe -v error -show_streams output.mp4 # video only on main, video+audio on fix
```
Add two new skill reference files that address measured LLM composition failures:
- fonts.md: Typography principles — banned fonts, guardrails for violations
(pairing two sans-serifs, defaulting to 400/700 weight), and guidance the LLM
genuinely doesn't apply without being told (register switching, tension as
meaning, easing direction as emotion). Includes Google Fonts API discovery
script with 7-category multi-strategy query.
- motion-principles.md: Motion design principles — guardrails for same-ease and
same-speed defaults, y-axis entrance monotony, and guidance for build/breathe/
resolve scene structure, hard cuts as intentional transitions, visual
composition rules for video-not-web density.
Both files validated against baseline evals: 3 compositions created without
guidance confirmed the LLM reaches for banned fonts (Inter, Cormorant Garamond,
Playfair Display, Roboto Condensed), uses power2.out on 45-72% of tweens,
enters 80%+ of elements from y-axis, and pairs multiple sans-serifs.
Also:
- Fix validate.ts $& replacement bug (runtime source containing $& caused
String.prototype.replace to re-insert the matched <script src=""> tag)
- Clean up font loading guidance across skills (compiler embeds automatically)
- Update house-style.md to reference fonts.md
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three new lint rules that catch structural issues causing compositions
to fail silently in preview:
- root_composition_missing_data_start: Root composition needs data-start="0"
for the runtime to begin playback
- standalone_composition_wrapped_in_template: index.html should not be
wrapped in <template> (only sub-compositions use that)
- root_composition_missing_html_wrapper: index.html needs <!DOCTYPE html>
and <html> wrapper for the bundler
Also adds rawSource to LintContext so rules can inspect pre-template-stripped
HTML, and isSubComposition to linter options so rules can distinguish root
from sub-composition files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): smart port selection with instance reuse
Replace the simple 10-port retry loop with best-in-class port handling:
- Multi-host port testing (127.0.0.1, 0.0.0.0, ::1, ::) catches ports
occupied by SSH forwarding or other interfaces invisible to localhost
- HTTP probe (/__hyperframes_config) detects existing HyperFrames
preview servers — reuses same-project instances instead of spawning
duplicates, skips different-project instances
- PID detection via lsof for actionable "Port N in use by PID X" logs
- Expanded scan range from 10 to 100 ports
- Added --force-new flag to bypass instance detection
- Async PID detection (execFile, no shell) and parallel host testing
Fixes the "10 ports are all in use" error that occurs when zombie
preview servers accumulate or devbox port forwarding occupies ports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): add --list and --kill-all flags to preview command
- `hyperframes preview --list` scans the port range and displays all
active HyperFrames preview servers with their project name, directory,
and PID
- `hyperframes preview --kill-all` kills all active preview servers
- Port scanning uses parallel batched probes (20 at a time) for speed
Gives users visibility into zombie preview servers and a one-command
way to clean them up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: apply HyperFrames design system to Mintlify theme
Update docs config and add custom CSS to match the HyperFrames brand:
- Switch theme from mint to maple, replace cyan palette with warm neutrals
- Add Inter (body/headings) and IBM Plex Mono (code) fonts
- Add custom.css with full light/dark mode CSS variables
- Default to light mode appearance
- Replace box-shadow hover effects with border-color (flat aesthetic)
- Add DESIGN.md to repo root as design system reference
- Fix docs CI to also trigger on DOCS_GUIDELINES.md pushes to main
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: replace HeyGen logo with HyperFrames text wordmark
Replace 41KB HeyGen SVG logos with lightweight (~400B) text-based SVGs
rendering "HyperFrames" in Inter semibold with tight tracking, matching
the wordmark style on hyperframes.heygen.com.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: use ABC Solar Display font for logo wordmark
Match the exact font rendering from hyperframes.heygen.com:
- Load ABC Solar Display Bold from HeyGen static assets CDN
- SVGs use 15.2px/600w/-0.15 letter-spacing (matches computed styles)
- Dark mode fill matches rgb(240,240,240) from the website
- Add @font-face in custom.css for site-wide availability
- Fix lefthook: remove css from oxfmt glob (oxfmt doesn't support CSS)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: convert logo SVGs to outlined paths
SVG <text> elements don't render custom fonts when loaded as <img>
(browser security restriction). Convert the ABC Solar Display glyphs
to SVG paths extracted from the font outlines — renders identically
everywhere with zero font dependency. Remove @font-face for the
display font from custom.css since it's no longer needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: constrain logo height to match website sizing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Revert "docs: constrain logo height to match website sizing"
This reverts commit 89e9cb598e.
* docs: switch body font from Inter to TT Norms Pro
Use TT Norms Pro (from HeyGen static assets CDN) to match
hyperframes.heygen.com. Loads weights 400-700 via @font-face
with Inter as fallback.
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
- Adds `--format mov` to the render CLI for ProRes 4444 transparent video output
- ProRes 4444 with alpha is the industry standard for transparent video overlays, supported by CapCut, Final Cut, Premiere, DaVinci, and After Effects
- WebM VP9 alpha technically works but is ignored by all major video editors — only browsers decode it
- Adds MOV to the studio export dropdown alongside MP4 and WebM
## Transparency format comparison
| Format | Codec | Alpha | Video editors | Browsers | File size |
| --- | --- | --- | --- | --- | --- |
| **MOV** | ProRes 4444 | Yes | CapCut, Final Cut, Premiere, DaVinci, After Effects | No (won't play in browser) | Large (~5-40 MB) |
| **WebM** | VP9 | Yes | None (shows black) | Chrome, Firefox | Small (~200 KB) |
| **MP4** | H.264 | No | All | All | Small |
> **Note:** ProRes MOV files do not play in Chromium browsers — they are an intermediate/editing format, not a delivery format. Use [rotato.app/tools/transparent-video](https://rotato.app/tools/transparent-video) to verify transparency works correctly.
## Changes
- **CLI**: Add `mov` to `--format` validation, examples, and output path logic
- **Engine**: `getEncoderPreset()` returns ProRes 4444 (`yuva444p10le`) for `mov` format; handle `.mov` in `applyFaststart` and `muxVideoWithAudio`; add `pix_fmt` to streaming encoder ProRes path
- **Producer**: Treat `mov` like `webm` for alpha capture (PNG frames, screenshot mode, `forceScreenshot`)
- **Studio**: Add MOV option to export format dropdown and render queue hook
- **Core**: Add `mov` to studio API types, render route, and mime helpers
- **Tests**: Add encoder preset tests for mov format (42 total, all passing)
## Usage
```bash
hyperframes render --format mov --output overlay.mov
```
## Test plan
- [x] `pnpm build` passes
- [x] `pnpm --filter @hyperframes/engine test` — 42 tests pass (2 new for MOV)
- [x] `oxlint` and `oxfmt` clean on all 12 changed files
- [x] End-to-end local render produces ProRes 4444 (`yuva444p12le`) with working alpha
- [x] Docker render with `--format mov` — ProRes 4444 confirmed via ffprobe
- [x] Studio dropdown shows MOV option in built JS
- [x] Transparency verified with [rotato.app/tools/transparent-video](https://rotato.app/tools/transparent-video)
## Summary
- **The `--docker` flag was a no-op stub** — `renderDocker` called the same local `executeRenderJob` as `renderLocal`, no container was ever launched
- Now `renderDocker` generates a Dockerfile, builds a versioned `hyperframes-renderer:<version>` image with Chrome/FFmpeg/fonts/chrome-headless-shell, and runs the render inside a container
- Image is cached per CLI version — first render builds (~2 min), subsequent renders reuse it
- Forces `linux/amd64` platform since chrome-headless-shell has no ARM Linux binary
- Uses `execFileSync` (array form) throughout to prevent shell injection
- Forwards `--quiet`, `--gpu`, and render config flags into the container
- `Dockerfile.render` added as a reference for manual builds
## Test plan
- [x] `hyperframes render --docker` builds image and produces valid MP4
- [x] Second run reuses cached image (no rebuild)
- [x] `--quiet` suppresses container output while keeping stderr for errors
- [x] Typecheck, lint, format all pass
- [ ] Verify `--gpu` with `--docker` on a machine with GPU access
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Description
Adds proper BT.709 color space metadata and full→limited range conversion to H.264/H.265 encoding. Chrome captures frames in full-range sRGB (BT.709 primaries), but without explicit color tagging, players guess the wrong color space and range — causing color shifts across iOS/Android/desktop and crushed dark values that compound the gradient banding issue fixed in #222.
**What changed:**
| Setting | Before | After |
|---------|--------|-------|
| `color_space` | `bt470bg` (guessed) | `bt709` (explicit) |
| `color_primaries` | `unknown` | `bt709` |
| `color_transfer` | `unknown` | `bt709` |
| `color_range` | `pc` (full, wrong for H.264) | `tv` (limited, correct) |
| `time_base` | `1/15360` (varies by platform) | `1/90000` (fixed) |
**Approach:**
- BT.709 VUI params embedded via x264-params/x265-params (`colorprim=bt709:transfer=bt709:colormatrix=bt709`) — ensures the bitstream itself carries color info
- FFmpeg-level metadata flags (`-colorspace:v bt709`, etc.) — belt-and-suspenders
- `scale=in_range=pc:out_range=tv` filter converts Chrome's full-range output to TV/limited range
- VAAPI path chains the range filter with existing `format=nv12,hwupload`
- `-video_track_timescale 90000` for consistent cross-platform A/V timing (same as Remotion)
- VP9 and ProRes encoding unaffected
## Testing
- Verified via ffprobe: all 5 color metadata fields now correct
- Directly tested FFmpeg args produce expected output
- 40 engine tests pass (8 new: color metadata h264/h265, range filter CPU, VAAPI filter chain, GPU skip, VP9 skip, timescale)
- Builds cleanly, lint + format pass
Add aq-mode=3 (auto-variance adaptive quantization) to CPU H.264/H.265
encoding. This redistributes bits from bright/textured areas to dark flat
areas where color banding is most visible in 8-bit yuv420p output.
- standard/high presets: aq-mode=3 + aq-strength=0.8 + deblock=1,1
- draft (ultrafast): aq-mode=3 only (deblock too slow for ultrafast)
- GPU and VP9 encoders unaffected (have their own AQ implementations)
Adds 6 regression tests verifying the params are emitted correctly.
Fixes color banding on dark gradients (eval issue #3, prompts 3,5,10,14).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
One-character fix: removes leading `/` from the preview URL hash.
Dev mode opened `#/project/<name>` but the studio expects `#project/<name>`. The route regex in `App.tsx` (`/^#project\/([^/]+)/`) never matched the slash-prefixed hash, so it fell through to auto-selecting the first project from `/api/projects` — ignoring the project path you passed on the CLI.
## Root cause
```
// preview.ts dev mode (line 176) — WRONG
`${frontendUrl}#/project/${pName}`
// App.tsx route parser (line 34) — expects this format
window.location.hash.match(/^#project\/([^/]+)/)
```
Local-studio mode and embedded mode already used the correct `#project/` format.
## Test plan
- [x] Typecheck passes
- [x] `npx tsx cli preview /path/to/project` opens the correct project in the studio
## Summary
Two fixes in the producer:
1. **Head styles/scripts extraction**: mirrors the runtime fix from PR #219. The producer's `inlineSubCompositions()` parsed only `bodyEl.innerHTML` from non-template sub-compositions, discarding all `<head>` content.
2. **Externalize postcss**: postcss is a CJS module with `require("path")` — bundling it into ESM output caused "Dynamic require of path is not supported" at runtime, breaking `npx tsx cli render` and `npx tsx cli preview` from the local dev build.
## Verified
Re-rendered the iris-wipe composition (eval prompt #25, previously scored 1.0/5 — entirely black):
| Frame | Before fix | After fix |
| --- | --- | --- |
| 0\.5s | Black | Red background + "HELLO" text |
| File size | 16\.9 KB (all black) | 64\.8 KB (actual content) |
Scene 1 now renders correctly. Scene 2's clip-path animation has a separate GSAP issue (the lint already warns about it via `scene_layer_missing_visibility_kill`).
## Test plan
- [x] `pnpm --filter @hyperframes/producer build` succeeds
- [x] `node --input-type=module -e "import './dist/index.js'"` loads without error
- [x] Re-render iris-wipe produces visible content (64.8 KB vs 16.9 KB)
- [x] Frame extraction confirms red "HELLO" scene renders correctly
## Summary
Fixes a bug where non-template sub-compositions (full HTML documents loaded via `data-composition-src`) lost all `<head>` styles and scripts. This affected **three code paths**:
1. **Runtime** (`compositionLoader.ts`) — browser preview via iframe fetch
2. **Bundler** (`htmlBundler.ts`) — studio preview HTML bundling (**this was causing the black preview**)
3. Producer fix is in PR #220
## What it fixes
**Eval prompt #25** (iris-wipe) renders entirely black in both the studio preview and rendered video because scene backgrounds (`#EF4444` red, `#3B82F6` blue), positioning, and the GSAP CDN script were all in `<head>` and silently dropped.
### Verified
Rebuilt core, started studio preview, fetched the bundled HTML from `/api/projects/iris-wipe/preview` — confirmed `#scene1 { background: #EF4444 }` and `.scene { position: absolute }` are now present in the output.
## Root cause
All three code paths did the same thing:
```js
const contentHtml = template ? template.innerHTML : bodyEl.innerHTML;
// ^ <head> content is already lost here
```
## Test plan
- [x] All 429 core tests pass
- [x] Studio preview endpoint returns correct bundled HTML with head styles included
- [x] `pnpm --filter @hyperframes/core build` succeeds
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
Adds a new lint rule `gsap_infinite_repeat` that flags `repeat: -1` in GSAP timelines as an error. This is a hard enforcement of the skill guardrail added in PR #217.
## What it fixes
The deterministic capture engine (`HeadlessExperimental.beginFrame`) seeks to exact frame times on a paused GSAP timeline. When a timeline contains `repeat: -1`, the timeline duration is infinite, which causes the capture engine to produce incorrect/blurry output.
**Eval prompt #20** (loading-spinner, scored 2.0/5) used `repeat: -1` on a dots animation cycle, producing "a highly compressed and blurry loading animation lacking visual clarity and professional polish."
## Changes
- `packages/core/src/lint/rules/gsap.ts` — new `gsap_infinite_repeat` rule (regex scan for `repeat: -1`)
- `packages/core/src/lint/rules/gsap.test.ts` — 2 new tests (detects infinite repeat, allows finite repeat)
## Test plan
- [x] `pnpm --filter @hyperframes/core test` — all 429 tests pass
- [x] Rule catches `repeat: -1` and reports as error with fix hint
- [x] Rule does not flag `repeat: 4` (finite repeats)
## Summary
Adds critical rendering constraints to the `hyperframes` skill discovered from eval analysis of 27 agent-generated compositions. These guardrails prevent agents from producing compositions that technically work but render poorly.
## What it fixes
| Rule Added | Eval Prompts Affected | Issue |
| --- | --- | --- |
| Ban `repeat: -1` | #20 loading-spinner (2.0/5) | Infinite timeline broke capture engine |
| Ban async timeline construction | #16 particle-logo (2.6/5) | Timeline empty at capture time |
| Min font size 16px (labels), 20px (body) | #7, #8, #13, #14, #15, #19 | Illegible text after encoding |
| Ban full-screen dark linear gradients | #3, #5, #10, #14 | H.264 color banding |
| `<link>` fonts over CSS `@import` | #7, #24 | Font loading race conditions |
## Changes
- **Rules section**: Added `repeat: -1` ban, async timeline ban, items 8-9 to "Never do" list
- **Typography section**: Expanded font size guidance with specific minimums per text role (headlines, body, labels)
- **New "Backgrounds and Color" section**: Guidance on avoiding gradient banding
- **Output Checklist**: 5 new items covering all new constraints
## Test plan
- [ ] Run eval with updated skill and compare avg quality scores
- [x] Skill renders correctly in `/hyperframes` invocation
* feat(skills): add WebGL shader transitions and restructure catalog
Add 14 WebGL fragment shader transitions to the transitions skill:
domain warp dissolve, ridged burn, whip pan, SDF iris, ripple waves,
gravitational lens, cinematic zoom, chromatic radial split, glitch,
swirl vortex, thermal distortion, flash through white, cross-warp
morph, and light leak (shader).
Restructure catalog.md from a 1045-line monolith into a 105-line
routing layer with 15 reference files. SKILL.md loads at 101 lines,
catalog.md loads at 105 lines — reference files loaded on demand
only for the transition type being implemented.
Key additions:
- Full WebGL setup boilerplate with media capture (images, video,
object-fit: cover, live video re-upload during transitions)
- Hard rules for shader transitions capturing all bugs found during
development (Y-flip, preserveDrawingBuffer, fwidth, boomerang,
tween proxy reuse, tl.call vs onComplete)
- CSS vs Shader decision guide in SKILL.md
- Visual pattern warning against repeating geometric patterns
- Shader transitions slotted into mood/energy mapping tables
- Noise libraries: quintic C2, ridged, erosion FBM, cosine palette
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(skills): fold transitions into hyperframes skill
Move transitions from a standalone skill (4th top-level) into
hyperframes/references/transitions/, aligning with the consolidation
in #211 that reduced 15 skills to 3. Fewer standalone skills means
higher trigger reliability for multi-skill tasks.
Also removes stale text-burn-dom.html reference from css-destruction.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(skills): consolidate 15 skills into 3 for better trigger reliability
Merge 9 GSAP skills (core, timeline, scrolltrigger, plugins, utils, react,
frameworks, performance, effects) and 6 HyperFrames skills (compose, captions,
tts, audio-reactive, marker-highlight, cli) into 3 consolidated skills:
- `gsap` — core API + timelines + performance in SKILL.md; scrolltrigger,
plugins, utils, react, frameworks, effects in references/
- `hyperframes` — composition authoring rules in SKILL.md; captions, tts,
audio-reactive, marker-highlight in references/
- `hyperframes-cli` — CLI commands (init, lint, preview, render, etc.)
Why: With 15 separate skills, agents must correctly trigger the right subset
for any task. "Create an animated video with captions" needed 6+ skills to
fire — each with ~90% trigger accuracy means ~53% chance of getting all of
them. With 3 skills, that same task needs just `hyperframes` + `gsap` (~90%
both fire). Progressive disclosure still works via references/ files loaded
on demand.
Also fixes: CLAUDE.md referenced `window.__GSAP_TIMELINE` (incorrect) —
corrected to `window.__timelines`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): add --skip-skills flag to init command
Allow skipping the AI coding skills installation prompt during
`hyperframes init` with `--skip-skills`. Useful when skills are
already installed or when the user wants to scaffold without them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(skills): address code review feedback on consolidation
Restore content lost during over-compression:
- captions: fix overflow to `visible` (not hidden — clips glow effects),
add container pattern warning, scale headroom formula, and self-lint
placement guidance
- audio-reactive: restore sampling frequency pattern (per-frame tl.call
loop vs single tween) and textShadow-on-container gotcha
- effects/typewriter: restore word rotation, appending words, spacing
with static text, and multi-line cursor handoff patterns
- effects/audio-visualizer: restore spatial mapping conventions, fetch vs
inline loading, WebGL/DOM rendering approaches, and canvas layering
- hyperframes-cli: restore --strict-all flag in render flags table
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): update build:copy and template for consolidated skill names
- build:copy: reference skills/hyperframes, skills/hyperframes-cli,
skills/gsap instead of the old 15 skill directory names
- _shared/CLAUDE.md template: update skill table to consolidated names
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
Two independent initiatives that improve agent DX and expand HyperFrames' reach.
### Initiative 1: Fix the Clip Animation Footgun
- `gsap_animates_clip_element` lint rule now uses smart detection — only errors when GSAP animates `visibility` or `display` on a clip element
- All other properties (opacity, transform, x, y, scale, etc.) are allowed silently
- This was the #1 agent failure in QA (10/10 agents hit it on v0.2.1)
### Initiative 2: `<hyperframes-player>` Web Component
- New `@hyperframes/player` package — zero dependencies, 3.3KB gzipped
- Iframe-based web component with Shadow DOM for perfect isolation
- Video-like API: `play()`, `pause()`, `seek()`, `currentTime`, `duration`, events
- Controls overlay with play/pause, scrubber (mouse + touch), time display, auto-hide
- Full docs page at `docs/packages/player.mdx`
## Before / After
### Clip animation lint
**Before (10/10 agents hit this):**
```
✗ gsap_animates_clip_element: GSAP animation targets a clip element.
Selector "#title" resolves to element <div id="title" class="clip">.
The framework manages clip visibility — animate an inner wrapper instead.
Fix: Wrap content in a child <div> and target that with GSAP.
```
**After (only errors on actual conflicts):**
```
# This passes lint — no error:
tl.from("#title", { opacity: 0, y: -50, scale: 0.8 }, 0);
# This still errors — actual conflict with runtime:
tl.to("#title", { visibility: "hidden" }, 3);
✗ gsap_animates_clip_element: GSAP animation sets visibility on a clip element.
Fix: Remove the visibility/display tween. Use opacity for fade effects.
```
### Embeddable player
**Before:** No way to embed a composition in a web page.
**After:**
```html
<script src="https://cdn.jsdelivr.net/npm/@hyperframes/player"></script>
<hyperframes-player src="./composition/index.html" controls></hyperframes-player>
```
```js
const player = document.querySelector('hyperframes-player');
player.play();
player.pause();
player.seek(2.5);
player.addEventListener('ready', (e) => console.log('Duration:', e.detail.duration));
```
## Test plan
- [x] 427 core tests pass (20 GSAP lint tests with smart detection)
- [x] 7 player tests pass (formatTime + element registration)
- [x] TypeScript compiles cleanly (core + player)
- [x] Lint: GSAP animating clip with safe props → 0 errors
- [x] Lint: GSAP animating clip with `visibility` → 1 error (correct)
- [x] Player builds to 3.3KB gzipped ESM
- [x] Lockfile updated for CI
- [x] Docs page added at `docs/packages/player.mdx`
## Summary
Addresses all 8 issues from the QA report to improve agent and user experience.
### P0 — Must Fix
- **Blank template broken captions**: Removed `compositions/captions.html` and its reference from the blank template. Every agent (10/10) hit 404 errors during render.
- **Inner-wrapper example**: Added a clear structural comment in the blank template showing the correct `class="clip"` + inner wrapper pattern.
- **Sub-composition introspection**: `hyperframes compositions` now reads external HTML files referenced via `data-composition-src` and shows their real duration/element count instead of `0.0s / 0 elements`.
### P1 — Fix Soon
- **Font mapping warnings**: Now lists all mapped fonts, suggests alternatives (use a mapped font, add @font-face, install locally), and links to docs.
- **Browser 404s**: Non-font "Failed to load resource" 404s now prefixed with `[non-blocking]` instead of `[Browser:ERROR]`.
- **Render concurrency**: Default workers increased from `cores/2` (max 4) to `cores*3/4` (max 6). Added `--concurrency` alias.
### P2 — Nice to Have
- **Transform conflict fix suggestion**: `gsap_css_transform_conflict` now suggests exact GSAP property replacements (e.g., `xPercent: -50, yPercent: -50`).
- **Upgrade --yes**: Now actually runs the install instead of just printing the command.
## Before / After
### Font mapping warning
**Before:**
```
[Compiler] No deterministic font mapping for: DM Sans
```
**After:**
```
[Compiler] No deterministic font mapping for: DM Sans
Mapped fonts: arial → inter, courier → jetbrains-mono, ...
To fix, pick one:
1. Use a mapped font name instead (see list above)
2. Add a @font-face block in your HTML with a local or hosted font file
3. Install the font locally on the render machine (Docker: add to Dockerfile)
4. Add an alias to FONT_ALIASES in deterministicFonts.ts (for contributors)
```
### Browser 404s during render
**Before:** `[Browser:ERROR] Failed to load resource: the server responded with a status of 404`
**After:** `[non-blocking] Failed to load resource: the server responded with a status of 404`
### Transform conflict lint
**Before:** `Fix: Remove the transform from CSS and use tl.fromTo...`
**After:** `Fix: Remove transform: translate(-50%, -50%) from CSS and replace with GSAP properties: xPercent: -50, yPercent: -50`
### Compositions command
**Before:**
```
overlay 0.0s 1920×1080 0 elements
```
**After:**
```
overlay 8.0s 1920×1080 2 elements ← compositions/overlay.html
```
## Test plan
- [x] All 422 core tests pass
- [x] TypeScript compiles cleanly (all 4 packages)
- [x] Full monorepo build succeeds
- [x] `hyperframes init --template blank` ships without broken captions reference
## What
Bumped all package versions to `0.2.2-alpha.4` and added five new lint rules for caption and GSAP animation quality checks.
## Why
The new lint rules address common issues in HyperFrames compositions:
- Caption overflow clipping when emphasis words are scaled above 1.0x
- Text shadow artifacts on caption group containers with semi-transparent children
- Mismatch between fitText maxWidth and scaled word dimensions
- Imperceptible audio reactivity from single tweens instead of time-sampled animations
- Scene layer visibility conflicts when relying only on opacity tweens
## How
Added three new caption-specific lint rules in `captions.ts`:
- `caption_overflow_clips_scaled_words` - detects `overflow: hidden` on caption containers when scripts scale words above 1.0x
- `caption_textshadow_on_group_container` - flags textShadow tweens applied to group containers instead of individual words
- `caption_fittext_scale_mismatch` - calculates effective width from fitText maxWidth × max scale factor and warns when it exceeds safe bounds
Added two new GSAP lint rules in `gsap.ts`:
- `audio_reactive_single_tween_per_group` - identifies audio-reactive captions using peak values instead of time-sampled loops
- `scene_layer_missing_visibility_kill` - detects multi-scene compositions missing hard visibility kills after opacity exit tweens
Enhanced documentation with new mask reveals guide and updated existing skills with overflow handling, scene management, and audio reactivity best practices.
## Test plan
- [x] Lint rules tested against existing composition patterns
- [x] Documentation updated with new techniques and constraints
- [x] Version bumps applied consistently across all packages
Replace the static "Tip" message at the end of `hyperframes init` with an
interactive prompt that offers to install AI coding skills. When the user
accepts, the skills command runs `npx skills add` with `--all` and
`stdio: "inherit"` so the native installer output is visible.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
- `cheerio` pulls `encoding-sniffer` → `whatwg-encoding@3.1.1` (deprecated), causing a warning on every `npm install -g hyperframes`
- `linkedom` was already bundled into the CLI via tsup `noExternal` and has zero deprecated transitive deps
- Rewrote `htmlBundler.ts` and `subComposition.ts` to use standard DOM APIs via `linkedom`
- Added a `parseHTMLContent` helper that wraps HTML fragments in a full document structure (required for `linkedom` to populate `document.body`)
- Removed `cheerio` from `cli` dependencies and tsup `external` list
- Replaced `cheerio` with `linkedom` in `core` `optionalDependencies`
## Test plan
- [x] All 411 tests pass (`bun run test` in `packages/core`)
- [x] Full monorepo build succeeds (`bun run build`)
- [x] TypeScript typecheck passes
* feat(cli): add skills command that installs all without selection
Wraps `npx skills add --all -g` so users don't need to manually
select skills or targets. Just run `hyperframes skills` and everything
gets installed to all supported AI tools.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): fix typecheck errors in skills command
Use spawn instead of execFile to avoid stdio type mismatch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two issues prevented `npx hyperframes` from running:
1. The tsup banner declared `const __filename` which collided with
esbuild's CJS-to-ESM `var __filename` shim. ESM strict mode
rejects const+var redeclaration. Changed to `var` so both
declarations coexist.
2. postcss (producer dependency) was not resolvable during bundling
due to bun's isolated module layout. Added postcss as an external
dependency of the CLI package.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## What
Added GSAP CDN imports to all template compositions and improved project linting to skip template placeholders.
## Why
Templates were missing GSAP script imports, causing JavaScript errors when GSAP animations tried to execute. The linter was also incorrectly flagging template placeholder values like `__VIDEO_SRC__` as missing audio sources.
## How
- Added `<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>` to all composition HTML files
- Updated `lintAudioSrcNotFound` function to skip template placeholders matching the pattern `__[A-Z_]+__`
- Fixed GSAP animation properties by adding `overwrite: "auto"` to prevent conflicts
- Replaced CSS transforms with GSAP `gsap.set()` calls for better animation control
- Moved video elements outside timed containers in some templates to avoid nesting issues
- Standardized transcript data format to use double quotes for JSON consistency
- Added proper timeline initialization with `window.__timelines = window.__timelines || {}`
- Enhanced caption styling with overflow handling and max-width constraints
## Test plan
- [x] Manual testing performed
- [x] Verified GSAP animations work correctly in all templates
- [x] Confirmed linter no longer flags template placeholders as errors
- [x] Tested video playback and audio synchronization
* fix(engine): suppress font-loading 404 noise in render console output
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(engine): downgrade resource 404s to buffer-only instead of suppressing
Address review feedback: instead of silently dropping "Failed to load
resource" errors (which could hide real asset failures), keep them in
browserConsoleBuffer for diagnostics but don't print to stdout. Real
asset 404s are still caught by the file server's own logging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(engine): narrow 404 filter to font CDN domains and woff2 files only
Address review: filter was too broad and could suppress real asset
failures. Now only suppresses 404s matching fonts.googleapis,
fonts.gstatic, or .woff2 file extensions. Missing images, scripts,
and videos will still surface as [Browser:ERROR] in render output.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(fonts): add Playfair Display, Noto Sans JP, Roboto, and 4 more to deterministic font database
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(engine): suppress font-loading 404 noise in render console output
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(engine): downgrade resource 404s to buffer-only instead of suppressing
Address review feedback: instead of silently dropping "Failed to load
resource" errors (which could hide real asset failures), keep them in
browserConsoleBuffer for diagnostics but don't print to stdout. Real
asset 404s are still caught by the file server's own logging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(engine): narrow 404 filter to font CDN domains and woff2 files only
Address review: filter was too broad and could suppress real asset
failures. Now only suppresses 404s matching fonts.googleapis,
fonts.gstatic, or .woff2 file extensions. Missing images, scripts,
and videos will still surface as [Browser:ERROR] in render output.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>