mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
0e4da52c8222b8d18a1211b34f2fb3bd0f7e79ee
703
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cbd38b32f7 | fix(catalog): restore the caption texture preview (#3258) | ||
|
|
c71fab322e | fix(catalog): center the menu morph preview (#3256) | ||
|
|
a2109a8633 | fix(catalog): keep the text stagger preview inside the frame (#3255) | ||
|
|
158f15e8f0 | fix(catalog): keep the animated bar chart preview in frame (#3254) | ||
|
|
dc01a12d71 | fix(catalog): keep the typewriter preview inside the frame (#3249) | ||
|
|
0474d4ae26 | feat(registry): add ChatGPT and Claude exchange templates (#3244) | ||
|
|
9b12329652 |
docs: simplify the Catalog overview with a section tour (#3240)
* docs: design Catalog overview video * docs: plan Catalog overview video implementation * test: pin Catalog overview montage contract * build: prepare Catalog overview video assets * feat: build Catalog overview montage * feat(docs): add Catalog overview preview loop * docs: simplify the Catalog overview * feat: strengthen Catalog montage selections * feat: showcase requested Catalog components * docs: embed Catalog tour in the HyperFrames player * fix(docs): harden catalog overview delivery * refactor(docs): run Catalog tour from source |
||
|
|
93fbb6d7fd | fix(registry): normalize ai chat portrait canvas (#3235) | ||
|
|
c69692464e | fix(registry): align notes reveal typing (#3234) | ||
|
|
e0ba41c024 | chore: release v0.7.107 (#3228) | ||
|
|
4347af3fe1 |
feat(registry): add five vertical ad-template blocks and their motion primitives (#3217)
* feat(registry): add the notification-cascade block and banner-push-in component A vertical ad-template block: phone notifications land one after another on a 2.000s beat and stack up the screen, each banner pushing the pile down as it arrives, before the whole stack lifts away into a closing card carrying the HyperFrames lockup. Every decisive number - the 214px pitch, the 22px entry rise, the expo.out restack - is measured from reference footage rather than invented. The backdrop plate is a generated photograph, shipped with the item. The banner-push-in component carries the reusable arrival move on its own: one banner fades in and rises into place while everything already on screen slides up one pitch in lockstep. Both pass the full browser gate (runtime, layout, motion, WCAG AA contrast) installed into a scratch project from a served registry. * feat(registry): add the share-sheet-carousel block and sheet-spring-up component A share sheet springs up from the bottom of the phone and cycles photographic previews inside its card on a fast cut rhythm - fourteen cuts over 7.2s - while the blurred ground swaps behind it on the same beats, ending on an accept tap. A brand strip with the HyperFrames lockup rides the bottom of the preview, as the reference's did. The four stills are generated photographs, shipped with the item. The sheet-spring-up component carries the arrival on its own: a real spring sampled frame by frame from reference footage - up from below, 192px overshoot, 0.72s settle - shipped as the sampled table rather than an ease that approximates it. Both pass the full browser gate (WCAG AA) installed from a served registry. * feat(registry): add the typing-indicator and streaming-text components Two chat primitives extracted from reference footage of real conversations. typing-indicator is the three-dot bubble that says someone is writing: it pops in at the thread's edge on the measured entry (scale 0.6 to 1, alpha 0.4 to 1, 0.10s power2.out), holds, and vanishes when the message lands. An optional bounded dot wave is included for compositions that want the dots to breathe. streaming-text is not a typewriter: words arrive in the uneven bursts of a streamed AI answer, on a token rhythm measured word-by-word from the reference, each word landing grey and inking to full colour over 0.27s. The GAPS table ships in the integration note and cycles for any word count. Both demos pass the browser gate. * feat(registry): add the ai-chat-reveal block A question is typed on a rising keyboard - measured keystroke rhythm, steady caret - sent, and answered by an AI that streams its reply word by word: thinking dot on the measured alpha ramp, tokens arriving in uneven bursts, a bulleted list, then a cut to a branded closing card. The typing and streaming schedules are generated at load from the measured gap tables, so every piece of copy is a variable - question, three answer paragraphs, three bullets, and the whole closing card - while the rhythm stays the reference's. Kept at the native 750x1624 capture size so the measured keyboard geometry stays pixel-true. Passes the full browser gate (145/145 WCAG AA) installed from a served registry. * feat(registry): add the message-thread-reveal block A full text conversation plays out on a phone: bubbles popping in on their measured entries, typing indicators holding the beat, a shared link card, an emoji burst, delivered receipts, the composer typing five messages keystroke by keystroke - and the whole thread scrolling up beat by beat on the measured y-choreography until a warm closing card lands. Every message swap keeps the measured keystroke tables intact: the new copy is length-matched character for character to the reference's typed lines, so all 97 typed frames land on their original beats. The footage plate is replaced by a CSS room-and-phone-body, the link card carries a generated still, and the closing card is rebuilt around the HyperFrames lockup with its stars, proof line, and feature icons preserved. Relates to the thread-message-stack work in #3046: that PR ships a caller-payload message-stack component; this block is the complete ad-template film around the same subject. Kept as its own commit so either can move independently. Passes the full browser gate (65/65 WCAG AA) installed from a served registry. * feat(registry): add the notes-reveal block and line-reveal-list component A personal note is typed character by character in a notes app - the caret riding every letter on the measured keystroke frames, the page scrolling up on the measured settle curves as each line lands - then cuts to a paper card with a marker headline, a circled word, a ticked checklist, and the video card. New copy is length-matched line by line to the measured reveal table, so all 176 keystrokes land on their original frames. The two hand-drawn faces (Permanent Marker, Courier Prime - both OFL) ship with the item; the reference's proprietary UI face is replaced by the system stack. The line-reveal-list component carries the paragraph rhythm on its own: lines land on the measured writing cadence that slows as the thought gets heavier - 2.20s, 2.03s, 2.97s, 3.10s, 3.33s, 3.63s - and the page makes room with the measured scroll-settle table instead of a linear glide. Both pass the browser gate (366/366 WCAG AA for the block) installed from a served registry. * docs(catalog): publish the ten new ad-template items Regenerated pages, payloads, navigation and the flat index for the five blocks and five components added in the preceding commits. Every existing page is untouched: the diff is ten new item pages, their payload assets, and the two regenerated indexes. * chore(registry): rebuild the on-device search index for the new items * style(registry): run oxfmt over the new item sources The repo-wide format gate covers registry HTML; the staged-file hook did not reach these. Payloads and pages regenerated to match. * feat(registry): restage the five component demos as device specimens The first demos dropped each primitive onto a bare canvas, which stripped the context the moves were measured in - a dark bubble on a black frame, a sheet floating in empty gradient. Each demo now plays inside a phone on a studio ground, in the move's native habitat - lock screen, chat thread, share sheet, assistant answer, notes page - with a specimen caption naming the component and its measured numbers. Interiors are authored at 1080x1920 and scaled into the screen, so they reuse the blocks' gate-proven geometry. Timelines are unchanged. All five pass the browser gate. * refactor(registry): shelve the five ad-template films as showcases The video-primitive tag pins an item to the Motion Primitives shelf and overrides every other grouping rule - right for an atomic move, wrong for a 25-second film. The five ad-template blocks move to the Showcases shelf (Scenes & demos), where complete scenes live; the primitives shelf keeps only the moves. * refactor(registry): retire banner-push-in and line-reveal-list Reviewed against the shelf they would join and cut as duplicates. banner-push-in was the thinnest cut against the existing notification family - notification-stack already owns the arrival-plus-lockstep-shift move, mirrored. line-reveal-list was pinched from both sides: the existing line-by-line-slide owns generic staggered lines, and the incoming notes-typing scene carries the measured writing cadence and scroll-settle at scene level. Their measured tables live on inside the films they were extracted from. * feat(registry): add four scene primitives - the moments, mountable social-proof-card: the app-store close as one scene - wordmark, five stars, proof line, three feature icons, CTA capsule - in a quiet stagger. Composes with, not against, star-rating-fill / testimonial-proof-card / cta-lockup, which stay the atomic owners of their moves. marker-checklist-card: a hand-lettered paper card - marker headline, an ellipse that draws itself around one word, checklist rows whose checks draw on. The paper-and-marker sibling of mk-specs-list. chat-thread: a conversation as a scene - measured bubble pop-ins, expo lockstep shifts, typing dots holding the beat, a media placeholder. Message content is one variable: heights are measured at mount and the choreography is generated to fit. Relates to #3046's caller-payload stack; kept independent. notes-typing: the notes-app confession - lines typed on the measured human keystroke rhythm, the caret riding every landed character. The scene counterpart to typed-prompt's bare prompt line. All four follow notification-stack's mountable-scene pattern: container units, theme-contract tokens, one paused timeline registered under the literal id. All demos pass the browser gate. * docs(catalog): regenerate for the reshelved films and scene primitives Ten pages become twelve: the five films move to the Showcases shelf, the two retired components' pages are gone, and the four scene primitives get pages under Motion Scenes. Every untouched page is byte-identical. * feat(registry): bring chat-thread to film fidelity, with a per-message type switch The scene now carries everything the reference conversation carries: status bar, back badge, avatar and name pill, bubbles with real iMessage tails, a Delivered receipt under the last sent bubble, large emoji rows, a media placeholder, and a link card with title, domain and chevron. Every message picks its type by prefix, the way marker-highlight picks a style: recv:/sent: text bubbles, emoji: a large row, img a media placeholder, card:Title~domain a link card. Heights are still measured at mount and the choreography generated, so any mix of types fits. * feat(registry): add chat-message - the atom, with its type as an enum ONE message arrives on the measured entry, growing from its sender's corner. The type is a proper enum - Received, Sent, Emoji, Image, Link card - switched on the catalog page the way marker-highlight switches its style, with the bubble tail and the Delivered receipt as their own switches. chat-thread remains the conversation-as-scene built from arrivals like this one; the pipe grammar belongs at that level, the enum belongs here. Demo cycles all five types, one at a time, with a type chip naming each. * fix(registry): neutral composer placeholder, gsap 3.14.2 pin, exact font licences Review fixes for #3217: - Rendered composer placeholder "iMessage" -> "Message" in chat-thread, its demo, and message-thread-reveal (technique comments stay as provenance) - notes-reveal now loads gsap@3.14.2 like every other item - Licence wording names each face: Courier Prime OFL 1.1, Permanent Marker Apache-2.0 (verified against embedded name-table records) Mirrored catalog payloads and pages regenerated for the four touched items. |
||
|
|
c9dd8413c3 | chore: release v0.7.106 (#3197) | ||
|
|
1ae2067b8d |
feat(catalog): put the variables panel back, on payloads (#3199)
* feat(catalog): put the variables panel back, on payloads The panel drove its preview by loading an .html from docs/public, a type the host does not publish, so it showed an empty frame in production and was parked when the catalog was re-landed. It now mounts the same JSON payload the plain player uses and re-mounts it as values change, injecting them as window.__hfVariables into the composition head before any of its scripts run, which is where the runtime reads overrides from. Doing it in the markup rather than after load is what stops the composition initialising with the wrong values first. 172 items with variables get the panel back; the playhead carries across a change so a tweak mid-shot does not jump back to frame zero. * fix(docs): drop the unused url form and the needless escapes * fix(docs): the panel cannot reference a binding beside the export * feat(catalog): make importing an SVG the obvious move A reader arrives at this control with a shape, not with path data, and the panel asked for the coordinates first. Import is now the primary action in a drop target you can see is a drop target, and the raw path sits behind a disclosure for anyone who wants it. * feat(cli): let a fruitless catalog search report the gap An agent that searches by meaning and finds nothing worth installing knows something we do not: the name of a move the catalog is missing. There was no way to tell us, so that knowledge was lost at the end of every run. hyperframes feedback --search-miss "<query>" --wanted "<the move>" records it. It carries no rating, so it never lands in the rating metric, and it is a separate deliberate command rather than something catalog --query does on its own: plain search still sends nothing, which is what the CLI promises. --rating stops being required at the arg level, since a miss has no rating to give. The check moved into the run body, where an absent one is now handled rather than crashing on undefined. * feat(cli): carry tuned variable values into the install snippet Someone who tunes a block on its catalog page had no way to keep those values: the install command was the same one everybody gets, and the tuning stayed on the page. hyperframes add <item> --vars '<json>' now prints a mount element carrying data-variable-values, so the values land where the block is used. They ride on the host rather than being written into the installed file. That keeps the composition on disk byte-identical to the registry's, so a later reinstall can still tell an edit from an update, and it lets two mounts of the same block carry different values. * fix(catalog): serve the item's own directory so runtime paths resolve Some compositions assemble their asset URLs at run time — "compositions/components/" + texture + ".png" for the texture masks, a font the compiler pulled into _remote_media — and no scan of the markup can see a string that does not exist until a script concatenates it. Those items either rendered black or were dropped to a video that had never been uploaded. Each item that needs it now has its prepared directory published, and its payload carries a <base> pointing at it, so any relative path the composition invents resolves. caption-texture renders its masks again, and variable-font-flex has a preview at all for the first time: its MP4 and poster are both 403. Both layouts are published, because which one a composition asks for differs per item, and a directory only earns that if it is under 2 MB. The 12 MB texture sheet keeps the recorded video it already had. * fix(catalog): let the variables panel actually drive the composition Every control on the panel was inert. The values reached the composition and nothing repainted, because the payload had already been compiled: compiling inlines a mounted component and resolves its variables into the markup and CSS, so by the time a reader turns a knob there is nothing left to change. An item that declares variables now ships uncompiled, keeping the mount the runtime loads at run time, which is the only state where data-variable-values still means anything. The component travels inline as a data URI rather than a sibling file, because .html is the one type the docs host will not publish. The demo's own pinned values come off, so the reader's choices reach the mount instead of losing to the values the demo picked to show itself off. Measured on the rendered frame rather than the DOM: green rgb(98,207,144), blue rgb(6,6,199), violet rgb(177,147,230), and back to green. docs/public/catalog drops from 48 MB to 35 MB along the way, since an uncompiled payload carries far less than an inlined one. * feat(catalog): keep variable changes in the url A reader who tuned a piece lost it on reload, and had nothing to send anyone. The values now live in the query string, scoped by composition id so two links never read each other,and only the ones that differ from the defaults are written, so changing one knob gives a short URL rather than every variable spelled out. replaceState rather than pushState: dragging a slider should not leave a trail of history entries. An unreadable value is ignored rather than thrown, so a truncated or hand-edited link opens the piece at its defaults. * fix(catalog): only rewrite the url when a value actually changed * refactor(catalog): memoise the declared defaults on their content * feat(catalog): offer an install command carrying the tuned values The Install block is generated before anyone touches a knob, so it can only ever print the plain command. Someone who spent a minute tuning a piece copied it and got the defaults back. The panel now carries its own command in the Snippet tab, with --vars holding exactly the values that differ. An untouched piece still offers the same short command, so nothing gets noisier for the common case. * fix(catalog): a piece with nothing to render is a skip, not a failure caption-blend-difference is a stylesheet and a paragraph of prose — a class you add to your own captions, with no standalone scene to show. The generator treated that as a build failure, so every run ended by reporting something broken when nothing was. It now reports the shape it is and keeps its recorded video, which is the only honest preview such an item has. A genuine render failure still throws. * fix(catalog): restore variables from the url on a cold load A shared link opened at the defaults. The first render happens on the server, where there is no window to read the query string from, and React then hydrates against that markup and never revisits it — so the values only appeared once you touched a control. The URL is read again after mount, which is the first moment it exists. The value is also escaped once now rather than twice: URLSearchParams already decodes on the way out, and decoding a second time turned an SVG path full of percent-escapes into something that no longer parsed, besides doubling the length of every link. * fix(catalog): mount the preview with the values a link carried The frame was built from the declared defaults and the shared values were posted to it afterwards, which is too late for anything the composition reads once at init: a path arrived after the mark had already been drawn from the default one, so a link looked right in the panel and wrong on screen. * feat(catalog): the install command follows the values you tuned Copying the Install line gave the plain command back, because that block is generated before anyone touches a knob and had no way to know what changed. The tuned command only existed in the panel Snippet tab, which is not where anyone looks for it. The line now reads the same query string the panel writes, so the two agree without either component knowing the other exists, and a shared link carries the right command too. replaceState fires no event, so the panel announces its own writes. * fix(catalog): send a text variable to the preview once it is finished Every other control in the explorer reports a whole value on every event: a slider at any position is a position, a swatch is a colour. A text field is not. Typing v3 into a badge posted v first, so the preview remounted and rendered a composition built from half a word. The post now waits while a text field has focus and goes out when the edit is committed, with Enter or by clicking away. The field itself is unchanged and still tracks every keystroke. --------- Co-authored-by: Miguel Angel Simon Sierra <miguelangelsi07@gmail.com> |
||
|
|
ce18acf075 |
feat(catalog): group the sidebar by what you came to make (#3194)
* feat(catalog): group the sidebar by what you came to make Nineteen shelves in one column is a list to read, not a menu to scan, and two of them were oversized: Effects held 54 items and Showcases 38, two thirds of which were near-identical code-snippet themes. The shelves now sit under eight openable sections named for what a reader is making, and the editor and terminal themes get a shelf of their own. Every existing shelf name survives underneath, so a bookmark into the catalog still lands where it did. Two things had to change to support nesting. The summary count walks sections as well as shelves, and the filter that preserves hand-written groups now recurses: a section holds groups rather than pages, so a check that only read `pages` found nothing generated in one, kept it as if a human had written it, and appended a fresh copy on every run. * fix(catalog): type the nested page count * fix(catalog): nest shelves inside pages so the sidebar renders them |
||
|
|
9734578e60 |
feat(registry): bring back the video-primitive moves (#3169)
Restores the 208 catalog items reverted after their previews 404'd in production, this time on the payload mechanism rather than the .html files that caused the outage. The generator no longer writes a preview document to docs/public. That writer, and the machinery under it, existed only to produce files the docs host discards, so it is gone rather than bypassed. Items now embed the composition itself via a payload, which is what the previous change already does for the items that were already in the catalog. The variables explorer is parked, not restored: it drove its preview through the same unpublished .html path, so it would have shown an empty frame. Items that declare variables get the live player plus the static variables table, and reconnecting the explorer to payloads is a follow-up. |
||
|
|
0f76305191 |
fix(catalog): keep the recorded video for canvas drawElement previews (#3171)
Eight blocks paint DOM into a canvas with ctx.drawElementImage(), which sits behind chrome://flags/#canvas-draw-element. A reader without the flag gets a preview that mounts, plays, and shows an empty canvas, which is what the 3D device blocks were doing even after their models were embedded correctly. The recorded video was captured by a renderer that has the capability, so it is the only preview these items can honestly show. All eight have a published mp4 and poster. |
||
|
|
470f802d9f |
fix(catalog): resolve assets a script loads by name (#3170)
The 3D blocks fetch their model with loader.load("models/iphone.glb"), which no
attribute or url() pattern can see, so the path survived into the payload
untouched. Inside a srcdoc iframe it resolved against the docs page and 404'd,
leaving vfx-iphone-device and ios26-liquid-glass mounted and playing with an
empty scene.
Names that resolve to a real file in the item's own directory now count as
references. They are deliberately weaker than markup references: a name that is
not a file is ignored rather than failing the item, so an ordinary string that
happens to look like a filename cannot push an item onto the video fallback.
Also clears an item's payload when a run decides it cannot build one, so the
page generator stops finding a previous run's file on disk and emitting a
player for a preview that no longer exists.
|
||
|
|
536165b6ef |
feat(catalog): play the real composition on catalog pages (#3168)
* feat(catalog): play the real composition on catalog pages Catalog previews were uploaded MP4s, published by hand to the CDN. Each page now embeds the composition itself, running in <hyperframes-player>, so a preview is the block rather than a recording of it. The composition is delivered as JSON under docs/public/catalog, and the player is mounted inside an iframe. Both are forced: the docs host publishes only JSON and images out of docs/public, and its MDX renderer strips unknown custom elements, so a player written into the page never reaches the DOM and an .html payload 404s in production. Assets are inlined as data URIs, which sidesteps the file-type restriction for fonts, scripts and models alike. 164 of 168 items build a payload; the rest keep their MP4 and say so. * chore: rebuild docs preview * chore: route isolation probe * chore: drop the route isolation probe page * chore: probe which asset types the docs host publishes * refactor(catalog): host preview assets instead of inlining them Fonts were being base64'd into every payload that used them, so a handful of files cost tens of megabytes in the repository to say the same thing over and over. Assets are now written once, content-addressed, under docs/public/catalog/assets and linked. Which types can be hosted was settled by fetching one file of each from a deployed preview: woff2, wav, mp4, svg and the image formats are published, glb is not. Types the host drops still travel inside the payload, because a link that 404s is worse than a larger payload. Also fixes the four type errors the scripts typecheck caught, all of them unchecked index access on a split() result. * refactor(catalog): share preview assets instead of repeating them * style: format the asset module |
||
|
|
945988d61f |
docs(changelog): weekly digest 2026-08-03–2026-08-10 (#3154)
* docs(changelog): weekly digest 2026-08-03–2026-08-10 Rewritten from `git log --no-merges` grouped by type and scope, not polished from the generator draft. The generator's picks were a 10-item list that put two internal tooling commits in Highlights, split the router story across three bullets that supersede each other, and covered none of the week's 70 fixes or 76 docs commits. This cites 109 commits and 45 PRs. Verified at HEAD rather than from commit bodies: - The DE parallel router is default-on for every install. The canary entry is gone from canaryRegistry.ts and render.ts no longer gates it. Framed as a restoration, since the canary had cut fleet exposure to 0.13 percent. - Catalog meaning search is opt-in behind a 33 MB consented download, so it is not described as default behaviour. - The Reference Project landed and was removed inside the same window, so it is reported as both rather than announced as new. - 30 Days really does carry thirty distinct films. Examples now has 23, not the 19 its commit subject claims, so no count is published for it. Every SHA is expanded from a token by a script that refuses to write unless it resolves to 40 chars, prefix-matches, is an ancestor of origin/main and falls inside the window. No SHA was hand-typed. * docs(changelog): correct release range to v0.7.90 in weekly digest v0.7.91 was never published. Its artifacts were folded into v0.7.92, so the sequence goes v0.7.90 -> v0.7.92 and the window's first release is v0.7.90. Still fifteen releases. * docs(changelog): embed the weekly video in the Aug 3-10 digest 45.1s square film built from the changelog-video skill. Uploaded to static.heygen.ai and the CloudFront path invalidated. |
||
|
|
c86d4013f5 |
Revert "feat(registry): the video-primitive moves, documented and customisable (#3090)" (#3162)
This reverts commit
|
||
|
|
3b53bfd2f7 |
feat(registry): the video-primitive moves, documented and customisable (#3090)
* feat(registry): add the video-primitive moves, and rebuild the catalog 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.
* docs(catalog): publish the primitive and component pages
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.
* docs: list the primitive and component pages in the site navigation
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.
* fix(registry): restore what the port took from main's components
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.
* docs(catalog): play the real composition, and show what can be changed
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.
* docs(catalog): put the code on the page
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.
* feat(registry): give 55 primitives variables that actually do something
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.
* feat(registry): variables for 16 more primitives, and stop the snippet 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.
* feat(registry): variables for 21 more primitives
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.
* feat(registry): variables for 4 more primitives, and make manifests agree 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.
* feat(registry): variables for 4 more primitives
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.
* refactor(registry): drop the UI primitives, this is a video catalog
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.
* feat(registry): every motion and transition primitive is now customisable
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.
* feat(registry): raise the catalog quality bar, and add eight primitives
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.
* fix(registry): let the split-flap board finish flipping on screen
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.
* fix(registry): keep the thread-message-stack payload parseable
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.
* fix(registry): close the apostrophe that truncated a variables declaration
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.
* fix(registry): make the review findings real fixes
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.
* fix(registry): stop shipping a stale copy of the catalog-search work
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.
* refactor(scripts): split the page builder into its numbered sections
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.
* fix(cli): repair what the cross-branch file take broke
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.
* fix(ci): resolve core from source and take only item directories
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.
* refactor(registry): load gsap from the cdn like every other item
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.
* feat(registry): swap in the detailed device models
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.
* chore(registry): rebase onto the merged catalog search
This branch carried its own copy of the catalog-search work so that merging it in either order could not regress the other. That copy is now the older one: main has the consent fix, the contributor path for someone without the embedding model, the restored download test and the corrected gate message. Every file main owns is taken from main, and the three duplicated CLI commits are dropped rather than replayed.
Regenerated afterwards, because the registry it describes has changed: registry.json, the vector index, the catalog pages and the skills manifest.
* fix(scripts): stop the rebase reverting the preview pipeline
Resolving the rebase in favour of this branch took three files whose newer versions had already merged, so the branch quietly reverted them.
generate-catalog-previews.ts lost encodeForWeb, which exists because publishing masters directly put 25 Mbps files on the docs CDN and one 20-second preview was 60 MB. It also lost the ffmpeg transcode, so a jpeg capture was being written straight to a .png path while the comment above still said it transcoded, and it lost openOpaqueCapture, re-creating the second copy of a capture setup that was extracted precisely to stop there being two. This PR renders previews for over 200 items, so all three shipped at scale.
scripts/tsconfig.json regained exclusions that hole the gate, and generate-template-previews.ts went back to importing the producer by package name, which is the CI failure that import was changed to fix.
All three are taken from main. Also drops an alignScreenToMinusZ copied into the laptop block, which has no front and back to compare and never called it, and makes the preview copy lstat so a symlinked directory cannot send it outside the repo.
* fix(registry): clear the five items this PR added that the linter rejects
The registry linter is not wired into CI, so five items this PR adds were shipping with real render defects nobody would have seen fail.
caption-camera-follow and grade-split-reveal styled their root by its own class. Sub-composition CSS is scoped to [data-composition-id=...] <selector>, so a selector whose leftmost part is the root class becomes a descendant selector and stops matching the root: the scene renders unstyled at render time while looking correct in every static check and in preview. Both now key off the attribute the scoper already adds.
logo-brand-close tweened letterSpacing, which the browser snaps to integer device pixels, so the ease-out tail stutters under seek-by-frame capture. It is a scaleX now.
terminal-simulator named SFMono-Regular, which the renderer cannot resolve, so the text silently fell back.
oversized-cursor was a false positive: the rule scans raw source for head tags and a literal one written inside a JS comment paired with the real closing tag. Confirmed against a render, nothing leaks into frame, so the comment says head element rather than the tag.
Also stops generate-registry-items.ts dropping catalogArtifact.revision. build-local-vectors.ts stamps it so the CLI and the coverage gate can tell whether the published vectors still describe this registry; regenerating the item list erased it, and the gate then failed until someone rebuilt the index.
41 items still fail the linter, every one of them pre-existing on main.
|
||
|
|
bd1c1af291 | chore: release v0.7.105 (#3152) | ||
|
|
68205dbbc1 |
feat(cli): search the catalog by meaning, on this machine (#3089)
* feat(cli): search the catalog by meaning, in three named tiers 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. * fix(cli): make this branch pass the repo's own gates 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. * feat(cli): ship the local search tiers only, drop the hosted one 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. * fix(cli): count only moves this registry cannot install as dropped The dropped count was computed against the list left after the user's own --type and --tag filters, so every move the user excluded was reported as one the registry is missing. Filtering made the number go up: the same query reported 277 unfiltered and 302 with --type block. The count exists so a caller can tell "nothing matched your words" apart from "the ranker suggested things this project cannot install". Conflating it with user filtering destroys exactly that signal, and worse, genuine index skew and a self-inflicted filter printed a byte-identical line with opposite remedies -- one means refresh the shelf, the other means drop a flag, and refreshing does nothing. Now counted against the registry rather than the filtered view. The manifest is already fetched whole and narrowed in memory, so keeping the unnarrowed name set costs no extra request, and item loading still runs only on the filtered subset. Verified against ground truth rather than by eye: the vector artifact holds 411 names, the registry holds 168 installable items, and 134 of those names exist in both, so 277 are genuinely uninstallable. The count now reads 277 unfiltered, 277 under --type block, 277 under --type component and 277 under --tag, and the skew it reports is real -- the artifact predates dropping the UI primitives and still ranks moves that are no longer on the shelf. Reported by Vance Ingalls, who also noted this closes an item the status doc listed as unverified. Two earlier sweeps could not make the count fire because neither combined a filter with a query. Tests pin the three cases: a genuinely absent name counts, a filter-excluded name does not, and a fully installable ranking reports zero. * fix(cli): tell the user when meaning search cannot see the catalog The on-device index was fetched once and never revalidated: the only freshness check was two existsSync calls. A move added after that fetch was invisible to meaning search permanently, not down-ranked but absent from the candidate set. The registry manifest on the same command carries a 24h TTL, so the two halves of one feature disagreed about staleness. The dropped count reported over-coverage only, names the index has that the registry lacks. Under-coverage was never computed, so the harmless direction was instrumented and the costly one was silent. Reproduced with an index truncated to 120 of 168 moves: dropped read 0, perfect health, while 48 moves were unreachable. Counts under-coverage from the name list the artifact already carries, so no extra request. Warns only when non-zero, and names the remedy. The remedy had to be made true: --on-device could not refresh a stale index because hasLocalVectors short-circuited the fetch. That flag now refetches when the index is absent or no longer covering. Two defects the reproduction surfaced. A failed refresh reported the tier unavailable while the old vectors were still on disk and still ranking. And the fetch wrote its two files one at a time, so failing between them paired a new name list with an old matrix, a hard load error rather than stale data. It now writes both or neither, which matters more once refresh runs on staleness. top_score returns, scoped to the on-device tier and set to the score of the best result actually shown rather than the ranking head, which can describe a row the caller never received. Also: scripts/ is now typechecked. It never was, which is how a build script that crashes after the paid embedding call, and two scripts whose imports do not resolve at all, went unnoticed. 43 errors fixed, no suppressions. And the docs stop describing a --smart hosted tier that was deleted, an item that does not exist, and a registry refresh that cannot fix a stale vector index. * ci: fail when the search index stops covering the registry The catalog vector artifact is regenerated by hand. Nothing in CI, in package.json or in a hook rebuilds it, because embedding needs the 32 MB model. So adding a registry item silently makes it invisible to meaning search until someone remembers to regenerate. The failure is asymmetric, which is what makes it easy to miss. Removing an item is self-healing: the ranker still scores the dead vector, then filters the name before display, so a user is never offered something they cannot install. Adding one is not: the item is absent from the candidate set entirely, not ranked low. Comparing the two name lists needs neither the model nor a network call, so the gate runs in seconds. CI checks rather than fixes, for the same reason it cannot regenerate. Scoped to blocks and components. Examples are starter projects a user scaffolds, never something catalog ranks, and the artifact carries no vector for them, so demanding one would keep this gate permanently red and it would be ignored within a week. Verified in both directions rather than assumed: adding an unindexed item exits 1 and names it, restoring the registry exits 0. * fix(catalog): rebuild the search index from the registry build-local-vectors.ts read registry/catalog-artifact/catalog.json, a file no script in this repo writes and which is not committed, so the documented regeneration command failed on a missing path. That is why the index could drift from the registry with nothing to run to fix it. It now reads registry/blocks/* and registry/components/* through catalogFromRegistry, the existing helper that already produced the right shape but had no caller. Rebuilding reproduces the shipped 168 rows byte for byte. A lefthook catalog-index command regenerates and re-stages both artifact files whenever a staged registry-item.json changes, mirroring the skills-manifest pattern, so adding or removing an item keeps the index in sync without anyone remembering to. Verified end to end: staging a new item took the artifact 168 to 169 rows and staged it in 0.80s. * fix(cli): refuse a half-downloaded vector cache The two artifact files have to agree on how many rows there are, and until now nothing checked that before writing them. A truncated or wrong-model response landed in the cache and only failed at load, on every later search, until someone cleared it by hand. The pair is now checked first and refused as a unit, and the cache is created 0o700 with 0o600 files rather than inheriting the umask of a directory the caller may have pointed anywhere. Also lifts the capture setup the two preview generators had drifted into sharing into scripts/preview-capture.ts, and splits the vector builders batching and packing out of main. Both were findings the audit attributed to this branch. * fix(cli): keep the catalog vitest run with the tests it runs Restacking took the base package.json wholesale, which dropped the vitest dependency and the scripts/catalog run this PR adds. Both belong here rather than under it. * fix(cli): stop the declined model download from happening anyway Answering no to the on-device download offer recorded no and warned, then carried on. The guard below it is localModelConsent() !== false, which the decline had just made false, so it was skipped rather than taken: control reached recordLocalModelConsent(true), overwrote the answer with yes, and fetched the 32 MB model the user had refused. Next run it never asked again. No test could catch it. The stub pinned localModelStatus to ready, so the prompt never fired, and recordLocalModelConsent was a no-op that recorded nothing. Two tests now cover the offer, and they need three things the old stubs did not model: the run has to look like a terminal, because off one the command treats --on-device as the consent and never asks; the ONNX probe has to answer true, or an accepted offer returns at the runtime guard before it can download; and the status has to follow the recorded answer, or the second offer later in the run fires as well. Removing the return makes the decline test fail. * fix(catalog): let someone without the model still add a component The pre-commit hook rebuilds the search index, and rebuilding needs the 32 MB embedding model. An outside contributor adding a registry item does not have it, so their commit died inside the ONNX loader on an ENOENT naming a path they never set, and the CI gate then told them to run the command that had just crashed. The model is an opt-in for search, not a build dependency, so nobody is charged for it to contribute. The builder checks first and explains itself, exiting 3 for cannot as distinct from 1 for failed. The hook treats 3 as skip and lets the commit through. The gate now names both paths: regenerate if you have the model, leave it if you do not and a maintainer will. Verified both ways: with no model the builder explains and the hook exits 0; with the model it still regenerates byte-identically. * docs: say that anyone can add a registry item, and stop hand-editing a generated file Two defects, one of them the reason 64 stale entries survived in registry.json. The checklist told contributors to add their item to registry/registry.json. That file is generated from the item directories, so an entry added by hand survives until the next regeneration and then vanishes, and one left behind for a directory that no longer exists is worse: hyperframes add resolves the name and then fails on missing files. Both CONTRIBUTING.md and the agent-facing skill reference now run the generator instead. Nothing said contribution was maintainer-only, but nothing said it was not either, and two steps do need assets an outside contributor has no reason to install. Those are now named in a table with what happens if you do not have them, matching how the preview image was already handled. The search index is the new one: the model behind it is a 32 MB opt-in for search, not a build dependency. * fix(cli): harden on-device catalog search * fix(cli): refresh stale catalog vectors * test: create catalog vector temp dirs securely |
||
|
|
c96b30c717 | chore: release v0.7.104 (#3147) | ||
|
|
adb13ce125 | chore: release v0.7.103 (#3127) | ||
|
|
b1f7d8881b | chore: release v0.7.102 (#3119) | ||
|
|
eba96feda7 | chore: release v0.7.101 | ||
|
|
d9b00e57eb |
chore: release v0.7.100 (#3093)
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
0bda6b55b8 |
feat(cli): track which registry items add installs (#3099)
* feat(cli): track which registry items `add` installs `cli_command` records that `add` ran and nothing about what it installed, and the registry is served from raw.githubusercontent.com, which gives no per-item counter either — so there is no way to tell which block or component people actually pull, and no way to know what is worth building more of. Emit one `registry_item_added` event per item written into a project, from `runAdd` after the install succeeds. That is the single choke point: the bulk `add <tag>` path re-enters it per item, and a failed or compatibility-refused install throws before it, so a refused install is never counted as a download. `requested` separates the item the user named from the transitive `registryDependencies` pulled in behind it; without it a popular dependency outranks everything that depends on it. Item names are public registry identifiers, never user content or project data, and the event goes through `trackEvent` — an install that opted out via `hyperframes telemetry disable`, `HYPERFRAMES_NO_TELEMETRY` or `DO_NOT_TRACK` sends nothing. * test(cli): cover `add` telemetry end to end against the built CLI The unit tests assert the emit seam and nothing past it. `shouldTrack()` short-circuits whenever `isDevMode()` is true, and that is true for any `.ts` entry, so under vitest a real event and no event are indistinguishable and the transport is never exercised at all. Drive the built CLI instead and assert on the HTTP body it actually produces: one event per installed item, the dependency reported with `requested: false`, an opted-out install sending no request at all (not merely one without this event), and a refused install counting nothing. Two fixtures, because neither case is reachable through the real registry. The registry origin is a first-class project setting, so a local one supplies the `registryDependencies` edge that no shipped catalog item declares today; and `globalThis.fetch` is wrapped to capture the batch rather than send it. The faked 200 is load-bearing: only a failed flush leaves events queued, and only a non-empty queue spawns the detached `flushSync` child that would bypass the hook and reach production analytics. Verified the check can fail — forcing `requested: true` for every item turns it red on exactly the dependency assertion. |
||
|
|
9aa90f6e3e | chore: release v0.7.99 | ||
|
|
6114749d8e | chore: release v0.7.98 | ||
|
|
c4b41072c7 | chore: release v0.7.97 | ||
|
|
066af73c13 |
Merge pull request #3075 from heygen-com/release/v0.7.96
chore: release v0.7.96 |
||
|
|
9769ba2c7b |
feat(sdk): expose a paint query for transparent-composition hit-testing (#3070)
* feat(sdk): expose a paint query for transparent-composition hit-testing A host layering a transparent composition over other content has to know whether a point carries ink before it decides to swallow a click. The adapter only answered "what element is here", so AI Studio wrote its own answer and could not reach the per-pixel alpha the adapter already samples for <img>. Add PreviewAdapter.paintsAt, plus the pieces it is built from on a new ./adapters/iframe subpath so a host with different hit-test policy can compose its own walk. The walk is geometric rather than elementsFromPoint-based: that stack omits pointer-events:none nodes, and a decorative overlay carrying it still paints, so a z-stack query would report no ink over visible artwork — the direction that makes a composition vanish from under the cursor. fullBleedFraction is an option rather than a constant because "a layer covering the whole frame is background, not artwork" is host policy, not a fact about the composition. * fix(sdk): scope the full-bleed frame to the root under the point compositionFrameArea took the smallest [data-composition-id] in the whole document, so an unrelated sub-composition sized the reference frame for points nowhere near it: a 300x300 badge in a corner made every mid-size painter in a 1920x1080 outer frame read as full-bleed, and the composition went click-through under artwork the user can plainly see. That is the direction the fail-safe exists to avoid, and the docs already described the intended behaviour — the innermost root CONTAINING the point. Also read the alpha channel instead of matching known transparent spellings. Only the `transparent` keyword computes to rgba(0, 0, 0, 0); a faded-out white stays rgba(255, 255, 255, 0), which the set counted as painted. That erred toward absorbing clicks rather than losing them, so it was a false positive rather than a hazard, but it is wrong. Both are pinned by tests that fail when the fix is reverted. * fix(sdk): stop the paint query answering "no ink" over visible artwork Four cases where the walk landed on the wrong side of its own fail-safe. The full-bleed veto tested the winner's border-box area even when the alpha sampler had just read an opaque pixel there, so a full-frame transparent PNG or SVG overlay — the case this feature exists for — reported background over visibly opaque artwork, with no fullBleedFraction that worked. Ink now carries how it was established, and a measured pixel is never vetoed. An image whose pixels could NOT be read stays inferred, so a tainted CDN overlay still yields to the veto rather than absorbing every click. Composition roots were excluded from candidacy outright, so a root carrying a background answered false even at fraction 0, where the docs promise every painting box counts. Roots are candidates now; the veto discounts them without a special case, since a root's box is the frame. An <img> with a clear pixel early-returned past its own background, padding plate and border, which any other element would have counted. A same-origin iframe mid-navigation exposes a readable but empty document, so the !doc guard never fired and a loading composition answered a confident "no ink" — the exact failure the null convention exists to prevent. Also: the sort comparator's epsilon tie was intransitive, leaving the smallest-first guarantee (and the lazy single-sample property that rides on it) engine-dependent; the guide's pass-through recipe called a function that does not exist and hand-waved the coordinate mapping that makes it correct; and the reference now states the under-counts alongside the over-counts, the walk's blindness to runtime-mounted content, and compositionPaintsAt's preconditions. Each fix is pinned by a test that fails when the fix is reverted. * refactor(sdk)!: invert the paint query to isProvablyEmptyAt paintsAt handed callers three falsy bottom values with opposite safe readings: false meant "no ink, pass the click through", null meant "not knowable, treat as painted", and undefined from an adapter without the method also meant painted. The idiomatic `if (!preview.paintsAt?.(x, y)) passThrough()` therefore did the dangerous thing for two of the three, and the convention needed defending in the interface docstring, the reference and the guide — plus a dedicated comment and a pinning test on the headless adapter to stop null regressing to false. Inverting the polarity collapses the tri-state to a plain boolean and makes the safe reading structural: true only when the composition was readable and nothing painted there, so ink, an unreadable or still-loading document, and a missing implementation all land on "keep the composition clickable". The prose stays as rationale, but nothing depends on a reader remembering it. PaintsAtOptions becomes PaintQueryOptions, since it now describes the walk that both the adapter method and the exported compositionPaintsAt share rather than one method's arguments. compositionPaintsAt keeps its ink-positive name: it answers the other question, and its docstring points callers who need the fail-safe contract at the adapter. Nothing is released yet, so no consumer is on the old name. |
||
|
|
026e6941ac | chore: release v0.7.96 | ||
|
|
fc0298de4a | chore: release v0.7.95 (#3068) | ||
|
|
27d113e56c | docs: add user interview booking links (#3059) | ||
|
|
fed2baf278 |
docs: correct composition offset rationale; pin nav fix; label-in-name
- html-schema: the previous clause claimed a composition host's data-media-start is never read. It is: readElementPlaybackStart (media.ts:16) resolves data-playback-start ?? data-media-start and timeline.ts calls it on composition clips. Rewrote to the accurate reason both reviewers gave — composition hosts are only inspected by the playback-start-first readers, media-start works as a fallback, but playback-start is what Studio writes/normalises to. - motion test: pin the HoverVideo click-suppression (preventDefault + stopPropagation). Removing it left the gate 12/12; now it fails. This is the bug that escaped static review and only surfaced by driving the live preview. - replica-compare: fold the visible 'Sound off/on' text into the aria-label so the accessible name contains it (WCAG 2.5.3, Rames). Round-5 findings from Magi and Rames. |
||
|
|
83db94e75e |
docs(examples): stop the sound control from following the card's link
Grid cards with a public source are wrapped in an <a>; the sound/play button lives inside it, so a click — mouse or keyboard — bubbled up and navigated to GitHub instead of toggling sound. preventDefault + stopPropagation on the control. Found by driving the live preview (self-review). |
||
|
|
f3d1d531d1 |
docs(examples): reset control state on offscreen release; precise a11y labels
- replica-compare: the offscreen teardown now resets muted (element + React state), matching HoverVideo — a pair unmuted before it scrolled away no longer returns reading 'Sound on' over a paused, sourceless pair (Magi blocker). - Both controls' aria-labels now follow the mode: under reduced motion the button plays/pauses the whole comparison, and when a preview is already autoplaying muted the action is 'unmute', not 'play with sound' (self-review + Rames). - Hardened the motion-suite assertion to scope 'startBoth' to toggleSound's body (a defined-but-unused helper no longer satisfies it) and pin the offscreen muted-reset transition (Rames mutation-test gap). - html-schema: 'hyperframes validate' inspects <audio> only; note that no media-start-only reader inspects a composition host, so the kind rule strands nobody (Rames). |
||
|
|
c49fea2e0c |
docs: complete the media-start layer map + fix the third page
Per Rames: add the engine audio mixer (audioMixer.ts:350 -> ffmpeg -ss) to the media-start-only readers — it's the live path that makes a lone data-playback-start a shipped output bug (trimmed picture over untrimmed audio). 'Set one' isn't enough; give the element-kind rule: <video>/<audio> use data-media-start, nested composition uses data-playback-start (Studio writes it; it's the child-timeline offset). Also fix editing-existing-videos.mdx, which offered both names as interchangeable for front trim of a clip — the exact kind where they aren't. |
||
|
|
630c8900f5 |
docs(examples): reduced-motion play starts both films; precise media-start layers
- replica-compare: the voluntary control now starts and pauses BOTH films (not just the reference), and the replica-sync effect attaches in view regardless of the preference, so a reduced-motion visitor who presses play sees the whole synchronized pair. Added a focused source-level assertion to the motion-check suite (the repo has no React runtime harness for snippets). - html-schema: describe each layer precisely instead of grouping the CLI — timing compiler, HTML parser, producer audio, and 'hyperframes validate' read only data-media-start; runtime, Studio and 'hyperframes snapshot' read data-playback-start first (Studio also writes it). Round-3 findings from Magi. |
||
|
|
b541245f1a |
docs(studio): note the third arrow-key meaning after merging Timeline navigation
The merged '## Timeline navigation' section documents a third arrow-key behaviour; the 'selected area decides' paragraph read as exhaustive with only two. Fold in the timeline-focus case and link the section. Coherence note from Rames. |
||
|
|
c9fd7465c9 |
docs(examples): allow voluntary playback under reduced motion; a11y parity
Both snippet players now keep a source assigned whenever in view (preload='none'
so nothing downloads until asked), gating only autoplay on reduced motion — so a
reduced-motion visitor can press play/unmute instead of being left with an inert
poster. HoverVideo renders a control on hasAudio={false} cards too (a play/pause
toggle), and ReplicaCompare no longer calls play() on a source it just removed.
Gave ReplicaCompare's button the same aria-pressed + focus-visible as HoverVideo.
Round-2 finding from Magi (#1) and Rames.
|
||
|
|
8e658bd6c9 |
docs(sdk): fix dispatch/can contract, second stale GSAP site, media-start layers
- edit-operations: dispatch() does not consult can() (session.ts:608 -> applyOp with no validation; a no-timeline addGsapTween/addLabel is a no-op but a missing target still writes via selector fallback). Use Rames's wording: call can() first and skip on failure — no false 'applies nothing' guarantee. - timing-and-animation: the second E_NO_GSAP_TIMELINE site — gated setGsapTween on an error it cannot return and called shipped parser code 'a later phase'. Rewrote to gate addGsapTween (which can return it); dropped the stale can() comment in types.ts:595. - html-schema: data-playback-start is read by runtime, Studio and CLI (Studio also writes it, timelineEditingHelpers.ts); only the compile path is media-start only. Document the layered precedence instead of calling it runtime-only. Round-2 findings from Magi (#2/#3/#4) and Rames. |
||
|
|
aa49f7d924 | Merge remote-tracking branch 'origin/main' into docs/pages-show-not-tell | ||
|
|
aa077e000d |
docs(examples): intersection-gate the recreate pair too
ReplicaCompare now assigns/decodes its reference+replica only near the viewport and releases both (pause + removeAttribute + load) on exit, closing the last eager-load surface from Magi's #5. Sync + reduced-motion guard preserved. |
||
|
|
0467bc8129 |
docs(examples): make hover-audio accessible, lazy-loaded, and guard-complete
HoverVideo now: (1) exposes a focusable button that toggles/plays sound by
keyboard, touch and pointer — hover is an enhancement, not the only path;
(2) gates the source behind an IntersectionObserver so offscreen cards no longer
download/decode, releasing the buffer on exit; (3) still blocks autoplay under
reduced motion while allowing voluntary playback via the button. Routed all 30
Thirty-Days cards through it (hasAudio={false} for the 12 silent ones) so no raw
<video autoPlay> bypasses the motion guard. Flagged by Magi (P1 #4/#5/#6).
|
||
|
|
0f6259d461 |
docs: correct three source-contradiction findings from review
- edit-operations: E_NO_GSAP_TIMELINE is raised only by addGsapTween and addLabel (mutate.ts:1747), not the set/remove tween ops; drop the false 'dispatch() still applies structurally' claim (mutate.gsap.test.ts verifies zero patches); add the requirement to the Labels section where addLabel lives. - html-schema: data-media-start is read by parser + timing compiler + runtime; data-playback-start is a runtime-only alias, so a lone playback-start doesn't shift trim/split. Document the surfaces instead of claiming one universal value. - testing-local-changes: bun unlink cleanup used a relative cd that resolves under the video project; use an absolute checkout path. Flagged by Magi (P1 #2/#3, P2 #7) and Rames. |
||
|
|
6ff6601dd0 |
fix(catalog): restore the required Related topics section on generated pages
My generator rebuild replaced the '## Related topics' section (still required by docs/AGENTS.md) with the provenance footer, dropping it from all 168 generated Catalog pages. Emit it again as the final section so pages end with it, and stop carriedSectionsFrom() breaking at the footer marker so a human section appended below the generated tail survives a regeneration. Adds a per-page regeneration assertion so a future drop fails CI. Flagged by Magi (P1) and Rames. |