Skip to content

feat(registry): the video-primitive moves, documented and customisable - #3090

Open
miguel-heygen wants to merge 28 commits into
mainfrom
feat/video-primitives-registry
Open

feat(registry): the video-primitive moves, documented and customisable#3090
miguel-heygen wants to merge 28 commits into
mainfrom
feat/video-primitives-registry

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Why

The catalog shipped 168 installable moves; the video-primitives work built 454. Those moves existed but were not installable, not documented, and not findable. This ports them in, documents them, and gives them variables.

Stacked on #3089 — review that one first.

How

The registry. 454 items, additive: every item main ships is still there, byte for byte. The no-deletions gate (scripts/check-no-main-deletions.mjs) runs on every commit and uses three-dot diff semantics, which is what a PR actually shows and what a merge actually does — the two-dot form reports deletions that a merge would never make.

The pages. All 358 are generated by scripts/generate-catalog-pages.ts, so they stay consistent. Four changes to that generator:

  • Previews play the composition. Every page pointed at a .mp4 on the CDN and every one of those answered 403 — the objects were never rendered or uploaded, so readers got a black box on a page whose entire job is to show them the thing. The player is already what is being documented, so the page embeds it: the item directory is copied under docs/public and an iframe loads it through a small wrapper. All but one play; the exception is a texture item with its own preview panel. The iframe is not decoration — compositions set styles on body, so the element cannot share a document with the docs around it.
  • The code is on the page. An independent reviewer compared these pages against shadcn/ui and motion.dev and returned one gap: they were pointers to a file the reader does not have yet. Each page now carries the item source in a collapsed panel, and every Variables table is followed by a paste-ready data-variable-values snippet pre-filled with that item's own defaults.
  • A visible copy button. A plain code fence renders one on hover only, and it was absent from the accessibility tree entirely. navigator.clipboard is unavailable on insecure origins, so the fallback path is load-bearing rather than decorative.
  • Navigation that can be browsed. The catch-all put most pages in one alphabetical "Effects" list. Rules keyed on tags already in the manifests split it; the largest group is now 73.

The variables. 173 of 358 items now declare them; main ships 2. These are not metadata: each is declared on the root, validated in the composition's own script, and wired to something visible, because a knob the code ignores would put a table in the published docs that lies about the piece. Every one falls back to its declared default on missing or unrecognised input, and with no overrides each item renders exactly as before.

Knobs were turned down rather than faked when they could not be honest — one that would contradict its own motion, one needing two different defaults depending on another knob, and an enum option that probed byte-identical to the default because the theme accent already was that token.

Two regressions this branch had introduced into files main already ships, both caught by the repo's gates and neither visible to the no-deletions check, since no file was deleted:

  • Four liquid-glass blocks stopped installing lib/liquid-glass.iife.js. The file was still in git; it had simply stopped being listed as something to install, so the installed composition's <script src> resolved to nothing. All 294 changed manifests were then audited against main: these four lost a file entry, and no item lost a top-level key.
  • Fourteen caption components gained an empty <video> with no src, which renders nothing and which the registry linter rejects. Removed surgically — four of the fourteen also carry a substantial rewrite, and a blunt revert would have taken that with it.

Test plan

  • Unit tests added/updated
  • Manual testing performed
  • Documentation updated (if applicable)

Every generated page resolves to a preview that exists on disk, and navigation resolves 498 page references with 0 dead links. Regenerating leaves the tree unchanged. Every variable declaration parses and deep-equals its manifest array, and every declared id is read by its composition, checked mechanically across the whole registry rather than sampled. Two pre-existing items declare the object-map form instead of an array and are excluded from that comparison.

Per item, variables were verified in a real browser: defaults byte-identical to the pre-change render on computed styles and on a pixel hash of the rendered element, bogus overrides identical to defaults, and every option measurably different. Motion knobs multiply a timeline-driven property so they never fight GSAP's inline writes; because those collapse to identity at rest, each was checked twice — once at rest and once with the driven property pinned mid-flight.

bun run lint exits 0, 2555 CLI tests and 1909 core tests pass, bun run test:scripts exits 0, the no-deletions gate passes (0 files deleted against main), and the search index covers all 358 items. Three demo.html files changed: morph-text, motion-blur and yt-screen-warp.

Size. 2,270 files. It is mostly generated output: the copies under docs/public that the previews play, 358 generated pages, and the registry items themselves. Committing generated output matches what this repo already does — docs/public/catalog-index.json is tracked on main and nothing regenerates it at build time. Reviewing scripts/generate-catalog-pages.ts and a couple of representative items covers the hand-written surface.

Not covered. 185 items still have no variables; that work is continuing. The 358 pages are generated and were not read individually — coverage is that every preview resolves and pages from both the block and component paths were opened and watched. scripts/sync-ui-primitives.ts mirrors canonical bodies into demo.html and those demos are now stale for the converted UI primitives; nothing runs that script in CI today.

Review follow-up

A two-axis review ran against this branch and its findings are fixed here, not deferred:

  • The branch carried re-authored copies of the catalog-search CLI commits rather than the ones on their own PR, so merging it would have rolled back six later fixes. Those files now come from that branch and the two agree.
  • registry.json still listed 64 items whose directories an earlier commit deleted, so hyperframes add would resolve a name and then fail on missing files. Regenerated from disk.
  • One item shipped a provenance block carrying internal identifiers, and four others carried absolute paths from the author's machine. Both removed, along with the type and the two JSON schemas that existed only to describe the first.
  • Ten manifests were missing the variables array their composition root declared, so their pages shipped no explorer. Synced; two more disagreed only in description text and the root was the truthful side.
  • downloadFile had lost its 30 second timeout in a rewrite meant only to fix redirects, leaving five callers with no stall guard. Restored.
  • A caption <video> removal left 24 CSS rules addressing elements that no longer exist. Removed.
  • Four build scripts that nothing invokes, two of which read a file outside the repository, are deleted.

