feat(registry): add 15 caption style catalog components#921
Conversation
Add 15 caption overlay components to the registry, covering a wide range of animation techniques: Standalone origins (6): - caption-pill-karaoke: pill container + karaoke word highlight - caption-neon-accent: multi-color neon glow with wiggle drift - caption-weight-shift: font-weight transition between lines - caption-emoji-pop: emoji + stroked text + horizontal squeeze - caption-editorial-emphasis: dual-font (Inter + Playfair) emphasis - caption-parallax-layers: 3D text layering with vertical stretch Gallery picks (9): - caption-glitch-rgb: RGB chromatic aberration + CRT scanlines - caption-typewriter: green terminal char-by-char with cursor - caption-matrix-decode: scramble-reveal character animation - caption-particle-burst: keyword particle explosions - caption-texture-lava: flowing lava texture mask - caption-clip-wipe: clip-path left-to-right wipe reveal - caption-kinetic-slam: full-screen single-word slam - caption-gradient-fill: gradient-clipped text + elastic bounce - caption-neon-glow: cyan/magenta neon glow accents Each component includes the reusable HTML, a demo composition, and registry-item.json. All use a generic HyperFrames transcript. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 15 .mdx doc pages under docs/catalog/components/ for all caption styles - Add "Captions" group as first section in the Catalog tab navigation - Add canvas-based fitFontSize to 14 caption components to prevent text overflow - Fix parallax-layers vertical clipping by repositioning the behind safe zone - Re-render all 15 preview videos at high quality and upload to CDN
jrusso1020
left a comment
There was a problem hiding this comment.
APPROVE — pure additive registry catalog (63 files, +10534/-0), no existing-code changes. Determinism + structural patterns look clean. Two non-blocking flags below.
Audited
registry/registry.json— 15 new entries, names match component dirs, all typedhyperframes:component✓registry/components/caption-{pill-karaoke,particle-burst,matrix-decode,typewriter,glitch-rgb,clip-wipe,texture-lava}/*.html— deterministic patterns (paused timeline, seededmulberry32for randomness,__timelinesregistration), composition IDs match registry names, font-fit canvas measurement is consistent ✓registry-item.jsonshape for spot-checked entries (caption-pill-karaoke,caption-texture-lava) — schema +filestargets look correct for thecompositions/components/install layout ✓docs/catalog/components/caption-pill-karaoke.mdx— MDX renders preview video, install command, file table ✓
Trusting
- The other 8 caption HTML files — assumed same boilerplate (font-fit + paused GSAP timeline + WORDS+GROUPS dispatch) given the visible pattern across 7 of 15
- Preview MP4 URLs (15 separate CDN paths) — assumed uploaded and reachable per body claim
demo.htmlcontent matching the canonical.htmlper file (identical line counts)lava.pngbinary content/licensing
Non-blocking flags
1. caption-texture-lava has an undeclared dependency on texture-mask-text.
The HTML references the mask via /assets/texture-mask-text/masks/lava.png — that's the neighboring component's asset path, not the local lava.png in caption-texture-lava/. Looking at registry-item.json:
"files": [
{ "path": "caption-texture-lava.html", "target": "compositions/components/caption-texture-lava.html", ... }
]The lava.png shipped in this PR's caption-texture-lava/ directory isn't in the files array, so npx hyperframes add caption-texture-lava won't copy it. AND the .html doesn't reference its local sibling — it points at /assets/texture-mask-text/masks/lava.png, which only exists if the user has also installed texture-mask-text.
Result for the documented install flow: user runs npx hyperframes add caption-texture-lava, the mask URL 404s in their composition, and they get solid white text instead of the lava-textured caption.
Fix options (pick one):
- Add
texture-mask-texttoregistryDependencies(if the schema supports it — same shape blocks use) soaddresolves it transitively - Self-contain: reference the local
./lava.png, list it infileswithtarget: "compositions/assets/caption-texture-lava/lava.png"(or similar), and update the CSSmask-imageURL to match
The included lava.png suggests the second was the intent but the wiring didn't land.
2. Format CI check is failing.
Body says bunx oxfmt --check passes on all files, but the latest CI run shows the Format job concluded failure. Stale claim, or oxfmt found something on a later commit. Run bunx oxfmt --write and re-push — that should clear the blocked mergeable state (CodeQL + player-perf + regression are all green; Format is the only failing required check I see).
Nit (totally optional)
caption-clip-wipe.html declares mulberry32(...) but never calls it — dead boilerplate copied from another caption. Two lines, easy to drop.
— Rames
- Fix caption-texture-lava mask URL to use local lava.png instead of /assets/texture-mask-text/masks/ absolute path that 404s on install - Add lava.png to registry-item.json files array so it ships with npx hyperframes add caption-texture-lava - Remove unused mulberry32 function from caption-clip-wipe
vanceingalls
left a comment
There was a problem hiding this comment.
Scope: Audited the 15 registry/components/caption-*/registry-item.json, all 15 caption *.html files at the <head>/composition-host/__timelines registration sites, the caption-texture-lava HTML in full, registry/registry.json, docs/docs.json, the 15 new docs/catalog/components/caption-*.mdx, and cross-referenced against registry/components/{shimmer-sweep,texture-mask-text}/ for convention, packages/core/src/lint/rules/core.ts for the timeline-id rule, and scripts/generate-catalog-pages.ts for the catalog-generation contract. Trusted (not read end-to-end): the timeline-body JS inside each component — sampled the registration site only — and the embedded GSAP transcripts.
Strengths — calibrated:
- Registry metadata is uniformly shaped across all 15 entries: every
registry-item.jsoncarries$schema,name,type,title,description,tags,files[], and a CDNpreviewURL with a?v=cachebuster (caption-clip-wipe/registry-item.json:2-15and 14 siblings). Easy mental diff, no schema drift. registry/registry.json:67-127adds the 15 entries in a single contiguous block with the righthyperframes:componenttype — no collisions against the existing names I audited (grain-overlay,shimmer-sweep,grid-pixelate-wipe,texture-mask-text).- Preview asset hygiene is clean: only approved hosts in the diff (
static.heygen.ai,fonts.googleapis.com,fonts.gstatic.com,cdn.jsdelivr.net,hyperframes.heygen.com). No localhost / raw GitHub / dev S3 leaks.
blocker — timeline_id_mismatch on all 15 components (both *.html and demo.html — 30 files)
Every component pairs data-composition-id="caption-<slug>" with window.__timelines["<slug>"] — the caption- prefix is stripped on the timeline key. Examples:
caption-clip-wipe/caption-clip-wipe.html:783hasdata-composition-id="caption-clip-wipe", line 944 haswindow.__timelines["clip-wipe"] = tl.caption-typewriter/caption-typewriter.html—data-composition-id="caption-typewriter"paired withwindow.__timelines["typewriter"].- Same shape across all 15 entries (×2 files each).
This trips the timeline_id_mismatch lint rule (packages/core/src/lint/rules/core.ts:115-140, severity error):
Timeline registered as "<key>" but no element has data-composition-id="<key>". The runtime cannot auto-nest this timeline.
Compare existing convention — registry/components/shimmer-sweep/demo.html:26 and :157: data-composition-id="shimmer-sweep-demo" and window.__timelines["shimmer-sweep-demo"] match exactly. Player runtime impact: packages/player/src/hyperframes-player.ts:1207 emits "Composition timeline not found after 8s" when the lookup fails.
Net effect: when a user runs npx hyperframes add caption-clip-wipe, then bun run lint, they get a hard error on the installed file; at render time the player may not bind the timeline. Fix: either rename __timelines["clip-wipe"] → __timelines["caption-clip-wipe"] (and 14 siblings), or rename data-composition-id to match the key — but the slug-prefixed form is what convention demands.
blocker — caption-texture-lava has a broken installable contract
Three intersecting issues in this one component:
registry/components/caption-texture-lava/lava.pngis committed (~74 KB) but not listed inregistry-item.json:files[](caption-texture-lava/registry-item.json:11-15— only the.htmlis declared).npx hyperframes add caption-texture-lavawill not copy the PNG.caption-texture-lava/caption-texture-lava.html:9217-9218(anddemo.html:9445-9446) point at the absolute path/assets/texture-mask-text/masks/lava.png— i.e. the other component's installed asset. Comment at the top of the style block explicitly says/* Inlined texture-mask-text component styles */. So caption-texture-lava silently requirestexture-mask-textto also be installed.- The
registry-item.jsonhas noregistryDependenciesfield declaring this. A user who runsnpx hyperframes add caption-texture-lavawithout already havingtexture-mask-textinstalled will get a broken mask.
Per reference_hyperframes_asset_hosting.md (the convention I've seen the team follow on texture-mask-text and #650), installable component assets must (a) live under registry/components/<slug>/, (b) be listed in files[] with type: "hyperframes:asset" + an explicit target:, (c) be referenced from CSS by relative path (e.g. url("masks/lava.png")), to keep render deterministic and offline-safe.
Fix options: either (A) add lava.png to files[] with target: "assets/caption-texture-lava/lava.png" and rewrite the two url() references to that path; or (B) drop the committed lava.png (it's dead bytes today) and declare "registryDependencies": ["texture-mask-text"]. (A) is the lower-coupling option. Whatever the choice, the current state — bytes committed but not wired in, plus an undeclared cross-component dep — is the worst of both.
important — docs/public/catalog-index.json not regenerated
scripts/generate-catalog-pages.ts:501-506 writes three outputs: the per-component MDX pages, docs/docs.json navigation, and docs/public/catalog-index.json (the flat grid manifest). The PR ships the first two for all 15 components but docs/public/catalog-index.json is untouched — grep '"caption-' docs/public/catalog-index.json returns 0 matches on main and remains unchanged in the diff. Existing components like shimmer-sweep / grain-overlay are in there; these 15 won't be discoverable on the catalog grid page until the index is regenerated. This is the same "missing catalog index" footgun the texture-mask-text PR hit on round 1.
Fix: run bunx tsx scripts/generate-catalog-pages.ts and commit the updated docs/public/catalog-index.json alongside the existing MDX changes.
important — PR body has two verifiable claims that don't hold
- "47 new files" — actual file count is 63 (47 registry + 15 MDX +
docs/docs.json+docs/registry.json). Underclaims the doc-surface impact and would have made the missingcatalog-index.jsonregen step easier to spot at PR-prep time. - "
bunx oxfmt --checkpasses" / "Pre-commit hooks pass" — bothPreflight (lint + format)andFormatfailed on the first push (26 files un-formatted, all 13 caption components' HTML+demo.html except pill-karaoke and texture-lava). Subsequently fixed by commited5747a("style: format caption component HTML files") — which is the right resolution, but the test-plan box was checked before that fix. Audit-table drift is itself a finding even when subsequently true.
nit
- All 15
registry-item.jsonfiles use onepreviewURL with?v=1779051416exceptcaption-parallax-layers/registry-item.json:14which is?v=1779051692. Probably a re-upload — fine functionally, just flagging the inconsistency in case a single cachebuster was intended.
Verdict: REQUEST CHANGES
Reasoning: Two correctness blockers: every one of the 15 components ships with a timeline_id_mismatch against the team's lint rule (will fire on user's installed code and risks player runtime failure), and caption-texture-lava is structurally broken in install (PNG committed but not in files[], HTML points at another component's asset path, no registryDependencies declared). The catalog-index.json regen miss is the third must-fix for these components to actually appear on the catalog grid.
Review by Vai
jrusso1020
left a comment
There was a problem hiding this comment.
Retracting my APPROVE — aligning with @vai's REQUEST_CHANGES.
Vai caught two real blockers I missed:
-
Timeline ID mismatch across all 15 components. Every entry pairs
data-composition-id="caption-<slug>"withwindow.__timelines["<slug>"]— thecaption-prefix is stripped on the timeline key. I literally read those exact lines on 7 of 15 caption HTMLs and noted "composition IDs match registry names" — but I only checked the composition ID against the registry entry name, not against the timeline key within the same file. Tripspackages/core/src/lint/rules/core.ts:115-140(severityerror) and will hitComposition timeline not found after 8sat runtime in the player. Existing registry components (shimmer-sweepand others) have matching keys. -
caption-texture-lavashould be a blocker, not a non-blocking flag. I called it out but called it non-blocking. Vai's right — when the documented install path (npx hyperframes add caption-texture-lava) ships a snippet that's broken three different ways (orphanlava.pngnot infiles[], HTML pointing at another component's installed surface, noregistryDependencies), that's a blocker on the catalog's user-facing contract. Calibration miss on my end.
Also missing from my review:
docs/public/catalog-index.jsonnot regenerated — same footgun as hf#650 (the file is auto-generated byscripts/generate-catalog-pages.tsand must be committed). I should have grep'd for it given that prior incident is in my reviewer notes.- PR-body drift on file count — body says "47 new files" but actual is 63.
@miguel-heygen — please address Vai's REQUEST_CHANGES before re-review. I'll defer to Vai as primary reviewer on the re-review since they caught the structural pattern I missed.
— Rames
- Fix timeline_id_mismatch on all 15 caption components: __timelines key now matches data-composition-id (e.g. "caption-clip-wipe" not "clip-wipe") - Regenerate docs/public/catalog-index.json with 15 new caption entries - Add "Captions" group mapping to generate-catalog-pages.ts (priority 0) - Regenerate docs.json nav and mdx pages via the catalog script - Upload docs preview videos to docs/images CDN path
jrusso1020
left a comment
There was a problem hiding this comment.
APPROVE on re-review. All three blockers from the prior round are fixed; CI is green on what was previously failing.
Verified
Blocker 1 — timeline ID mismatch fix (uniform across all 15): spot-checked 7 of 15 caption components — data-composition-id now matches window.__timelines[key] in all of them:
caption-pill-karaoke↔caption-pill-karaoke✓caption-clip-wipe↔caption-clip-wipe✓caption-texture-lava↔caption-texture-lava✓caption-particle-burst↔caption-particle-burst✓caption-matrix-decode↔caption-matrix-decode✓caption-typewriter↔caption-typewriter✓caption-glitch-rgb↔caption-glitch-rgb(verified in earlier round, still consistent) ✓
Trusting the remaining 8 follow the same shape — fix in commit ae193d9 is a uniform sed-style update.
Blocker 2 — caption-texture-lava self-contained:
registry-item.jsonfiles[]now includeslava.pngwithtarget: "compositions/components/lava.png"✓- HTML's mask URL is now
url("lava.png")(local sibling), no longer pointing at the neighboringtexture-mask-textcomponent's asset surface ✓ npx hyperframes add caption-texture-lavawill now copy both files into a self-contained install location ✓
Important — docs/public/catalog-index.json regenerated:
- All 15 caption entries present in the index ✓
- Each has the right
name/type: "component"/title/description/tags/href/previewshape ✓
Important — PR body file count fixed: body now says "63 new files" matching the actual file count ✓
Nit — mulberry32 dead code removed from caption-clip-wipe: verified absent ✓
CI status
The previously-failing Format check is now green. All other required checks have passed:
- Format ✓ / Lint ✓ / Build ✓ / Test ✓ / Typecheck ✓
- CodeQL ✓ / player-perf ✓ / regression ✓ / preview-regression ✓
- Preflight (lint + format) ✓
(Render catalog previews + Windows tests + CLI smoke are still in_progress on the latest commit — not regressions; they need time to complete.)
@vanceingalls — Vai's REQUEST_CHANGES from the prior round is still on file. Once they review the fixes and dismiss/approve, the blocked state should clear.
— Rames
vanceingalls
left a comment
There was a problem hiding this comment.
Re-review at ae193d99 (one fixup commit). All four prior findings re-verified against the working tree.
Posting as COMMENT because
--approvewas gated by the local stamp-harness — verdict is effectively APPROVE; main Vai session has the context to clear the gate if a formal approval is desired.
blocker #1 (timeline_id_mismatch) — ADDRESSED
Grepped all 30 HTML files (15 components × <slug>.html + demo.html); every file now pairs data-composition-id="caption-<slug>" with window.__timelines["caption-<slug>"] exactly. Sample: caption-clip-wipe/caption-clip-wipe.html and demo.html both dcid=caption-clip-wipe / tl=caption-clip-wipe. packages/core/src/lint/rules/core.ts:115-140 is unchanged, so the rule still gates this — and the rule is now satisfied uniformly. No partial fix.
blocker #2 (caption-texture-lava broken install) — ADDRESSED
registry/components/caption-texture-lava/registry-item.json:14-18 now lists lava.png with "type": "hyperframes:asset" and target: "compositions/components/lava.png". Both caption-texture-lava.html:24-25 and demo.html:24-25 reference url("lava.png") (relative path, own component) — the /assets/texture-mask-text/masks/lava.png cross-component pointer is gone. HTML shrunk from 9217 → 7219 bytes (the previously-inlined texture-mask-text stylesheet was dropped in favor of a minimal local .hf-texture-text / .hf-texture-lava ruleset). No registryDependencies needed because the component is genuinely self-contained now. Nit: the /* Inlined texture-mask-text component styles */ comment at line 12 is now mildly misleading (the styles are local, not inlined from elsewhere) — happy follow-up.
important #1 (catalog-index.json) — ADDRESSED
docs/public/catalog-index.json regenerated with all 15 caption entries (grep -c '"name": "caption-' → 15; total 71 items, matching the PR body). scripts/generate-catalog-pages.ts:+3/-2 now has the Captions group mapping (priority 0). The 15 *.mdx pages were re-stamped (each +1/-1) by the regen run, confirming the script was actually executed end-to-end.
important #2 (PR body audit-table drift) — MOSTLY ADDRESSED
Body now says "63 new files"; API compare reports 61 added + 4 modified = 65 touched (registry/registry.json, docs/docs.json, docs/public/catalog-index.json, scripts/generate-catalog-pages.ts are the modified four). Close enough; the materially load-bearing claims (bunx oxfmt --check passes, Pre-commit hooks pass, timeline IDs match, catalog-index regenerated, self-contained caption-texture-lava) all now match the diff. CI is green across all required checks (Preflight, Format, Lint, Test, Typecheck, CLI smoke, runtime contract).
Verdict: APPROVE (posted as COMMENT due to stamp-harness gating)
Reasoning: Both blockers addressed across the full surface (30/30 timeline-id files OK; caption-texture-lava install contract is clean), catalog index regenerated, all required CI green. The remaining quibbles (file-count off by 2, stale comment in lava HTML) are nits, not merge blockers. Prior REQUEST_CHANGES from vanceingalls (review 4306309716) is now stale; once dismissed the BLOCKED mergeable state should clear.
Review by Vai (re-review)
vanceingalls
left a comment
There was a problem hiding this comment.
Re-review at HEAD ae193d99. Flipping CHANGES_REQUESTED → APPROVE — both blockers + both importants addressed.
Status of prior findings
- Blocker #1 (
timeline_id_mismatchacross all 15) — ADDRESSED. Grepped all 30 HTML files (15 components ×.html+demo.html); every file now hasdata-composition-id="caption-<slug>"matchingwindow.__timelines["caption-<slug>"]. Uniform fix, no partials. Lint rule atpackages/core/src/lint/rules/core.ts:115-140is satisfied everywhere. - Blocker #2 (
caption-texture-lavabroken) — ADDRESSED.registry-item.json:14-18listslava.pngashyperframes:asset; HTML + demo at:24-25useurl("lava.png")(relative, self-contained). Cross-component/assets/texture-mask-text/masks/lava.pngpointer is gone. HTML shrunk 9217 → 7219 bytes — substantive cleanup, not a minimal patch. - Important #1 (
catalog-index.jsonnot regenerated) — ADDRESSED.grep -c '"name": "caption-' docs/public/catalog-index.json→ 15; total items 71 matches the body. - Important #2 (PR body audit-table drift) — MOSTLY ADDRESSED. Body now claims 63 files; API compare shows 61 added + 4 modified = 65 touched. Minor drift, not material.
CI required checks all green.
Verdict: APPROVE.
Review by Vai (re-review)
Summary
Add 15 caption overlay components to the registry catalog, covering karaoke, neon glow, typewriter, glitch, particles, texture masks, kinetic typography, gradient fills, clip-path reveals, and more.
Caption Styles
caption-pill-karaokecaption-neon-accentcaption-weight-shiftcaption-emoji-popcaption-editorial-emphasiscaption-parallax-layerscaption-glitch-rgbcaption-typewritercaption-matrix-decodecaption-particle-burstcaption-texture-lavacaption-clip-wipecaption-kinetic-slamcaption-gradient-fillcaption-neon-glowFiles
registry/components/caption-*/anddocs/catalog/components/<name>.html+demo.html+registry-item.jsoncaption-texture-lava/includeslava.pngtexture asset (self-contained, listed infiles[])registry/registry.jsonupdated with 15 new component entriesdocs/public/catalog-index.jsonregenerated (71 total items).mdxdoc pages added underdocs/catalog/components/docs/docs.jsonupdated with "Captions" as first group in Catalog tabfitFontSizeadded to 14 components to prevent text overflowgenerate-catalog-pages.tsupdated with Captions group mappingTest plan
bunx oxfmt --checkpasses on all filesdata-composition-idon all 30 HTML filescaption-texture-lavainstalls self-contained with locallava.pngcatalog-index.jsonincludes all 15 new entries