feat(registry): the video-primitive moves, documented and customisable - #3090
Open
miguel-heygen wants to merge 28 commits into
Open
feat(registry): the video-primitive moves, documented and customisable#3090miguel-heygen wants to merge 28 commits into
miguel-heygen wants to merge 28 commits into
Conversation
miguel-heygen
force-pushed
the
feat/video-primitives-registry
branch
from
August 7, 2026 18:22
c305559 to
48e8dc0
Compare
Fallow audit reportFound 50 findings. Dead code (9)
Duplication (22)
Health (19)
Generated by fallow. |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
3 tasks
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.
miguel-heygen
force-pushed
the
feat/video-primitives-registry
branch
from
August 9, 2026 18:01
eeeaf18 to
36fb3f7
Compare
| var el = document.createElement("img"); | ||
| el.id = "gt-img-" + k; | ||
| el.alt = ""; | ||
| el.src = src; |
| 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> |
…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}`); |
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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:.mp4on 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 underdocs/publicand 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 onbody, so the element cannot share a document with the docs around it.data-variable-valuessnippet pre-filled with that item's own defaults.navigator.clipboardis unavailable on insecure origins, so the fallback path is load-bearing rather than decorative.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:
liquid-glassblocks stopped installinglib/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.<video>with nosrc, 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
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 lintexits 0, 2555 CLI tests and 1909 core tests pass,bun run test:scriptsexits 0, the no-deletions gate passes (0 files deleted against main), and the search index covers all 358 items. Threedemo.htmlfiles changed:morph-text,motion-blurandyt-screen-warp.Size. 2,270 files. It is mostly generated output: the copies under
docs/publicthat the previews play, 358 generated pages, and the registry items themselves. Committing generated output matches what this repo already does —docs/public/catalog-index.jsonis tracked on main and nothing regenerates it at build time. Reviewingscripts/generate-catalog-pages.tsand 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.tsmirrors canonical bodies intodemo.htmland 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:
registry.jsonstill listed 64 items whose directories an earlier commit deleted, sohyperframes addwould resolve a name and then fail on missing files. Regenerated from disk.variablesarray 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.downloadFilehad lost its 30 second timeout in a rewrite meant only to fix redirects, leaving five callers with no stall guard. Restored.<video>removal left 24 CSS rules addressing elements that no longer exist. Removed.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.