Known gaps. The fallow audit still reports 12 findings against this branch: duplication across the three theme packs and some test boilerplate, plus two functions in the page generator above the complexity threshold. Clearing them needs test coverage on the build scripts, which is its own piece of work rather than something to fold in here.

@miguel-heygen
miguel-heygen force-pushed the feat/video-primitives-registry branch from c305559 to 48e8dc0 Compare August 7, 2026 18:22
@miguel-heygen
miguel-heygen changed the base branch from feat/catalog-smart-search to main August 8, 2026 00:44
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Fallow audit report

Found 50 findings.

Dead code (9)
Severity Rule Location Description
major fallow/unresolved-import packages/core/src/compiler/htmlBundler.test.ts:9 Import '../generated/runtime-inline' could not be resolved
major fallow/unresolved-import packages/core/src/compiler/htmlBundler.ts:23 Import '../generated/runtime-inline' could not be resolved
major fallow/unresolved-import scripts/generate-template-previews.ts:32 Import '@hyperframes/producer' could not be resolved
major fallow/unresolved-import scripts/generate-template-previews.ts:33 Import '@hyperframes/producer' could not be resolved
major fallow/unresolved-import scripts/generate-template-previews.ts:34 Import '@hyperframes/producer' could not be resolved
major fallow/unresolved-import scripts/generate-template-previews.ts:35 Import '@hyperframes/producer' could not be resolved
major fallow/unused-file themes/bold.css:1 File is not reachable from any entry point
major fallow/unused-file themes/editorial.css:1 File is not reachable from any entry point
major fallow/unused-file themes/neutral.css:1 File is not reachable from any entry point
Duplication (22)
Severity Rule Location Description
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.test.ts:200 Code clone group 1 (30 lines, 2 instances)
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.test.ts:233 Code clone group 1 (30 lines, 2 instances)
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.test.ts:623 Code clone group 2 (31 lines, 2 instances)
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.test.ts:670 Code clone group 2 (31 lines, 2 instances)
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.test.ts:700 Code clone group 3 (7 lines, 2 instances)
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.test.ts:738 Code clone group 4 (9 lines, 2 instances)
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.test.ts:769 Code clone group 5 (9 lines, 2 instances)
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.test.ts:834 Code clone group 6 (8 lines, 2 instances)
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.test.ts:1405 Code clone group 7 (9 lines, 2 instances)
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.ts:144 Code clone group 8 (8 lines, 2 instances)
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.ts:160 Code clone group 9 (20 lines, 2 instances)
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.ts:173 Code clone group 10 (6 lines, 2 instances)
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.ts:332 Code clone group 9 (20 lines, 2 instances)
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.ts:349 Code clone group 10 (6 lines, 2 instances)
minor fallow/code-duplication packages/core/src/compiler/htmlBundler.ts:364 Code clone group 8 (8 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/compositionLoader.test.ts:495 Code clone group 3 (7 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/compositionLoader.test.ts:532 Code clone group 4 (9 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/compositionLoader.test.ts:1656 Code clone group 5 (9 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/compositionLoader.test.ts:1693 Code clone group 6 (8 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/getVariables.test.ts:234 Code clone group 7 (9 lines, 2 instances)
minor fallow/code-duplication scripts/generate-catalog-pages.test.ts:153 Code clone group 11 (7 lines, 2 instances)
minor fallow/code-duplication scripts/generate-catalog-pages.test.ts:160 Code clone group 11 (7 lines, 2 instances)
Health (19)
Severity Rule Location Description
critical fallow/high-crap-score scripts/backfill-block-previews.ts:45 'backfillBlocks' has CRAP score 110.0 (threshold: 30.0, cyclomatic 10)
major fallow/high-crap-score scripts/backfill-block-previews.ts:82 'normalizeComponents' has CRAP score 72.0 (threshold: 30.0, cyclomatic 8)
critical fallow/high-crap-score scripts/generate-catalog-pages.ts:260 'textureSampleWord' has CRAP score 148.4 (threshold: 30.0, cyclomatic 24)
minor fallow/high-crap-score scripts/generate-catalog-pages.ts:749 'writePlayerPreview' has CRAP score 49.5 (threshold: 30.0, cyclomatic 13)
minor fallow/high-crap-score scripts/generate-catalog-pages.ts:1264 'generateItemMdx' has CRAP score 49.5 (threshold: 30.0, cyclomatic 13)
critical fallow/high-crap-score scripts/generate-catalog-previews.ts:74 'discoverItems' has CRAP score 306.0 (threshold: 30.0, cyclomatic 17)
critical fallow/high-crap-score scripts/generate-catalog-previews.ts:153 'prepareProjectDir' has CRAP score 420.0 (threshold: 30.0, cyclomatic 20)
minor fallow/high-crap-score scripts/generate-catalog-previews.ts:277 'generateThumbnail' has CRAP score 42.0 (threshold: 30.0, cyclomatic 6)
critical fallow/high-crap-score scripts/generate-catalog-previews.ts:365 'parseArgs' has CRAP score 110.0 (threshold: 30.0, cyclomatic 10)
minor fallow/high-crap-score scripts/generate-catalog-previews.ts:392 'main' has CRAP score 42.0 (threshold: 30.0, cyclomatic 6)
minor fallow/high-crap-score scripts/generate-registry-items.ts:90 'probeCanvas' has CRAP score 30.0 (threshold: 30.0, cyclomatic 5)
minor fallow/high-crap-score scripts/generate-registry-items.ts:110 'walk' has CRAP score 30.0 (threshold: 30.0, cyclomatic 5)
minor fallow/high-crap-score scripts/generate-registry-items.ts:161 'writeRegistryManifest' has CRAP score 30.0 (threshold: 30.0, cyclomatic 5)
major fallow/high-crap-score scripts/generate-registry-items.ts:192 'main' has CRAP score 72.0 (threshold: 30.0, cyclomatic 8)
minor fallow/high-crap-score scripts/generate-template-previews.ts:76 'parseArgs' has CRAP score 42.0 (threshold: 30.0, cyclomatic 6)
critical fallow/high-crap-score scripts/generate-template-previews.ts:97 'discoverTemplates' has CRAP score 132.0 (threshold: 30.0, cyclomatic 11)
minor fallow/high-crap-score scripts/generate-template-previews.ts:174 'main' has CRAP score 42.0 (threshold: 30.0, cyclomatic 6)
minor fallow/high-crap-score scripts/registry-primitive-payloads.test.ts:13 '<arrow>' has CRAP score 30.0 (threshold: 30.0, cyclomatic 5)
major fallow/high-crap-score scripts/variables-explorer.test.ts:134 'boundsOf' has CRAP score 72.0 (threshold: 30.0, cyclomatic 8)

Generated by fallow.

@mintlify

mintlify Bot commented Aug 9, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
hyperframes 🟢 Ready View Preview Aug 9, 2026, 3:46 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

Browsing the registry means matching names and tags, which fails whenever the
author's wording differs from yours. "make the pace feel faster" finds nothing
when the move is described as "velocity-driven blur". This ranks by meaning
instead.

Three tiers, and the command always says which one answered:

  words       shared vocabulary, free, offline, no account
  on-device   bge-small, free, offline, one opt-in download
  hosted      Gemini, free for signed-in HeyGen users

The tier is stated because a quietly worse answer looks exactly like a good
one. --json carries it as a token alongside dropped, shown, total and
top_score, so an agent reads provenance as data rather than matching English
that is written to be reworded.

Two consents, asked once each, and never conflated. Sending a query is a
privacy question, so the prompt says the query is sent. Downloading a model is
a disk and bandwidth question, so that prompt talks about size. Neither fires
without a terminal: an unattended run sends nothing and downloads nothing
unless a flag records that a person agreed.

The catalog is derived from registry-item.json rather than from a separate
document, so the set that is ranked and the set that can be installed are the
same object by construction. Only the on-device vectors are committed; the
hosted vectors are nine megabytes and belong on the server.

top_score is reported and never acted on. A "nothing matched" threshold looked
clean on long briefs and collapsed on the short queries people type: "a logo
appears" scores 0.6181 and keyboard mash scores 0.6417, so any cut that catches
the noise rejects the real query. The measurement is in the evals directory
rather than in this branch.

Not covered here. The published recall figures were measured against a separate
hand-written document, not against registry text, so they should not be quoted
for this catalog until re-measured. The offline tier needs a normal install: a
single-file build cannot load the native ONNX runtime, which the command now
reports instead of silently degrading. And the drop-detection path has never
been observed firing outside its author's tests.
Three things `bun run lint` and `fallow audit --base origin/main` rejected.
CI runs both, so none of this branch would have gone green. Found by running
them, not by reading the diff.

process.exit in catalog.ts, twice: an invalid --type and a cancelled picker.
check:cli-process-ownership reserves that for cli.ts, and the rule is not
cosmetic — process.exit tears the process down where it stands, so anything
cli.ts has queued to run on the way out is dropped. finishCommand throws a
CliResultSignal that cli.ts turns into the exit code, which is what init.ts
already does for a cancelled prompt.

Three exports with no consumers. normalize keeps its body and loses its export;
localEmbedder is the only caller. modelsDirectory goes entirely, having no
caller inside its file or out. The WordPieceConfig re-export goes, and with it
the import it existed to forward: the type is exported from wordpiece.ts, where
its consumers already take it from.

Complexity. prepareOnDeviceTier is lifted out of run(), which took run from 64
cyclomatic and CRAP 948 to 54 and 684. That block is one decision — can the
offline tier run, and if not, why not — and its only product is a list of
warnings, so it reads and tests as a unit, which it could not do inline.

The rest is suppressed rather than refactored, each with its reason on the line
above. Finishing run() means extracting its three output paths, and that is a
refactor of a command this branch already changes for other reasons: a separate
initiative, not something to absorb here. Every suppression says what shape the
function has and why; a bare marker on a function nobody can justify is how a
threshold stops meaning anything.

Verified: `bun run lint` exits 0, fallow reports no issues across 27 changed
files, and 2540 CLI tests pass.
Search now has two tiers, both local: shared-vocabulary word matching, and the
opt-in on-device model. The hosted tier, which sent the query to a HeyGen
endpoint and ranked it with a hosted model, is removed.

This is a scope decision, not a defect. The endpoint works and its own change is
reviewed and green; it is simply not what we want to ship first. Landing local
only means the feature has no backend dependency, no auth requirement, and
nothing leaves the machine unless someone opts into downloading a model.

Gone: registry/smartSearch.ts and its test, the --smart and --no-smart flags,
the outcome plumbing through the command, the remote branch of applySearch, the
remote tier, and the hosted-only JSON fields (ranking, catalog_version,
top_score). Also the smartSearchEnabled consent field in telemetry config, which
was the persisted storage behind the hosted consent and would otherwise have
been left as dead configuration surface.

Kept exactly as they were: both local tiers, the --on-device and --yes flags,
the download consent prompt, and the runtime check that happens before the
download rather than after it. The --json envelope still reports query, tier,
tier_detail, shown, total, dropped, warnings and results, so an agent can still
tell which tier answered and why. tierToken now distinguishes on-device from
words.

Verified: lint exits 0, fallow reports no issues, 2522 CLI tests pass, and the
command was exercised directly. A query answers on the on-device tier where the
model is installed and falls back to word matching where it is not, reporting
that fallback in warnings rather than silently. An unknown --type still exits 1
with a readable message, and --smart is now rejected as an unknown flag.
…g around them

Adds the motion primitives: 277 new components and the blocks that go with
them, plus the ui-primitives, themes and generators they are produced by. The
registry index goes from 176 items to 454, and the search catalog is rebuilt so
the set that is ranked is the set that can be installed.

Additive on purpose. An earlier pass of this port used rsync --delete, which
removed 101 files that exist on main because the incoming set is not a superset
of the current one: beat-freeze-cut and camcorder-hud among them. Whether the
re-port replaces those or sits alongside them is a product decision and not one
a sync flag should make, so nothing is removed here. If any of them are meant
to go, that belongs in its own commit where it can be seen.

The generator is ported too. Main's version only scans examples, so running it
without this change silently rewrote the index down to nine items. It also
rewrites example manifests from templates.json and will overwrite hand-edits;
those were reverted here after each run, and the diff is worth reading rather
than trusting.

Not covered. The 445 moves are not individually reviewed in this commit; the
machinery that ranks and installs them landed separately so it could be read on
its own. The internal evaluation corpus is deliberately absent: it is 1,400
files of briefs, gold labels and verdicts, and this repository is public.
Adds the Mintlify pages for the moves this PR ships: 163 component pages, 13
primitive pages, and the navigation that lists them. Without these the moves
land installable and undocumented, which is the worse half of a catalog.

Three things left out deliberately.

The 78 MB of docs/public. Nothing references it: every page loads its preview
from static.heygen.ai, so those bytes would be weight in a public repo with no
reader. Checked rather than assumed, by grepping the pages for the path.

Pages for the thirteen moves that were specified and never built. They had
documentation but no registry item, so a reader would have followed a page to a
`hyperframes add` that fails. Their nav entries are pruned with them, and every
one of the 309 remaining catalog and primitive nav entries was verified to point
at a page that exists.

Spike and scratch files that sit alongside the real docs on the source branch:
qa-gallery.html, experiment pages, bundled player javascript. They are working
artifacts, not documentation.

Not covered: the pages are generated output and have not been read individually.
The nav is verified to resolve, and the previews load from a CDN this commit
does not control, so a broken image would show up in review rather than here.
The pages this PR adds were unreachable: nothing in docs.json pointed at them.
This appends a Motion primitives group to the existing Catalog tab and a
Primitives tab, both built from main's navigation rather than replacing it.

Copying the source branch's docs.json wholesale was the first attempt and was
wrong. That file describes a different site, tabs Documentation / Catalog /
Primitives / Packages / SDK / Reference against main's Guides / Studio /
Catalog / Developers, and it references pages only that branch has, so the
preview server reported six dead links.

Verified by running the preview and resolving every entry: 484 page refs, 0
dead, no warnings. Group-relative refs are why a flat existence check is the
wrong validator here: cursor resolves through catalog/components and mcp
through guides, so checking docs/<ref>.mdx flat pruned 22 entries that were
fine.
Two regressions this branch introduced into items main already ships. Both were
found by the repo's own gates in packages/cli, not by reading the diff, and
neither is visible to the no-deletions check: no file was deleted, the contents
of files were changed.

The four liquid-glass blocks stopped installing their library. main lists
lib/liquid-glass.iife.js as a second file on each; the port wrote the older
manifest over main's and dropped that entry. The file is still in git and still
on disk, it simply stopped being something `hyperframes add` writes, so the
installed composition's <script src="lib/liquid-glass.iife.js"> would have
resolved to nothing. Every one of the 294 registry-item.json files this branch
touches was then audited against main: these four lost a file entry, and no
item lost a top-level key.

Fourteen caption components gained an empty <video>. The port added
`<video id="wp-video" ...></video>` — no src, no <source> — to each component
and its demo. It renders nothing and the registry linter rejects it as
media_missing_src. Removed rather than given a placeholder, because main's
version of each of these composes over whatever the host composition provides,
so the element only ever added a broken node; a made-up src would ship a
reference to footage that does not exist.

The removal is deliberately surgical. Four of the fourteen also carry a
substantial rewrite from the port, and only the media element and the rule that
styled it are touched, so a blunt revert cannot take the rewrite with it.

Verified: 2540 CLI tests pass, `bun run lint` exits 0. Before this, three tests
failed.
Four changes to generate-catalog-pages.ts, so all 445 pages stay consistent
rather than 445 files being edited by hand.

The preview plays the composition instead of pointing at a video. Every new
page pointed at static.heygen.ai/<name>.mp4 and every one of those answered
403, so the reader got a black box where the whole point of the page is to
show them the thing. The objects were never uploaded and rendering 445 of them
would have to happen again on every change. The player is already the thing
being documented, so the page embeds it: the item's directory is copied under
docs/public and an iframe loads it through a small wrapper. 444 of 445 pages
play; the remaining one is a texture item that uses its own preview panel.

The iframe is not decoration. Compositions set styles on `body`, so dropping
the element straight into the MDX would put a composition's global CSS in the
same document as the documentation around it.

Three things this got wrong first, all found by opening the page rather than
reading the output:

  - The wrapper loaded itself. `../<dir>/<name>.html` from inside preview/<dir>/
    resolves back into preview/<dir>/. The player embedded the player and the
    frame went black with a second set of controls shrinking into the corner.
  - Copying only demo.html was not enough. Most demos are a mount shell whose
    child carries data-composition-src="./<name>.html", so the sibling has to
    come with it. Every URL answered 200 and the frame was still empty.
  - `autoplay` and `loop` are not player attributes. Writing them did nothing
    and every preview sat paused on frame 0 — which is blank for any
    composition that animates in. The wrapper drives play() and loops on
    `ended` instead.

The Variables table. generateParams reads `params`; every item ported from the
video-primitives work declares `variables`, a richer schema with a type, a
default and a range. 112 items carry one and not a single page showed it, so
the most useful thing on the page was the one thing missing.

Nav groups. `if (entry.type === "component") return "Effects"` was the
catch-all, so Effects held 267 of 445 pages: an alphabetical wall. Rules keyed
on tags that already exist in the manifests split it; the largest group is now
73.

An install command with a visible copy button. A plain code fence renders one
on hover only, and it was absent from the accessibility tree entirely. This is
the one line every reader comes to take. navigator.clipboard is unavailable on
insecure origins, which is exactly the local preview these pages are written
against, so the fallback path is load-bearing and is what was exercised in
testing.

Verified: 888 preview URLs fetched, 0 failures. Regenerating three times in a
row produces no change, after a first attempt where "Variables" was added to
GENERATED_HEADINGS with a capital V — the set is compared lowercased, so each
run carried the previous section forward and appended a new one.

Not covered: the 445 pages were not read individually. Coverage here is that
every preview resolves and that a page from each of the block and component
paths was opened and watched.
A reviewer with no stake in the work compared these pages against shadcn/ui's
component pages and motion.dev, and returned one gap: the pages carry almost no
code, so they are pointers to a file the reader does not have yet. Its sharpest
example was the Variables table — names, defaults and accepted values, headed
"set the ones you want to change on the element", on a page that never shows an
element or the syntax for setting anything on one.

Two additions, in the generator so all 445 pages get them.

A snippet under the Variables table: the real mount element with
data-variable-values filled in from the item's own defaults, so it is
copy-and-run correct before it is edited. That is the syntax the demos actually
use, not an illustration written for the page.

The item's source, in a collapsed Accordion. These files run 99 to 463 lines,
so inlining them raw would bury everything else; collapsed, the code is on the
page and one click away. Accordion is already what these docs use for this.

A second reviewer, fresh, confirmed the change landed: it called the table and
snippet actionable rather than filler and said the collapsed source earns its
place.

Also here: the preview retries play() until the clock moves. `ready` can flip
before the runtime the player injects for a mounted sub-composition has finished
wiring up, and a play() landing in that window silently does nothing.

Both reviewers additionally reported every preview frozen at 0:00 and called it
fatal. It is not. The player's clock runs on requestAnimationFrame
(direct-timeline-clock.ts), browsers suspend rAF in a hidden tab, and the
reviewing tab was hidden: document.visibilityState read "hidden" while the
player reported ready and not paused, and a one-second rAF loop never completed
a single tick. Seeking the same composition by hand renders it correctly at any
offset. So the retry stops after ~15s instead of spinning forever, and the
comment says why an automated check of a background tab will always read 0.

Not covered: the reviewers' other standing finding, that only some items carry
variables at all, so the pages do not have one shape. 125 of the 206 items
tagged as a primitive declare none, and giving them variables means authoring
them into each composition, not editing metadata.

Verified: lint exits 0, the no-deletions gate passes, nav resolves 598 refs with
0 dead, and regenerating three times running changes nothing.
The catalog pages listed variables for 112 of 454 items and nothing for the
rest, so most pages could show a reader what a piece looks like but not what
they could change about it. This adds them to 55 more, taking the count to 167.

These are not metadata. A variable is only real if the composition reads it, so
each one is declared on the root, validated in the composition's own script, and
wired to something visible: travel distance, blur radius, direction, density,
accent family, tone, label text. Declaring a knob the code ignores would put a
table in the published docs that lies about the piece, which is worse than
having no table.

Every one falls back to its declared default when the incoming value is missing
or unrecognised, so a bad override degrades to the shipped look rather than to a
broken frame. With no overrides at all, each item renders exactly as it did
before: that was checked per item against `git show HEAD:` in a real browser,
comparing computed styles rather than eyeballing.

Four things this ran into that are worth writing down.

An apostrophe anywhere in a description terminates the single-quoted
data-composition-variables attribute and breaks the HTML parse. Every
declaration in the registry now parses; that is checked, not assumed.

Where a timeline drives GSAP's own y/scale/filter, GSAP writes inline styles
that beat any CSS custom property, so those knobs cannot be won from the
composition. Most of these items keep their motion in a user-owned "Timeline
integration" comment rather than in code, so no timing variables were declared
for them at all. A direction knob on a wipe can still be wired honestly, by
remapping clip-path inset sides through multipliers whose defaults reproduce the
original exactly.

Colour tokens that only reach a :focus-visible outline, or an element sitting at
opacity 0 at rest, render identically in a video. Those were skipped rather than
shipped as knobs that appear to do nothing.

CSS shorthand defaults need care: `border: var(--x, 0 solid transparent)` moves
computed border-color off currentColor even at zero width. Defaults were chosen
to reproduce the original computed style, not merely to look equivalent.

Not covered: 72 primitives still have no variables, and the UI-primitive demos
that scripts/sync-ui-primitives.ts mirrors are now stale for the converted
items. Nothing runs that script in CI today.

Verified: every declaration parses and deep-equals its manifest array, every
declared id is read by the composition, no demo.html changed, and lint exits 0.
…t clipping

Takes the count from 167 to 183 of 454. Same contract as the last batch: each
variable is declared on the root, validated in the composition's own script,
and wired to something visible, because a knob the code ignores would put a
table in the published docs that lies about the piece.

The snippet under each Variables table was clipping. Its data-variable-values
payload is one long line and the code block cut it off mid-value, with no wrap
and no scrollbar, so the one line on the page that exists to be copied could not
be read. The fence now carries `wrap`. Worth noting how that survived: the
generated markdown was correct and every mechanical check passed. It only failed
in a browser, which is where it was eventually seen.

Two techniques this round that are worth keeping.

Where an accent has a themed token family, the knob sets a new custom property
consumed by that one surface, with a fallback to the existing token, rather than
overriding the shared accent. Default therefore sets nothing, so an externally
themed accent is not clobbered, and the non-default options still follow the
theme in both light and dark.

Where GSAP owns the property outright and no CSS multiply can win — number-wheel
animates `y` inline — the knob is wired at build time instead: extra revolutions
lengthen the digit strip and move the target, so travel changes while the resting
frame stays identical. That is a real answer rather than a skipped knob.

Motion knobs that multiply a timeline-driven custom property collapse to identity
at rest, so every one of them was verified with that property pinned to a
mid-flight value rather than at t=0, where all options look the same by
construction.

Not covered: 56 primitives still have no variables.

Verified: every declaration parses and deep-equals its manifest array, every
declared id is read by the composition, no demo.html changed, lint exits 0, and
the wrap fix was confirmed on the rendered page rather than in the markdown.
Takes the count from 183 to 204 of 454. Same contract: declared on the root,
validated in the composition script, wired to something visible, defaults
reproducing the pre-edit render exactly.

Three kinds of knob were turned down this round rather than faked, and the
reasons are worth keeping.

A knob that contradicts its own motion. The sheet panel could be moved to the
left, but the recipe drives GSAP x from the right, so the panel would slide in
from the wrong side while the control claimed otherwise.

A knob that needs two defaults. A separator length means width horizontally and
height vertically, so one token would be wrong half the time.

An option that is not an option. Two components were given a green accent that
probed byte-identical to their default, because the theme accent already is that
token. A row in the docs table that does nothing is worse than a missing row, so
it was replaced with one that differs.

Accent knobs set a new property with a fallback to the shared token rather than
overriding it, verified by rendering with an external accent in place and
confirming the default still yields to it. Motion knobs multiply a
timeline-driven property so they never fight the inline styles GSAP writes;
because those collapse to identity at rest, each was checked twice, once at rest
against HEAD and once with the driven property pinned mid-flight.

Verified: every declaration parses and deep-equals its manifest array, every
declared id is read, defaults match HEAD on computed styles and on a pixel hash
of the rendered element, no demo.html changed, lint exits 0, and the
no-deletions gate passes.
This branch changes skills/hyperframes-cli/SKILL.md and
skills/hyperframes-registry/SKILL.md but never regenerated
skills-manifest.json, so the tracked fingerprints still described the old
files. CI enforces the two staying in sync and failed on it.

The lefthook pre-commit hook regenerates and re-stages this automatically when
skills/** is staged; these commits were made with the hook bypassed, which is
how the drift got in.
…gree with their compositions

Takes the count to 207 of 454.

Four items carried a different description for their exit variable in
registry-item.json than in their own data-composition-variables. The catalog page
renders the manifest, so the published table described the knob one way while the
composition header described it another. The composition wins: it is the file
that implements the variable and the declaration is what the runtime reads.

The skill docs no longer describe a hosted tier, since the CLI now ships the two
local tiers only, and skills-manifest.json is regenerated to match.
Takes the count to 211 of 454.

Two knobs are worth calling out because they touch things the timeline also
touches. skeleton-block slide multiplies the driven row offset, so it is
identity at rest and only bends the middle of the move. slider value sets the
resting fill together with the readout text, aria-valuenow and aria-valuetext,
so all three agree; a composition that tweens the fill takes over from there and
owns the readout, which the comment header states plainly rather than hiding. A
multiplier was rejected there because a 0 to 1 tween would push the fill past
the end of the track.

Knobs on elements that sit at opacity 0 at rest were kept only where the shipped
recipe reveals them, and verified with the reveal forced on as well as at rest.
Skipped: an aria-label string knob that never renders, and an accent token
declared in one item CSS that nothing consumes.
Removes 66 items tagged ui-primitive: accordions, buttons, inputs, dialogs, a
calendar. They are a shadcn-style interface component set that happens to be
expressible as HTML. None of them animate anything, so in a catalog whose job is
to offer moves for video they widen the surface without making it more useful,
and each one is a page a reader has to skip past to reach something that moves.

Every one is new on this branch. None exists on main, so nothing main ships is
being taken away; that was confirmed against origin/main before deleting rather
than assumed, and the no-deletions gate still passes.

Removed with them: registry/ui-primitives, the Operator Black token and contract
files only these items consumed, and the tooling that maintained them
(sync-ui-primitives.ts and scripts/lib/ui-primitives). No other registry item
declares a dependency on any of the 66, so nothing else loses a piece. The now
empty UI Primitives navigation rule goes too.

Generated output is pruned with the sources. The page generator writes files but
never removes ones whose source has gone, so a stale page would have survived and
404d its own preview. Verified: 0 orphan pages, 0 orphan previews, and the
navigation resolves 532 references with none dead.

This does discard variables authored for 54 of them earlier on this branch. That
work is in the history if these ever come back, and it is the right trade: they
should not have been in a video catalog to begin with.

The catalog is now 388 items. Lint exits 0 and 2522 CLI tests pass.
…able

The last 16 primitives get variables, so none is left without them. 173 of 388
items now declare variables; the rest are blocks and showcases, which are whole
scenes rather than parameterised moves.

Same contract throughout: declared on the root, validated in the composition
script, wired to something visible, and falling back to the declared default on
missing or unrecognised input. With no overrides every item renders exactly as
it did before, verified per item against the pre-change render in a real browser
at rest and at pinned mid-flight states, comparing computed styles and rects and
in most cases a screenshot hash.

This round refused several knobs rather than shipping ones that only look real.

A tilt-card depth knob was written, measured, and thrown away: the card sets
overflow hidden, which forces transform-style flat, so the authored translateZ
is already inert and every option probed identical. It ships a glow knob
instead, which drives inset and visibly changes at rest and under the drift.

slot-machine-roll has no free travel knob because the roll is exactly one row
height and any multiplier lands the reel off-register; size scales row height
and roll distance together, which is the only honest version. soft-blur-in
offers up and down but not left and right, because the shipped tween resets y
and not x, so a horizontal offset would never animate away.

Two pre-existing bugs surfaced while checking honestly, both left alone as out
of scope but worth recording. zoom-through-transition and tracking-in each tween
a custom property that is never set, so CSS reads it as zero and the move starts
from zero rather than from its authored value. The depth and tracking knobs are
scoped around that and their headers say so, rather than pretending the tween is
what it appears to be.

Verified: 388 items, 0 primitives without variables, 0 items where the manifest
and the composition disagree, 0 declared-but-unread variables, nav resolves 532
references with none dead, no demo.html changed, lint exits 0, and the
no-deletions gate passes.
Cuts 37 components, adds 8, and writes down the standard both decisions were
made against.

The 37 removals are all new on this branch and absent from main, so nothing
shipped is withdrawn. Each was audited with two pieces of evidence: source
identity after name normalisation, and a composition-level contact sheet
showing the members animate identically.

The largest group was 13 files byte-identical apart from an h3 and one
sentence. Nothing marqueed, panned, zoomed, deployed or dragged. An honesty
tiebreak decided survivors: frosted-glass-wipe has no backdrop-filter,
spring-scale-in has no spring, masked-slide-reveal has no mask,
short-slide-right travels up, and three-particle-ribbon differs from
three-orbiting-cards by one number while having neither particles nor a
ribbon.

Two independent audits agreed 10 out of 10 on a shared calibration sample,
in both directions, including three items a first pass wrongly condemned.

The rubric is the durable part. Fatal criteria are separated from fixable
ones, because no-timeline alone hits 97 items including some of the best;
promoting it would have cut 97 and left a worse catalog. It also records the
harness rules that make a verdict reproducible: render from the composition
rather than the demo, since demos carry content the installed item does not,
and mount sub-compositions rather than inlining them, since inlining renders
black frames indistinguishable from a dead item.

The eight additions target measured gaps. Camera language ranked first
because PSNR across 30 reference demos showed the most impressive
environments barely move: they are sets, not shots.

camera-shake carries nine lens-accurate profiles, amplitude scaled by focal
length so a wide lens shakes differently from a telephoto. rack-focus splats
each light through the aperture shape, so a defocused point becomes an image
of the iris, with flux conserved so highlights survive defocus.
camera-dolly-zoom solves focal length from distance, holding subject size to
0.000 percent drift while the background grows 53 percent. Plus
oscilloscope-trace with history-free phosphor persistence, bar-chart-race,
split-flap-board, spiral-galaxy and vfx-anamorphic-flare.

Each is verified by rendered frames and a seek-equals-playback check rather
than by check passing, which is not a visual gate.
The board declared 8s but every flap had settled by 3.5s, so more than half the composition was a still frame and the catalog preview opened on it.
A line wrap had put literal newlines inside the JSON string literals of the blocks data-hf-primitive-data payload, so JSON.parse threw in the browser and the composition never ran. The preview script hid it: it rewrote the payload in the temporary copy it captured from, so the catalog picture looked right while every installed copy stayed broken. That repair pass is gone and the payload is fixed where it ships.

Its two tests could not have caught this. Both were written against vitest in a directory the repo runs with node:test, so neither was in test:scripts and neither had ever run. They are converted and registered, along with a new one that JSON.parses every payload in the registry, and that one was checked against the re-wrapped shape before being kept.
var el = document.createElement("img");
el.id = "gt-img-" + k;
el.alt = "";
el.src = src;
Comment thread packages/cli/src/registry/localSemantic.ts Fixed
var el = document.createElement("img");
el.id = "gt-img-" + k;
el.alt = "";
el.src = src;
};

function scriptBody(html: string): string {
const match = /<script>([\s\S]*)<\/script>/.exec(html);
/** Whether a composition builds a timeline of its own. The recipe in the
* trailing comment of a snippet does not count: a comment animates nothing. */
function ownsTimeline(html: string): boolean {
return html.replace(/<!--[\s\S]*?-->/g, "").includes("__timelines");
<meta name="viewport" content="width=1920, height=1080" />
<title>Liquid Glass Media Controls</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<meta name="viewport" content="width=1920, height=1080" />
<title>Liquid Glass Widgets</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
Comment thread packages/cli/src/registry/localSemantic.ts Fixed
Comment thread packages/cli/src/registry/localSemantic.ts Fixed
…ation

chromatic-aberration-wipe described its accent as "the incoming scene's gradient" inside a single-quoted data-composition-variables attribute, so the attribute ended mid-JSON and the tag never closed. The formatter refused to parse the file, which is how it surfaced, but the runtime would have read a truncated declaration.

Also formats the 159 registry and docs files the branch had left unformatted, regenerates the skills manifest, and drops docs/primitives: those 13 pages import /snippets/PrimitivePlayer.jsx and read docs/public/primitives/, neither of which is on this branch, so mint failed the build on them. Nothing links to them and they ship whole on feat-video-primitives.

CI ran test:scripts before building core, so the preview test added here failed on a missing dist rather than on anything it checks. It now runs after the builds.
Ten items declared variables on their composition root but had no variables key in the manifest, so their generated pages shipped no explorer at all. Their manifests now mirror the root. Two more disagreed only in description text, and the root was the truthful side: both compositions paint an inset ring, not the slabs or colour pair the manifest described.

The caption <video> removal left 24 CSS rules addressing elements that no longer exist. Removed, excluding the four ids that were already orphaned on main.

thread-message-stack could not stay fixed: oxfmt reflows a divs contents and lands a newline inside a JSON string literal, so the payload broke again on the next format. A script tag is not an option because the runtime strips every script out of the mounted clone. The reader normalizes HTML whitespace instead, which is what makes it survive any reflow, and the guard test now asserts that contract rather than the byte layout.

downloadFile had lost its 30s timeout, DownloadOptions, and the mid-pipeline error plumbing in a rewrite that was only meant to fix redirects. Five callers were left with no stall guard. Restored, redirect handling kept.

warnUnknownEnumValues re-did the parse readDeclaredDefaults had already done. Both now share one readDeclarations, and the rest splits into compositionLabel, declaredOptions and unknownEnumValue. 1909 core tests unchanged.

Deletes build-qa-gallery, theme-gate and generate-primitive-pages: nothing invokes them, two read a coverage map four directories above the repo root, and the pages the third generates are no longer on this branch. Wires check-no-main-deletions, which is the opposite case, real and tested and never run.
This branch carried re-authored copies of the CLI search commits rather than the ones on their own PR, so merging it would have rolled back six later fixes: the vector cache that refuses a half download, the 0o700/0o600 modes, the rebuilt-from-registry index generator, the coverage gate and its CI job, and the scripts typecheck. Those files now come from that branch.

registry.json still listed 64 items whose directories the UI-primitive removal deleted, so hyperframes add would resolve a name and then fail on missing files. Regenerated from disk: 358 searchable items, 358 vectors, gate green.

Also drops an internal provenance block from thread-message-stack, along with the type and the two JSON schemas that existed only to describe it. It published an artifact id, a version id and a heygenverse:// URI, none of which mean anything to someone installing a block, and a public registry is the wrong place for them.

Typechecking scripts/ for the first time surfaced 45 errors in this branch. Fixed rather than suppressed: the geometry test reads positions through one accessor that names a missing index instead of letting NaN reach a tolerance compare, and the null-returning shape helper is asserted at its call sites, except in the test whose subject is the null.
generateItemMdx had grown to cyclomatic 26 across 196 lines while its own comments already named the seams. previewSection, usageSection and footerSection now own one each, taking it to 13. Regenerating all 358 pages afterwards produces a byte-identical tree, which is the check that matters for a generator.
// with an old matrix, which loads as an error instead of as stale data.
const fetched: Array<[string, Buffer]> = [];
for (const file of ["local-vectors.json", "local-vectors.bin"] as const) {
const response = await fetch(`${base}/catalog-artifact/${file}`);
Comment thread packages/cli/src/registry/localSemantic.ts Dismissed
Taking files wholesale from the catalog-search branch reverted the downloadFile timeout restored one commit earlier, so five callers were back to no stall guard at HEAD. Restored on both branches this time, since that branch never had it either.

It also took that branch test:scripts line without the vitest it depends on, so the script exited 127 and the CI Test job would have failed on a missing binary rather than a test. vitest is a root devDependency now, and the run is scoped to scripts/catalog/ with the slash: without it the prefix also matched catalog-preview-temp.test.ts, a node:test file with no vitest suite in it. Both branches had that one.

Four registry items and their docs copies carried absolute paths from a working directory. A public registry is the wrong place for them and history is permanent, so the sentences now name the source without the path.

Skill docs came from before the code they describe: the catalog command reports unindexed and applies installability after ranking, and both SKILL.md files now say so.

Also drops a double type assertion and ten dead ?? NaN coalesces from the geometry test, the second of which reintroduced exactly the NaN-into-a-tolerance-compare that the checked accessor exists to prevent.
The scripts typecheck failed on generate-registry-items importing @hyperframes/core by package name. It resolves on a machine with a warm node_modules, which is why it passed locally, and not in CI. Every other script in the directory already imports core from source and says why in a comment.

The preview job derived its item list with a sed that needs a trailing slash, so registry/components/CATALOG.md never matched, survived as a full path, and was handed to the renderer as an item name. The grep now requires a directory component. Simulated against this PR: 219 items, none of them a path.
store-badge-lockup vendored gsap 3.14.2 as a 4,200 line minified file and installed it into the users project, while 540 other items load that exact version from jsDelivr. Repointed, the copy deleted and the manifest entry with it, so hyperframes add store-badge-lockup no longer writes a second copy of gsap into someone elses compositions directory. Re-rendered and re-generated: the preview still draws.
Replaces the iPhone and MacBook models in the three device blocks. The old assets were untitled meshes with no keyboard on the laptop; these name every part and model the keycaps, speaker grilles, antenna bands and camera plateau.

Not a drop-in. The compositions found the screen by side effects, the material that happened to carry an emissiveMap for the phone and a mesh literally called matte for the laptop, and neither exists now. They select front-glass and display instead.

Both panels ship UVs authored for a tiling material, the laptop runs u -6.3 to 6.3, so one screen image clamped and smeared across the panel. Planar UVs are derived from each panel bounds at load.

The old phone display sat at the model minimum Z and the timeline spins assume a screen facing -Z. These face +Z, so the model is aligned by reading which of its own parts is front rather than re-timing the animation.

Removes the hand-drawn Apple logo from two blocks: the replacement ships apple-logo meshes, and the drawn one used coordinates read off the old lid, so it floated beside the device.

The preview copy only took top level files, so models/, lib/ and assets/ never reached docs/public and 38 items rendered there without their assets. That is the source of the non-blocking 404s in the preview job. It recurses now, which also brings vendored bundles across, so the generated tree is out of the lint scope.

The html-in-canvas notice is a Danger callout: without the flag the preview is a black rectangle, which is a prerequisite rather than a caveat.
const copy =
drivable && fromDir === itemDir && entryName === entry
? Buffer.from(appendToBody(entryHtml, variableBootstrap(ownFile)))
: readFileSync(from);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants