From 254fe4bd8a3f00c1e70ac5e38a1bce25060f7590 Mon Sep 17 00:00:00 2001 From: WaterrrForever Date: Tue, 1 Sep 2026 23:41:11 +0800 Subject: [PATCH] feat(registry): say what a promoted template's slots mean, not where they sit (#3562) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(registry): say what a promoted template's slots mean, not where they sit Every declaration described its slot by position — appName was "small credit in the corner of every banner" — and every editing contract described the supplied brand only by what it may not touch, chatgpt-exchange going as far as "only the subject discussed inside the conversation". An agent filling those slots has nothing telling it that the video is an advertisement for that brand, and remixes shipped with an invented brand instead of the real one. Adds `portrays` to the 35 identity-bearing variables across seven templates: what the slot stands for, as a list, kept orthogonal to `role` (which says which aspect of the composition a knob affects, not what a value means to a viewer). Rewrites those variables' descriptions to state meaning, and gives every TEMPLATE.md the affirmative sentence it was missing. notes-reveal now records honestly that it declares no identity slot at all, which is why a remix of it cannot carry a brand. `portrays` is optional metadata: rendering ignores it, the promoted contract still projects {id, type, role}, and packages that predate it keep working. Variable ids, types, roles, labels, and defaults are untouched, so installs and renders see the same contract. * feat(notes-reveal): give the template somewhere to say whose ad it is This was the one promoted template that declared no identity slot at all, and the previous commit could only record that honestly. A remix of it could not name the advertiser, could not carry its domain, and had nowhere to place its mark — so the ad it produced was unattributable no matter how good the copy was. The closing card ends at y=1708, leaving 212px of dotted scene. A sign-off strip lives there: `brandLogo` (portrays subject_logo) above `brandDomain` (portrays subject_domain), in the card's own plum and Courier Prime. Putting it outside the card means the hand-lettered headline and the checklist keep the geometry they were designed with — verified by `hyperframes check`, which reports the same 0 errors and the same warning counts as origin/main, with one more contrast check (367/367 AA). No name slot: this template has no text slot that can hold a bare brand name. `cardTop`'s marker line continues into the fixed words "OF ONE FILE", and a name set beside a wordmark reads twice — rendering it proved that before this was committed. The mark plus the domain carry the attribution, which is what the other end-card templates do. Rendered to check rather than assumed: the packaged default reads as the HyperFrames wordmark over hyperframes.heygen.com, and a resolved symbol mark (Notion's cube over notion.com) sits correctly in the same strip. * fix(notes-reveal): make the note the ad, and stop the headline leaving the card Three defects, all visible in the reported gamma remix. The seven body lines carried their characters as pre-split `` elements so the reveal animation could address them by id. That made the note permanently unwritable: 46% of the 24.9s runtime, and the only place the note says anything, was fixed copy about making videos with HyperFrames -- shipped inside an advertisement for a Dutch DIY chain. The spans are now built at runtime from seven declared variables, keeping the same ids, so the animation is untouched and the copy is the brand's. `cardMid` and `cardBottom` ("OF", "ONE FILE") were literals with no variable at all, so a remix could only ever change the first of the card's three lines. Every text slot sat at a fixed left edge on one line, guarded only by a character cap -- the wrong unit. `cardTop` allowed 12 characters; "THE POWER" (9) fits and "CREATE DECKS" (12) does not, which is why both reported gamma remixes shipped with the closing headline over the edge of the card. Slots are now fitted to their box at render time, shrinking type only as far as needed and never past the designed size. The card headlines also carried `data-layout-allow-overflow`, switching off the one check that would have caught it; those waivers are gone. The note's title and body keep theirs, because `#note-body` is translated across the whole scene and leaves the canvas by design. Timing is preserved rather than regenerated: each body line is revealed inside the frame window the packaged rhythm already used, so `JUMPS` and the hand-tuned `SCROLLS` curves stay valid and the composition is still 24.867s for any length of copy. A line whose length matches the packaged copy keeps its rhythm frame for frame. Verified: rendered with the packaged defaults, SSIM 1.000000 across all 746 frames against the previous template -- the design is bit-identical. Rendered with the gamma values that overflowed, and with a stress set of long and one-word values: both stay inside the card and the note, both still 24.867s. `hyperframes check` passes, with layout warnings down from 28 to 8 and no new issue class. Note that `check` takes no variable values, so it guards the template's defaults and the fit is what guards a remix. * fix(notes-reveal): make the fit floor and the character caps agree A cap that admits a string still too wide at the floor font size puts the text back outside its box -- the exact bug the fit was added for. With the floor at 0.6 and the title cap at 40, a 40-character title still overflowed `#note-body` by 30px, because "Things nobody told me" is 21 characters and 900px at 88px, so 42.9px per character, and 40 characters need to reach about 0.45 of the design size to fit. Floor to 0.45 and the caps in: titles 34, body lines 52, cardTop and cardBottom 16. Every cap is now satisfiable with room to spare, and the common case stays far from the floor. Found by probing all eight promoted templates with values about 1.8x the length of their packaged defaults, which is the length a real remix produces; short sentinel values only ever prove a slot is wired. Defaults are unaffected: SSIM 1.000000 across all 746 frames, still 24.867s, `hyperframes check` still passes. * fix(exchange): scroll the composer to follow the caret `prompt` is an editable slot in both templates, but `.comp-text` is a fixed-width single line with `white-space: pre` inside a composer that clips, and the packaged prompt already reaches the mic -- it has no headroom at all. A longer prompt therefore slid under the mic and the send button and lost its tail, with the caret hidden behind them: the remix read as having typed into nowhere, cut mid-word. Both now scroll the typed text left to keep the caret in view, the way a real single-line input does, on the very per-character times the reveal already uses. The limit is measured to the opaque round button rather than the mic, because the packaged prompt's caret already sits 2px past the mic's left edge in chatgpt-exchange -- measuring to the mic would scroll the packaged composition. Shrink-to-fit was the other option and it is worse: with no headroom it shrinks the packaged prompt too, and it still cannot absorb a prompt twice as long without unreadable type. It was tried, measured, and dropped. A prompt that fits emits no keyframes and the element is never handed to GSAP at all -- even a zero translate stamps a transform, which changes how the text rasterizes. claude-exchange puts this inside its existing `build()`, which already re-runs on `document.fonts.ready`, so it measures the final font. chatgpt-exchange builds once, so its scroll keyframes are kept in a list and rebuilt on the same event. Verified on the packaged defaults: claude-exchange SSIM 1.000000 across 1284 frames. chatgpt-exchange is not frame-deterministic -- rendering it twice unchanged gives 0.999959 with 249 frames under 0.9999, and before-vs-after gives 0.999967 with the same 249 -- so the change sits inside its own noise. Durations unchanged at 14.9s and 21.4s. `hyperframes check` reports byte-identical results to the unchanged templates. * fix(notes-reveal): fit the checklist cells instead of counting their characters The three checklist rows were the only text slots with no width fit, so their character cap was doing the job a measurement should: it was set to 8 because that was the widest the packaged strings needed, and a gamma.com remix delivered "DIRECT MEE" and rendered "DIRECT M". Fit the labels and values like every other text slot -- dividing the label's scaleX(0.86) back out of its column, since fitWidth measures the untransformed width -- and return the cap to a backstop at 18. "DIRECT MEE" needs no shrink at all: it ends at x=694 against a tick at x=715, so the truncation was never about room. Measured: the packaged defaults still render at their authored 44px and 50px, and all three values clear the tick. * fix(chatgpt-exchange): nest the caret scroll so its rebuild can be undone `tl.to()` returns the TIMELINE, not the tween it created, so `caretScrollTweens` held 49 references to the master timeline. The fonts-ready rebuild then called `.kill()` on the master timeline 49 times and re-added its keyframes on top of the ones it meant to replace. Measured with an overflowing prompt: master timeline detached from the global timeline (tl.parent -> null) scroll tweens on #cge-typed: 49 before the rebuild, 98 after 49 timestamps left with two competing tweens on the same property GSAP's `Animation.kill()` interrupts and detaches rather than killing children, so the composition kept rendering under an explicit seek -- which is why the packaged-defaults SSIM check passed and never saw this. It is still wrong: the timeline is off the ticker, and the stale keyframes were measured against the fallback font. Only an overflowing prompt reaches it, i.e. exactly the remix the scroll exists for. The packaged prompt emits no steps, so nothing is created and nothing is killed. Now one nested child timeline holds the steps: a real object that can be killed and replaced, and killing it cannot reach `tl`. Children sit at the same absolute times and the nest is added at 0, so the motion is unchanged. Verified: overflowing prompt holds at 49 scroll tweens across the rebuild with no duplicated timestamps and the timeline stays attached. Packaged defaults against the pre-feature baseline -- SSIM 0.999998 over 894 frames, 13 frames below 0.9999 against a 249-frame noise floor for this template, duration 14.9s unchanged. Found by Magi in review of #3562. Co-Authored-By: Claude Opus 5 (1M context) * feat(lint): catch a timeline's return value being treated as a tween Magi asked for a regression that makes the caret-scroll failure unable to return. A render fixture cannot be that guard, and neither can a unit test. Both were measured before choosing this: - The unit-test DOM has no layout. In happy-dom `getBoundingClientRect()` is {left: 0, width: 0} and offsetWidth/offsetLeft are 0, so the template's own guard computes limit = -8 and returns before collecting a single keyframe, whatever the prompt length. `document.fonts` does not exist there either, so the rebuild never runs. Both preconditions are unreachable -- the test would pass on the broken code and the fixed code alike. Rames flagged this; verified. - A render fixture would not fail either. Rendering the long prompt through the real pipeline on the broken vs fixed template gives SSIM 0.999994 over 894 frames with ZERO frames below 0.999. The renderer drives the composition by explicit seek, and the duplicated keyframes are added last so they win, which is exactly why `.kill()` on the master timeline stayed invisible. A golden mp4 would have cost a fixture and a shard and guarded nothing. What is testable is the root cause, and it is a static property: `tl.to()` returns the TIMELINE, `gsap.to()` returns a Tween, and the two read identically. So the guard is a lint rule, which needs no DOM, fires on the source, and covers every composition in the registry rather than this one template. The rule keys on the timeline handles `collectTimelineVarNames` already finds, which is what keeps `gsap.to()` and `Array.from()` out of it. Pushing a timeline return into an array is flagged outright -- that is the collect-to-discard shape. A bound return is flagged only once something tween-scoped is aimed at it, so a pointless-but-harmless capture stays quiet. Verified against the real files, not just synthetic ones: the pre-fix chatgpt-exchange raises exactly one error and names the offending line; the fixed file is clean. Across all 642 HTML files under registry/ and examples/ the rule finds nothing, and lint output on the eight promoted blocks is identical to origin/main error-for-error and warning-for-warning, so error severity costs no existing template. 540 lint tests pass, 6 of them new. Requested by Magi in review of #3562; unit-test DOM measurement by Rames Jusso. --- docs/catalog/blocks/ai-chat-reveal.mdx | 38 +- docs/catalog/blocks/chatgpt-exchange.mdx | 18 +- docs/catalog/blocks/claude-exchange.mdx | 12 +- docs/catalog/blocks/message-thread-reveal.mdx | 32 +- docs/catalog/blocks/notes-reveal.mdx | 102 +++- docs/catalog/blocks/notification-cascade.mdx | 20 +- docs/catalog/blocks/share-sheet-carousel.mdx | 20 +- docs/catalog/blocks/slack-notification-ad.mdx | 6 +- docs/concepts/variables.mdx | 33 ++ packages/lint/src/rules/gsap.test.ts | 100 ++++ packages/lint/src/rules/gsap.ts | 55 +++ registry/blocks/ai-chat-reveal/TEMPLATE.md | 2 +- .../blocks/ai-chat-reveal/ai-chat-reveal.html | 12 +- .../blocks/ai-chat-reveal/registry-item.json | 17 +- registry/blocks/chatgpt-exchange/TEMPLATE.md | 2 +- .../chatgpt-exchange/chatgpt-exchange.html | 87 +++- registry/blocks/claude-exchange/TEMPLATE.md | 2 +- .../claude-exchange/claude-exchange.html | 60 ++- .../blocks/message-thread-reveal/TEMPLATE.md | 2 +- .../message-thread-reveal.html | 10 +- .../message-thread-reveal/registry-item.json | 14 +- registry/blocks/notes-reveal/TEMPLATE.md | 15 +- .../assets/hyperframes-logo-black.svg | 31 ++ .../blocks/notes-reveal/notes-reveal.html | 459 +++++++++++++----- .../blocks/notes-reveal/registry-item.json | 97 +++- .../blocks/notification-cascade/TEMPLATE.md | 2 +- .../notification-cascade.html | 6 +- .../notification-cascade/registry-item.json | 9 +- .../blocks/share-sheet-carousel/TEMPLATE.md | 2 +- .../share-sheet-carousel/registry-item.json | 9 +- .../share-sheet-carousel.html | 6 +- .../blocks/slack-notification-ad/TEMPLATE.md | 2 +- .../slack-notification-ad.html | 6 +- registry/catalog-artifact/local-vectors.bin | Bin 572928 -> 572928 bytes 34 files changed, 1021 insertions(+), 267 deletions(-) create mode 100644 registry/blocks/notes-reveal/assets/hyperframes-logo-black.svg diff --git a/docs/catalog/blocks/ai-chat-reveal.mdx b/docs/catalog/blocks/ai-chat-reveal.mdx index a740ac3a0..2e2433a1f 100644 --- a/docs/catalog/blocks/ai-chat-reveal.mdx +++ b/docs/catalog/blocks/ai-chat-reveal.mdx @@ -10,7 +10,7 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx"; previewSrc="/public/catalog/blocks/ai-chat-reveal.json" compositionId="ai-chat-reveal" compositionSrc="compositions/ai-chat-reveal.html" - variables={[{"id":"botName","type":"string","role":"content","label":"Assistant name","description":"Title in the chat header.","default":"Assistant"},{"id":"userMessage","type":"string","role":"content","label":"User message","description":"The question typed on the keyboard and sent. Around 40 characters keeps the measured pacing.","default":"How do I turn my HTML into real video?"},{"id":"answer1","type":"string","role":"content","label":"Answer paragraph 1","description":"First streamed paragraph.","default":"You do not need an editor. HyperFrames renders the HTML you already write into deterministic, pixel-perfect video."},{"id":"answer2","type":"string","role":"content","label":"Answer paragraph 2","description":"Short second beat.","default":"It is markup, not magic."},{"id":"answer3","type":"string","role":"content","label":"Answer paragraph 3","description":"Lead-in to the list.","default":"What you get out of the box:"},{"id":"bullet1","type":"string","role":"content","label":"Bullet 1","description":"First list item.","default":"A catalog of motion primitives you install and own"},{"id":"bullet2","type":"string","role":"content","label":"Bullet 2","description":"Second list item.","default":"GSAP timelines that seek to any frame"},{"id":"bullet3","type":"string","role":"content","label":"Bullet 3","description":"Third list item.","default":"4K renders from a single CLI command"},{"id":"ecHeadline","type":"string","role":"content","label":"End-card headline","description":"Closing card headline. A | breaks the line.","default":"It's not magic.|It's HTML."},{"id":"ecSub","type":"string","role":"content","label":"End-card subline","description":"Supporting sentence on the closing card.","default":"Open-source video rendering — write HTML, ship pixel-perfect video."},{"id":"ecCta","type":"string","role":"content","label":"End-card CTA","description":"Label on the closing card button.","default":"Try HyperFrames"},{"id":"ecFooter","type":"string","role":"content","label":"End-card footer","description":"Letter-spaced line at the very bottom.","default":"HYPERFRAMES.HEYGEN.COM"},{"id":"brandLogo","type":"image","role":"content","label":"Brand logo","description":"Transparent logo in the closing-card header.","default":"assets/hyperframes-logo-white.svg"}]} + variables={[{"id":"botName","type":"string","role":"content","label":"Assistant name","description":"Name of the assistant answering in this mock chat. It belongs to the host product, never to the brand this video promotes.","portrays":["host_identity"],"default":"Assistant"},{"id":"userMessage","type":"string","role":"content","label":"User message","description":"The question typed on the keyboard and sent. Around 40 characters keeps the measured pacing.","default":"How do I turn my HTML into real video?"},{"id":"answer1","type":"string","role":"content","label":"Answer paragraph 1","description":"First streamed paragraph. It names the brand this video promotes and says what that brand does.","default":"You do not need an editor. HyperFrames renders the HTML you already write into deterministic, pixel-perfect video."},{"id":"answer2","type":"string","role":"content","label":"Answer paragraph 2","description":"Short second beat.","default":"It is markup, not magic."},{"id":"answer3","type":"string","role":"content","label":"Answer paragraph 3","description":"Lead-in to the list.","default":"What you get out of the box:"},{"id":"bullet1","type":"string","role":"content","label":"Bullet 1","description":"First list item.","default":"A catalog of motion primitives you install and own"},{"id":"bullet2","type":"string","role":"content","label":"Bullet 2","description":"Second list item.","default":"GSAP timelines that seek to any frame"},{"id":"bullet3","type":"string","role":"content","label":"Bullet 3","description":"Third list item.","default":"4K renders from a single CLI command"},{"id":"ecHeadline","type":"string","role":"content","label":"End-card headline","description":"Closing card headline. A | breaks the line.","default":"It's not magic.|It's HTML."},{"id":"ecSub","type":"string","role":"content","label":"End-card subline","description":"Positioning sentence of the brand this video promotes, on the closing card.","portrays":["subject_tagline"],"default":"Open-source video rendering — write HTML, ship pixel-perfect video."},{"id":"ecCta","type":"string","role":"content","label":"End-card CTA","description":"Closing-card button label, which names the brand this video promotes. Use the real name of that brand.","portrays":["subject_name"],"default":"Try HyperFrames"},{"id":"ecFooter","type":"string","role":"content","label":"End-card footer","description":"Domain of the brand this video promotes, letter-spaced at the very bottom. Use the real domain.","portrays":["subject_domain"],"default":"HYPERFRAMES.HEYGEN.COM"},{"id":"brandLogo","type":"image","role":"content","label":"Brand logo","description":"Transparent mark of the brand this video promotes, in the closing-card header.","portrays":["subject_logo"],"default":"assets/hyperframes-logo-white.svg"}]} > ```html ai-chat-reveal.html @@ -18,19 +18,19 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx"; @@ -1068,19 +1068,19 @@ want to change on the element: | Variable | Default | Accepts | What it does | | --- | --- | --- | --- | -| `botName` | `Assistant` | string | Title in the chat header. | +| `botName` | `Assistant` | string | Name of the assistant answering in this mock chat. It belongs to the host product, never to the brand this video promotes. | | `userMessage` | `How do I turn my HTML into real video?` | string | The question typed on the keyboard and sent. Around 40 characters keeps the measured pacing. | -| `answer1` | `You do not need an editor. HyperFrames renders the HTML you already write into deterministic, pixel-perfect video.` | string | First streamed paragraph. | +| `answer1` | `You do not need an editor. HyperFrames renders the HTML you already write into deterministic, pixel-perfect video.` | string | First streamed paragraph. It names the brand this video promotes and says what that brand does. | | `answer2` | `It is markup, not magic.` | string | Short second beat. | | `answer3` | `What you get out of the box:` | string | Lead-in to the list. | | `bullet1` | `A catalog of motion primitives you install and own` | string | First list item. | | `bullet2` | `GSAP timelines that seek to any frame` | string | Second list item. | | `bullet3` | `4K renders from a single CLI command` | string | Third list item. | | `ecHeadline` | `It's not magic.|It's HTML.` | string | Closing card headline. A | breaks the line. | -| `ecSub` | `Open-source video rendering — write HTML, ship pixel-perfect video.` | string | Supporting sentence on the closing card. | -| `ecCta` | `Try HyperFrames` | string | Label on the closing card button. | -| `ecFooter` | `HYPERFRAMES.HEYGEN.COM` | string | Letter-spaced line at the very bottom. | -| `brandLogo` | `assets/hyperframes-logo-white.svg` | image | Transparent logo in the closing-card header. | +| `ecSub` | `Open-source video rendering — write HTML, ship pixel-perfect video.` | string | Positioning sentence of the brand this video promotes, on the closing card. | +| `ecCta` | `Try HyperFrames` | string | Closing-card button label, which names the brand this video promotes. Use the real name of that brand. | +| `ecFooter` | `HYPERFRAMES.HEYGEN.COM` | string | Domain of the brand this video promotes, letter-spaced at the very bottom. Use the real domain. | +| `brandLogo` | `assets/hyperframes-logo-white.svg` | image | Transparent mark of the brand this video promotes, in the closing-card header. | Set them with `data-variable-values` on the element that mounts it. These are the defaults, so this behaves exactly like the preview above until you change one: @@ -1102,19 +1102,19 @@ defaults, so this behaves exactly like the preview above until you change one: diff --git a/docs/catalog/blocks/chatgpt-exchange.mdx b/docs/catalog/blocks/chatgpt-exchange.mdx index 4751a0d73..6fd1865e3 100644 --- a/docs/catalog/blocks/chatgpt-exchange.mdx +++ b/docs/catalog/blocks/chatgpt-exchange.mdx @@ -54,21 +54,21 @@ Move it in time with `data-start`. Put it on a different timeline row with { "id": "tableHeadTool", "type": "string", "role": "content", "label": "Table heading 2", "description": "Tool column heading.", "default": "Best tool" }, { "id": "tableHeadWhy", "type": "string", "role": "content", "label": "Table heading 3", "description": "Explanation column heading.", "default": "Why" }, { "id": "row1Use", "type": "string", "role": "content", "label": "Row 1 use case", "description": "First comparison use case.", "default": "Overall realism" }, - { "id": "row1Tool", "type": "string", "role": "content", "label": "Row 1 tool", "description": "First recommended product.", "default": "HeyGen" }, - { "id": "row1Why", "type": "string", "role": "content", "label": "Row 1 explanation", "description": "First recommendation explanation.", "default": "Most natural facial expressions, lip sync, gestures, voice cloning and localization. Benchmark for talking head videos." }, - { "id": "row1Chip", "type": "string", "role": "content", "label": "Row 1 source", "description": "First source-chip label.", "default": "Official A.I Ranking" }, + { "id": "row1Tool", "type": "string", "role": "content", "label": "Row 1 tool", "description": "Product in the recommended first row: the brand this video promotes, by its real name.", "portrays": ["subject_name", "recommended_position"], "default": "HeyGen" }, + { "id": "row1Why", "type": "string", "role": "content", "label": "Row 1 explanation", "description": "Why the recommended first-row product wins. Describe the promoted brand from what its own site claims.", "portrays": ["recommended_position"], "default": "Most natural facial expressions, lip sync, gestures, voice cloning and localization. Benchmark for talking head videos." }, + { "id": "row1Chip", "type": "string", "role": "content", "label": "Row 1 source", "description": "Source chip on the first row. It asserts where the ranking came from, so use a neutral label whenever the comparison is written for this remix.", "portrays": ["authority_badge"], "default": "Official A.I Ranking" }, { "id": "row2Use", "type": "string", "role": "content", "label": "Row 2 use case", "description": "Second comparison use case.", "default": "Enterprise/training" }, - { "id": "row2Tool", "type": "string", "role": "content", "label": "Row 2 tool", "description": "Second recommended product.", "default": "Synthesia" }, + { "id": "row2Tool", "type": "string", "role": "content", "label": "Row 2 tool", "description": "Product compared in the second row: a real product other than the brand this video promotes.", "portrays": ["competitor_name"], "default": "Synthesia" }, { "id": "row2Why", "type": "string", "role": "content", "label": "Row 2 explanation", "description": "Second recommendation explanation.", "default": "Better collaboration, SCORM, compliance, team workflows; less creator-focused." }, - { "id": "row2Chip", "type": "string", "role": "content", "label": "Row 2 source", "description": "Second source-chip label.", "default": "Official A.I Ranking" }, + { "id": "row2Chip", "type": "string", "role": "content", "label": "Row 2 source", "description": "Source chip on the second row. It asserts where the ranking came from, so use a neutral label whenever the comparison is written for this remix.", "portrays": ["authority_badge"], "default": "Official A.I Ranking" }, { "id": "row3Use", "type": "string", "role": "content", "label": "Row 3 use case", "description": "Third comparison use case.", "default": "Mobile UGC" }, - { "id": "row3Tool", "type": "string", "role": "content", "label": "Row 3 tool", "description": "Third recommended product.", "default": "Captions" }, + { "id": "row3Tool", "type": "string", "role": "content", "label": "Row 3 tool", "description": "Product compared in the third row: a real product other than the brand this video promotes.", "portrays": ["competitor_name"], "default": "Captions" }, { "id": "row3Why", "type": "string", "role": "content", "label": "Row 3 explanation", "description": "Third recommendation explanation.", "default": "Extremely fast mobile workflow and social editing. Great for Reels creators." }, - { "id": "row3Chip", "type": "string", "role": "content", "label": "Row 3 source", "description": "Third source-chip label.", "default": "Creator Stack" }, + { "id": "row3Chip", "type": "string", "role": "content", "label": "Row 3 source", "description": "Source chip on the third row. It asserts where the ranking came from, so use a neutral label whenever the comparison is written for this remix.", "portrays": ["authority_badge"], "default": "Creator Stack" }, { "id": "row4Use", "type": "string", "role": "content", "label": "Row 4 use case", "description": "Fourth comparison use case.", "default": "Real-time conversations" }, - { "id": "row4Tool", "type": "string", "role": "content", "label": "Row 4 tool", "description": "Fourth recommended product.", "default": "Tavus" }, + { "id": "row4Tool", "type": "string", "role": "content", "label": "Row 4 tool", "description": "Product compared in the fourth row: a real product other than the brand this video promotes.", "portrays": ["competitor_name"], "default": "Tavus" }, { "id": "row4Why", "type": "string", "role": "content", "label": "Row 4 explanation", "description": "Fourth recommendation explanation.", "default": "Interactive avatars that can hold live conversations." }, - { "id": "row4Chip", "type": "string", "role": "content", "label": "Row 4 source", "description": "Fourth source-chip label.", "default": "Creator Stack" } + { "id": "row4Chip", "type": "string", "role": "content", "label": "Row 4 source", "description": "Source chip on the fourth row. It asserts where the ranking came from, so use a neutral label whenever the comparison is written for this remix.", "portrays": ["authority_badge"], "default": "Creator Stack" } ]' > diff --git a/docs/catalog/blocks/claude-exchange.mdx b/docs/catalog/blocks/claude-exchange.mdx index 681bf2189..7f2cf229f 100644 --- a/docs/catalog/blocks/claude-exchange.mdx +++ b/docs/catalog/blocks/claude-exchange.mdx @@ -52,13 +52,13 @@ Move it in time with `data-start`. Put it on a different timeline row with { "id": "lead", "type": "string", "role": "content", "label": "Search lead", "description": "Sentence introducing the search step.", "default": "I'll search for the current state of this space since AI avatar tools move fast." }, { "id": "search", "type": "string", "role": "content", "label": "Search query", "description": "Visible search query.", "default": "best AI avatar video generator 2026" }, { "id": "answer1", "type": "string", "role": "content", "label": "Answer paragraph 1", "description": "First answer paragraph.", "default": "It depends on what you're making, but the field has consolidated fast and one platform now covers most of it." }, - { "id": "answer2", "type": "string", "role": "content", "label": "Answer paragraph 2", "description": "Second answer paragraph.", "default": "**HeyGen** is where most teams land. Independent testing, not just vendor blogs, puts **Avatar IV** highest for talking-head realism, with facial micro-expressions and gesture control that hold up at a full-screen crop {HeyGen}. It also has the deepest enterprise adoption of the group, including a large share of the Fortune 100, which matters if you need the same presenter on brand across a year of campaigns." }, + { "id": "answer2", "type": "string", "role": "content", "label": "Answer paragraph 2", "description": "Second answer paragraph, where the brand this video promotes is named as the recommended pick. Keep any evidence claim to what that brand publishes.", "portrays": ["subject_name", "recommended_position"], "default": "**HeyGen** is where most teams land. Independent testing, not just vendor blogs, puts **Avatar IV** highest for talking-head realism, with facial micro-expressions and gesture control that hold up at a full-screen crop {HeyGen}. It also has the deepest enterprise adoption of the group, including a large share of the Fortune 100, which matters if you need the same presenter on brand across a year of campaigns." }, { "id": "answer3", "type": "string", "role": "content", "label": "Answer paragraph 3", "description": "Lead-in to the use-case list.", "default": "By use case, the pieces shake out roughly like this:" }, - { "id": "answer4", "type": "string", "role": "content", "label": "Answer bullet 1", "description": "First use-case bullet.", "default": "**Marketing / hyper-realistic talking heads** → Avatar IV" }, - { "id": "answer5", "type": "string", "role": "content", "label": "Answer bullet 2", "description": "Second use-case bullet.", "default": "**Enterprise training** → Video Translate for every locale" }, - { "id": "answer6", "type": "string", "role": "content", "label": "Answer bullet 3", "description": "Third use-case bullet.", "default": "**UGC-style performance ads** → Instant Avatar from one selfie, then batch the variants" }, - { "id": "answer7", "type": "string", "role": "content", "label": "Answer bullet 4", "description": "Fourth use-case bullet.", "default": "**Real-time conversational / interactive** → Interactive Avatar" }, - { "id": "answer8", "type": "string", "role": "content", "label": "Answer bullet 5", "description": "Fifth use-case bullet.", "default": "**Custom digital twin from a selfie** → Instant Avatar in about five minutes" }, + { "id": "answer4", "type": "string", "role": "content", "label": "Answer bullet 1", "description": "First use-case bullet, naming a product of the brand this video promotes.", "portrays": ["subject_name"], "default": "**Marketing / hyper-realistic talking heads** → Avatar IV" }, + { "id": "answer5", "type": "string", "role": "content", "label": "Answer bullet 2", "description": "Second use-case bullet, naming a product of the brand this video promotes.", "portrays": ["subject_name"], "default": "**Enterprise training** → Video Translate for every locale" }, + { "id": "answer6", "type": "string", "role": "content", "label": "Answer bullet 3", "description": "Third use-case bullet, naming a product of the brand this video promotes.", "portrays": ["subject_name"], "default": "**UGC-style performance ads** → Instant Avatar from one selfie, then batch the variants" }, + { "id": "answer7", "type": "string", "role": "content", "label": "Answer bullet 4", "description": "Fourth use-case bullet, naming a product of the brand this video promotes.", "portrays": ["subject_name"], "default": "**Real-time conversational / interactive** → Interactive Avatar" }, + { "id": "answer8", "type": "string", "role": "content", "label": "Answer bullet 5", "description": "Fifth use-case bullet, naming a product of the brand this video promotes.", "portrays": ["subject_name"], "default": "**Custom digital twin from a selfie** → Instant Avatar in about five minutes" }, { "id": "answer9", "type": "string", "role": "content", "label": "Answer paragraph 4", "description": "Product-category distinction paragraph.", "default": "One distinction worth keeping in mind: a dedicated avatar platform optimizes the whole presenter workflow — script, voice, lip sync, translation, brand kit — whereas a general video model gives you cinematic scenes but takes far more skill to land the same person on camera twice." }, { "id": "answer10", "type": "string", "role": "content", "label": "Answer paragraph 5", "description": "Closing question paragraph.", "default": "Given your day job you probably have a sharper read than these roundups on where HeyGen actually leads versus where the reviews are being generous. Is this for a specific project, or comparing for positioning?" } ]' diff --git a/docs/catalog/blocks/message-thread-reveal.mdx b/docs/catalog/blocks/message-thread-reveal.mdx index 7c1b473b4..5e944287e 100644 --- a/docs/catalog/blocks/message-thread-reveal.mdx +++ b/docs/catalog/blocks/message-thread-reveal.mdx @@ -10,7 +10,7 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx"; previewSrc="/public/catalog/blocks/message-thread-reveal.json" compositionId="message-thread-reveal" compositionSrc="compositions/message-thread-reveal.html" - variables={[{"id":"contactName","type":"string","role":"content","label":"Contact name","description":"Name pill in the chat header; the avatar shows its first letter.","default":"Rachel"},{"id":"questionMessage","type":"string","role":"content","label":"Opening question","description":"First incoming message that establishes what the conversation is about.","default":"what r u using for the launch video??"},{"id":"teaserMessage","type":"string","role":"content","label":"Teaser reply","description":"First typed outgoing reply before the shared link card.","default":"wait look 👀"},{"id":"cardImage","type":"image","role":"content","label":"Link card image","description":"Preview image on the shared link card.","default":"assets/card-image.jpg"},{"id":"cardTitle","type":"string","role":"content","label":"Link card title","description":"Title on the shared link card.","default":"HyperFrames | Write HTML, render pixel-perfect video"},{"id":"cardDomain","type":"string","role":"content","label":"Link card domain","description":"Domain line under the card title.","default":"hyperframes.heygen.com"},{"id":"reactionMessage","type":"string","role":"content","label":"Link reaction","description":"Incoming reaction immediately after the shared link.","default":"OMG IT'S HTML"},{"id":"reactionEmoji","type":"string","role":"content","label":"Reaction emoji","description":"Emoji-only reaction after the shared link.","default":"🤯🤯🤯"},{"id":"benefitMessage","type":"string","role":"content","label":"Benefit reply","description":"Typed outgoing reply that names the primary benefit.","default":"renders in 4K. no editor"},{"id":"discoveryMessage","type":"string","role":"content","label":"Discovery question","description":"Incoming question asking where the product was found.","default":"where did u find this"},{"id":"sourceMessage","type":"string","role":"content","label":"Source reply","description":"Typed outgoing reply naming where to find the product.","default":"on heygen"},{"id":"workflowMessage","type":"string","role":"content","label":"Workflow reply","description":"Typed outgoing reply describing how the product works.","default":"u just write plain html tags"},{"id":"ownershipMessage","type":"string","role":"content","label":"Ownership reply","description":"Typed outgoing reply that closes the product explanation.","default":"the code is yours forever"},{"id":"installMessage","type":"string","role":"content","label":"Intent reply","description":"Incoming message expressing intent to try the product.","default":"installing rn"},{"id":"thanksMessage","type":"string","role":"content","label":"Closing reply","description":"Final incoming message in the conversation.","default":"ty bestie 💚"},{"id":"brandLogo","type":"image","role":"content","label":"Brand logo","description":"Transparent horizontal wordmark on the closing card.","default":"assets/hyperframes-logo-black.svg"},{"id":"ecProof","type":"string","role":"content","label":"End-card proof","description":"Social-proof line under the stars.","default":"12,000+ creators"},{"id":"ecFeature1","type":"string","role":"content","label":"End-card feature 1","description":"First two-line benefit; use | to break the line.","default":"Write|plain HTML"},{"id":"ecFeature2","type":"string","role":"content","label":"End-card feature 2","description":"Second two-line benefit; use | to break the line.","default":"Render|pixel-perfect"},{"id":"ecFeature3","type":"string","role":"content","label":"End-card feature 3","description":"Third two-line benefit; use | to break the line.","default":"Own|every line"},{"id":"ecCta","type":"string","role":"content","label":"End-card CTA","description":"Label on the closing card button.","default":"Start rendering free"}]} + variables={[{"id":"contactName","type":"string","role":"content","label":"Contact name","description":"Name pill in the chat header; the avatar shows its first letter.","default":"Rachel"},{"id":"questionMessage","type":"string","role":"content","label":"Opening question","description":"First incoming message that establishes what the conversation is about.","default":"what r u using for the launch video??"},{"id":"teaserMessage","type":"string","role":"content","label":"Teaser reply","description":"First typed outgoing reply before the shared link card.","default":"wait look 👀"},{"id":"cardImage","type":"image","role":"content","label":"Link card image","description":"Preview image on the shared link card.","default":"assets/card-image.jpg"},{"id":"cardTitle","type":"string","role":"content","label":"Link card title","description":"Title of the shared link card. It leads with the real name of the brand this video promotes.","portrays":["subject_name"],"default":"HyperFrames | Write HTML, render pixel-perfect video"},{"id":"cardDomain","type":"string","role":"content","label":"Link card domain","description":"Domain of the brand this video promotes, under the card title. Use the real domain.","portrays":["subject_domain"],"default":"hyperframes.heygen.com"},{"id":"reactionMessage","type":"string","role":"content","label":"Link reaction","description":"Incoming reaction immediately after the shared link.","default":"OMG IT'S HTML"},{"id":"reactionEmoji","type":"string","role":"content","label":"Reaction emoji","description":"Emoji-only reaction after the shared link.","default":"🤯🤯🤯"},{"id":"benefitMessage","type":"string","role":"content","label":"Benefit reply","description":"Typed outgoing reply that names the primary benefit.","default":"renders in 4K. no editor"},{"id":"discoveryMessage","type":"string","role":"content","label":"Discovery question","description":"Incoming question asking where the product was found.","default":"where did u find this"},{"id":"sourceMessage","type":"string","role":"content","label":"Source reply","description":"Typed outgoing reply naming where to find the brand this video promotes.","default":"on heygen"},{"id":"workflowMessage","type":"string","role":"content","label":"Workflow reply","description":"Typed outgoing reply describing how the product works.","default":"u just write plain html tags"},{"id":"ownershipMessage","type":"string","role":"content","label":"Ownership reply","description":"Typed outgoing reply that closes the product explanation.","default":"the code is yours forever"},{"id":"installMessage","type":"string","role":"content","label":"Intent reply","description":"Incoming message expressing intent to try the product.","default":"installing rn"},{"id":"thanksMessage","type":"string","role":"content","label":"Closing reply","description":"Final incoming message in the conversation.","default":"ty bestie 💚"},{"id":"brandLogo","type":"image","role":"content","label":"Brand logo","description":"Transparent horizontal wordmark of the brand this video promotes, on the closing card.","portrays":["subject_logo"],"default":"assets/hyperframes-logo-black.svg"},{"id":"ecProof","type":"string","role":"content","label":"End-card proof","description":"Social-proof line under the stars. Claim only a figure the brand itself publishes; use neutral copy when there is none.","portrays":["authority_badge"],"default":"12,000+ creators"},{"id":"ecFeature1","type":"string","role":"content","label":"End-card feature 1","description":"First two-line benefit; use | to break the line.","default":"Write|plain HTML"},{"id":"ecFeature2","type":"string","role":"content","label":"End-card feature 2","description":"Second two-line benefit; use | to break the line.","default":"Render|pixel-perfect"},{"id":"ecFeature3","type":"string","role":"content","label":"End-card feature 3","description":"Third two-line benefit; use | to break the line.","default":"Own|every line"},{"id":"ecCta","type":"string","role":"content","label":"End-card CTA","description":"Label on the closing card button.","default":"Start rendering free"}]} > ```html message-thread-reveal.html @@ -22,19 +22,19 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx"; { "id": "questionMessage", "type": "string", "role": "content", "label": "Opening question", "description": "First incoming message that establishes what the conversation is about.", "default": "what r u using for the launch video??" }, { "id": "teaserMessage", "type": "string", "role": "content", "label": "Teaser reply", "description": "First typed outgoing reply before the shared link card.", "default": "wait look 👀" }, { "id": "cardImage", "type": "image", "role": "content", "label": "Link card image", "description": "Preview image on the shared link card.", "default": "assets/card-image.jpg" }, - { "id": "cardTitle", "type": "string", "role": "content", "label": "Link card title", "description": "Title on the shared link card.", "default": "HyperFrames | Write HTML, render pixel-perfect video" }, - { "id": "cardDomain", "type": "string", "role": "content", "label": "Link card domain", "description": "Domain line under the card title.", "default": "hyperframes.heygen.com" }, + { "id": "cardTitle", "type": "string", "role": "content", "label": "Link card title", "description": "Title of the shared link card. It leads with the real name of the brand this video promotes.", "portrays": ["subject_name"], "default": "HyperFrames | Write HTML, render pixel-perfect video" }, + { "id": "cardDomain", "type": "string", "role": "content", "label": "Link card domain", "description": "Domain of the brand this video promotes, under the card title. Use the real domain.", "portrays": ["subject_domain"], "default": "hyperframes.heygen.com" }, { "id": "reactionMessage", "type": "string", "role": "content", "label": "Link reaction", "description": "Incoming reaction immediately after the shared link.", "default": "OMG IT'S HTML" }, { "id": "reactionEmoji", "type": "string", "role": "content", "label": "Reaction emoji", "description": "Emoji-only reaction after the shared link.", "default": "🤯🤯🤯" }, { "id": "benefitMessage", "type": "string", "role": "content", "label": "Benefit reply", "description": "Typed outgoing reply that names the primary benefit.", "default": "renders in 4K. no editor" }, { "id": "discoveryMessage", "type": "string", "role": "content", "label": "Discovery question", "description": "Incoming question asking where the product was found.", "default": "where did u find this" }, - { "id": "sourceMessage", "type": "string", "role": "content", "label": "Source reply", "description": "Typed outgoing reply naming where to find the product.", "default": "on heygen" }, + { "id": "sourceMessage", "type": "string", "role": "content", "label": "Source reply", "description": "Typed outgoing reply naming where to find the brand this video promotes.", "default": "on heygen" }, { "id": "workflowMessage", "type": "string", "role": "content", "label": "Workflow reply", "description": "Typed outgoing reply describing how the product works.", "default": "u just write plain html tags" }, { "id": "ownershipMessage", "type": "string", "role": "content", "label": "Ownership reply", "description": "Typed outgoing reply that closes the product explanation.", "default": "the code is yours forever" }, { "id": "installMessage", "type": "string", "role": "content", "label": "Intent reply", "description": "Incoming message expressing intent to try the product.", "default": "installing rn" }, { "id": "thanksMessage", "type": "string", "role": "content", "label": "Closing reply", "description": "Final incoming message in the conversation.", "default": "ty bestie 💚" }, - { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent horizontal wordmark on the closing card.", "default": "assets/hyperframes-logo-black.svg" }, - { "id": "ecProof", "type": "string", "role": "content", "label": "End-card proof", "description": "Social-proof line under the stars.", "default": "12,000+ creators" }, + { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent horizontal wordmark of the brand this video promotes, on the closing card.", "portrays": ["subject_logo"], "default": "assets/hyperframes-logo-black.svg" }, + { "id": "ecProof", "type": "string", "role": "content", "label": "End-card proof", "description": "Social-proof line under the stars. Claim only a figure the brand itself publishes; use neutral copy when there is none.", "portrays": ["authority_badge"], "default": "12,000+ creators" }, { "id": "ecFeature1", "type": "string", "role": "content", "label": "End-card feature 1", "description": "First two-line benefit; use | to break the line.", "default": "Write|plain HTML" }, { "id": "ecFeature2", "type": "string", "role": "content", "label": "End-card feature 2", "description": "Second two-line benefit; use | to break the line.", "default": "Render|pixel-perfect" }, { "id": "ecFeature3", "type": "string", "role": "content", "label": "End-card feature 3", "description": "Third two-line benefit; use | to break the line.", "default": "Own|every line" }, @@ -1677,19 +1677,19 @@ want to change on the element: | `questionMessage` | `what r u using for the launch video??` | string | First incoming message that establishes what the conversation is about. | | `teaserMessage` | `wait look 👀` | string | First typed outgoing reply before the shared link card. | | `cardImage` | `assets/card-image.jpg` | image | Preview image on the shared link card. | -| `cardTitle` | `HyperFrames | Write HTML, render pixel-perfect video` | string | Title on the shared link card. | -| `cardDomain` | `hyperframes.heygen.com` | string | Domain line under the card title. | +| `cardTitle` | `HyperFrames | Write HTML, render pixel-perfect video` | string | Title of the shared link card. It leads with the real name of the brand this video promotes. | +| `cardDomain` | `hyperframes.heygen.com` | string | Domain of the brand this video promotes, under the card title. Use the real domain. | | `reactionMessage` | `OMG IT'S HTML` | string | Incoming reaction immediately after the shared link. | | `reactionEmoji` | `🤯🤯🤯` | string | Emoji-only reaction after the shared link. | | `benefitMessage` | `renders in 4K. no editor` | string | Typed outgoing reply that names the primary benefit. | | `discoveryMessage` | `where did u find this` | string | Incoming question asking where the product was found. | -| `sourceMessage` | `on heygen` | string | Typed outgoing reply naming where to find the product. | +| `sourceMessage` | `on heygen` | string | Typed outgoing reply naming where to find the brand this video promotes. | | `workflowMessage` | `u just write plain html tags` | string | Typed outgoing reply describing how the product works. | | `ownershipMessage` | `the code is yours forever` | string | Typed outgoing reply that closes the product explanation. | | `installMessage` | `installing rn` | string | Incoming message expressing intent to try the product. | | `thanksMessage` | `ty bestie 💚` | string | Final incoming message in the conversation. | -| `brandLogo` | `assets/hyperframes-logo-black.svg` | image | Transparent horizontal wordmark on the closing card. | -| `ecProof` | `12,000+ creators` | string | Social-proof line under the stars. | +| `brandLogo` | `assets/hyperframes-logo-black.svg` | image | Transparent horizontal wordmark of the brand this video promotes, on the closing card. | +| `ecProof` | `12,000+ creators` | string | Social-proof line under the stars. Claim only a figure the brand itself publishes; use neutral copy when there is none. | | `ecFeature1` | `Write|plain HTML` | string | First two-line benefit; use | to break the line. | | `ecFeature2` | `Render|pixel-perfect` | string | Second two-line benefit; use | to break the line. | | `ecFeature3` | `Own|every line` | string | Third two-line benefit; use | to break the line. | @@ -1719,19 +1719,19 @@ defaults, so this behaves exactly like the preview above until you change one: { "id": "questionMessage", "type": "string", "role": "content", "label": "Opening question", "description": "First incoming message that establishes what the conversation is about.", "default": "what r u using for the launch video??" }, { "id": "teaserMessage", "type": "string", "role": "content", "label": "Teaser reply", "description": "First typed outgoing reply before the shared link card.", "default": "wait look 👀" }, { "id": "cardImage", "type": "image", "role": "content", "label": "Link card image", "description": "Preview image on the shared link card.", "default": "assets/card-image.jpg" }, - { "id": "cardTitle", "type": "string", "role": "content", "label": "Link card title", "description": "Title on the shared link card.", "default": "HyperFrames | Write HTML, render pixel-perfect video" }, - { "id": "cardDomain", "type": "string", "role": "content", "label": "Link card domain", "description": "Domain line under the card title.", "default": "hyperframes.heygen.com" }, + { "id": "cardTitle", "type": "string", "role": "content", "label": "Link card title", "description": "Title of the shared link card. It leads with the real name of the brand this video promotes.", "portrays": ["subject_name"], "default": "HyperFrames | Write HTML, render pixel-perfect video" }, + { "id": "cardDomain", "type": "string", "role": "content", "label": "Link card domain", "description": "Domain of the brand this video promotes, under the card title. Use the real domain.", "portrays": ["subject_domain"], "default": "hyperframes.heygen.com" }, { "id": "reactionMessage", "type": "string", "role": "content", "label": "Link reaction", "description": "Incoming reaction immediately after the shared link.", "default": "OMG IT'S HTML" }, { "id": "reactionEmoji", "type": "string", "role": "content", "label": "Reaction emoji", "description": "Emoji-only reaction after the shared link.", "default": "🤯🤯🤯" }, { "id": "benefitMessage", "type": "string", "role": "content", "label": "Benefit reply", "description": "Typed outgoing reply that names the primary benefit.", "default": "renders in 4K. no editor" }, { "id": "discoveryMessage", "type": "string", "role": "content", "label": "Discovery question", "description": "Incoming question asking where the product was found.", "default": "where did u find this" }, - { "id": "sourceMessage", "type": "string", "role": "content", "label": "Source reply", "description": "Typed outgoing reply naming where to find the product.", "default": "on heygen" }, + { "id": "sourceMessage", "type": "string", "role": "content", "label": "Source reply", "description": "Typed outgoing reply naming where to find the brand this video promotes.", "default": "on heygen" }, { "id": "workflowMessage", "type": "string", "role": "content", "label": "Workflow reply", "description": "Typed outgoing reply describing how the product works.", "default": "u just write plain html tags" }, { "id": "ownershipMessage", "type": "string", "role": "content", "label": "Ownership reply", "description": "Typed outgoing reply that closes the product explanation.", "default": "the code is yours forever" }, { "id": "installMessage", "type": "string", "role": "content", "label": "Intent reply", "description": "Incoming message expressing intent to try the product.", "default": "installing rn" }, { "id": "thanksMessage", "type": "string", "role": "content", "label": "Closing reply", "description": "Final incoming message in the conversation.", "default": "ty bestie 💚" }, - { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent horizontal wordmark on the closing card.", "default": "assets/hyperframes-logo-black.svg" }, - { "id": "ecProof", "type": "string", "role": "content", "label": "End-card proof", "description": "Social-proof line under the stars.", "default": "12,000+ creators" }, + { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent horizontal wordmark of the brand this video promotes, on the closing card.", "portrays": ["subject_logo"], "default": "assets/hyperframes-logo-black.svg" }, + { "id": "ecProof", "type": "string", "role": "content", "label": "End-card proof", "description": "Social-proof line under the stars. Claim only a figure the brand itself publishes; use neutral copy when there is none.", "portrays": ["authority_badge"], "default": "12,000+ creators" }, { "id": "ecFeature1", "type": "string", "role": "content", "label": "End-card feature 1", "description": "First two-line benefit; use | to break the line.", "default": "Write|plain HTML" }, { "id": "ecFeature2", "type": "string", "role": "content", "label": "End-card feature 2", "description": "Second two-line benefit; use | to break the line.", "default": "Render|pixel-perfect" }, { "id": "ecFeature3", "type": "string", "role": "content", "label": "End-card feature 3", "description": "Third two-line benefit; use | to break the line.", "default": "Own|every line" }, diff --git a/docs/catalog/blocks/notes-reveal.mdx b/docs/catalog/blocks/notes-reveal.mdx index 45d21ba48..98a09436f 100644 --- a/docs/catalog/blocks/notes-reveal.mdx +++ b/docs/catalog/blocks/notes-reveal.mdx @@ -10,7 +10,7 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx"; previewSrc="/public/catalog/blocks/notes-reveal.json" compositionId="notes-reveal" compositionSrc="compositions/notes-reveal.html" - variables={[{"id":"titleL1","type":"string","role":"content","label":"Note title line 1","description":"First line of the note title.","default":"Things nobody told me"},{"id":"titleL2","type":"string","role":"content","label":"Note title line 2","description":"Second line of the note title.","default":"about video."},{"id":"cardTop","type":"string","role":"content","label":"Card headline top","description":"First marker line on the closing card.","default":"THE POWER"},{"id":"check1Label","type":"string","role":"content","label":"Checklist 1 label","description":"Typewriter label of the first checklist row.","default":"WRITE"},{"id":"check1Value","type":"string","role":"content","label":"Checklist 1 value","description":"Marker value of the first checklist row.","default":"HTML"},{"id":"check2Label","type":"string","role":"content","label":"Checklist 2 label","description":"Typewriter label of the second checklist row.","default":"RENDER"},{"id":"check2Value","type":"string","role":"content","label":"Checklist 2 value","description":"Marker value of the second checklist row.","default":"IN 4K"},{"id":"check3Label","type":"string","role":"content","label":"Checklist 3 label","description":"Typewriter label of the third checklist row.","default":"SHIP"},{"id":"check3Value","type":"string","role":"content","label":"Checklist 3 value","description":"Marker value of the third checklist row.","default":"TODAY"}]} + variables={[{"id":"titleL1","type":"string","role":"content","label":"Note title line 1","description":"First line of the note title.","default":"Things nobody told me"},{"id":"titleL2","type":"string","role":"content","label":"Note title line 2","description":"Second line of the note title.","default":"about video."},{"id":"cardTop","type":"string","role":"content","label":"Card headline top","description":"First marker line on the closing card.","default":"THE POWER"},{"id":"check1Label","type":"string","role":"content","label":"Checklist 1 label","description":"Typewriter label of the first checklist row.","default":"WRITE"},{"id":"check1Value","type":"string","role":"content","label":"Checklist 1 value","description":"Marker value of the first checklist row.","default":"HTML"},{"id":"check2Label","type":"string","role":"content","label":"Checklist 2 label","description":"Typewriter label of the second checklist row.","default":"RENDER"},{"id":"check2Value","type":"string","role":"content","label":"Checklist 2 value","description":"Marker value of the second checklist row.","default":"IN 4K"},{"id":"check3Label","type":"string","role":"content","label":"Checklist 3 label","description":"Typewriter label of the third checklist row.","default":"SHIP"},{"id":"check3Value","type":"string","role":"content","label":"Checklist 3 value","description":"Marker value of the third checklist row.","default":"TODAY"},{"id":"brandLogo","type":"image","role":"content","label":"Brand logo","description":"Transparent mark of the brand this video promotes, in the sign-off strip under the card.","portrays":["subject_logo"],"default":"assets/hyperframes-logo-black.svg"},{"id":"brandDomain","type":"string","role":"content","label":"Sign-off domain","description":"Domain of the brand this video promotes, closing the sign-off strip. Use the real domain, or a call to action when no domain fits.","portrays":["subject_domain"],"default":"hyperframes.heygen.com"}]} > ```html notes-reveal.html @@ -26,7 +26,9 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx"; { "id": "check2Label", "type": "string", "role": "content", "label": "Checklist 2 label", "description": "Typewriter label of the second checklist row.", "default": "RENDER" }, { "id": "check2Value", "type": "string", "role": "content", "label": "Checklist 2 value", "description": "Marker value of the second checklist row.", "default": "IN 4K" }, { "id": "check3Label", "type": "string", "role": "content", "label": "Checklist 3 label", "description": "Typewriter label of the third checklist row.", "default": "SHIP" }, - { "id": "check3Value", "type": "string", "role": "content", "label": "Checklist 3 value", "description": "Marker value of the third checklist row.", "default": "TODAY" } + { "id": "check3Value", "type": "string", "role": "content", "label": "Checklist 3 value", "description": "Marker value of the third checklist row.", "default": "TODAY" }, + { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent mark of the brand this video promotes, in the sign-off strip under the card.", "portrays": ["subject_logo"], "default": "assets/hyperframes-logo-black.svg" }, + { "id": "brandDomain", "type": "string", "role": "content", "label": "Sign-off domain", "description": "Domain of the brand this video promotes, closing the sign-off strip. Use the real domain, or a call to action when no domain fits.", "portrays": ["subject_domain"], "default": "hyperframes.heygen.com" } ]' > @@ -142,6 +144,34 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx"; background: #602640; opacity: 0.92; } + /* Sign-off strip: the card ends at y=1708, leaving 212px of dotted scene. The + advertiser's identity lives here rather than inside the card, so the hand-lettered + headline and checklist keep the geometry they were designed with. */ + #brand-strip { + position: absolute; + left: 0; + top: 1742px; + width: 1080px; + height: 132px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 14px; + } + #brand-mark { + display: block; + height: 52px; + width: auto; + } + #brand-domain { + font-family: "Courier Prime", monospace; + font-weight: 700; + font-size: 27px; + letter-spacing: 4px; + color: #602640; + opacity: 0.62; + } #nrv-video { position: absolute; left: 380px; @@ -447,6 +477,10 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
+
+ +
hyperframes.heygen.com
+
@@ -1082,6 +1116,17 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx"; values[1].textContent = t(vars.check2Value, "IN 4K", 8); labels[2].textContent = t(vars.check3Label, "SHIP", 8); values[2].textContent = t(vars.check3Value, "TODAY", 8); + document.getElementById("brand-domain").textContent = t( + vars.brandDomain, + "hyperframes.heygen.com", + 30, + ); + // An image slot arrives as a URL. Leave the packaged mark in place when it is absent, + // so a remix that could not resolve a logo signs off with no claim rather than a + // broken image. + if (typeof vars.brandLogo === "string" && vars.brandLogo.trim()) { + document.getElementById("brand-mark").setAttribute("src", vars.brandLogo.trim()); + } })(); const FPS = 30; const EV = [ @@ -1474,7 +1519,7 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx"; -That writes `compositions/notes-reveal.html`, plus 4 supporting files under `assets/`. +That writes `compositions/notes-reveal.html`, plus 5 supporting files under `assets/`. ## Add it to your video @@ -1511,6 +1556,8 @@ want to change on the element: | `check2Value` | `IN 4K` | string | Marker value of the second checklist row. | | `check3Label` | `SHIP` | string | Typewriter label of the third checklist row. | | `check3Value` | `TODAY` | string | Marker value of the third checklist row. | +| `brandLogo` | `assets/hyperframes-logo-black.svg` | image | Transparent mark of the brand this video promotes, in the sign-off strip under the card. | +| `brandDomain` | `hyperframes.heygen.com` | string | Domain of the brand this video promotes, closing the sign-off strip. Use the real domain, or a call to action when no domain fits. | Set them with `data-variable-values` on the element that mounts it. These are the defaults, so this behaves exactly like the preview above until you change one: @@ -1519,7 +1566,7 @@ defaults, so this behaves exactly like the preview above until you change one:
``` @@ -1540,7 +1587,9 @@ defaults, so this behaves exactly like the preview above until you change one: { "id": "check2Label", "type": "string", "role": "content", "label": "Checklist 2 label", "description": "Typewriter label of the second checklist row.", "default": "RENDER" }, { "id": "check2Value", "type": "string", "role": "content", "label": "Checklist 2 value", "description": "Marker value of the second checklist row.", "default": "IN 4K" }, { "id": "check3Label", "type": "string", "role": "content", "label": "Checklist 3 label", "description": "Typewriter label of the third checklist row.", "default": "SHIP" }, - { "id": "check3Value", "type": "string", "role": "content", "label": "Checklist 3 value", "description": "Marker value of the third checklist row.", "default": "TODAY" } + { "id": "check3Value", "type": "string", "role": "content", "label": "Checklist 3 value", "description": "Marker value of the third checklist row.", "default": "TODAY" }, + { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent mark of the brand this video promotes, in the sign-off strip under the card.", "portrays": ["subject_logo"], "default": "assets/hyperframes-logo-black.svg" }, + { "id": "brandDomain", "type": "string", "role": "content", "label": "Sign-off domain", "description": "Domain of the brand this video promotes, closing the sign-off strip. Use the real domain, or a call to action when no domain fits.", "portrays": ["subject_domain"], "default": "hyperframes.heygen.com" } ]' > @@ -1656,6 +1705,34 @@ defaults, so this behaves exactly like the preview above until you change one: background: #602640; opacity: 0.92; } + /* Sign-off strip: the card ends at y=1708, leaving 212px of dotted scene. The + advertiser's identity lives here rather than inside the card, so the hand-lettered + headline and checklist keep the geometry they were designed with. */ + #brand-strip { + position: absolute; + left: 0; + top: 1742px; + width: 1080px; + height: 132px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 14px; + } + #brand-mark { + display: block; + height: 52px; + width: auto; + } + #brand-domain { + font-family: "Courier Prime", monospace; + font-weight: 700; + font-size: 27px; + letter-spacing: 4px; + color: #602640; + opacity: 0.62; + } #nrv-video { position: absolute; left: 380px; @@ -1961,6 +2038,10 @@ defaults, so this behaves exactly like the preview above until you change one:
+
+ +
hyperframes.heygen.com
+
@@ -2596,6 +2677,17 @@ defaults, so this behaves exactly like the preview above until you change one: values[1].textContent = t(vars.check2Value, "IN 4K", 8); labels[2].textContent = t(vars.check3Label, "SHIP", 8); values[2].textContent = t(vars.check3Value, "TODAY", 8); + document.getElementById("brand-domain").textContent = t( + vars.brandDomain, + "hyperframes.heygen.com", + 30, + ); + // An image slot arrives as a URL. Leave the packaged mark in place when it is absent, + // so a remix that could not resolve a logo signs off with no claim rather than a + // broken image. + if (typeof vars.brandLogo === "string" && vars.brandLogo.trim()) { + document.getElementById("brand-mark").setAttribute("src", vars.brandLogo.trim()); + } })(); const FPS = 30; const EV = [ diff --git a/docs/catalog/blocks/notification-cascade.mdx b/docs/catalog/blocks/notification-cascade.mdx index f895ccce1..912947067 100644 --- a/docs/catalog/blocks/notification-cascade.mdx +++ b/docs/catalog/blocks/notification-cascade.mdx @@ -10,7 +10,7 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx"; previewSrc="/public/catalog/blocks/notification-cascade.json" compositionId="notification-cascade" compositionSrc="compositions/notification-cascade.html" - variables={[{"id":"notifTitle","type":"string","role":"content","label":"Notification title","description":"Bold first line on every banner.","default":"New render"},{"id":"message1","type":"string","role":"content","label":"Message 1","description":"Subtitle of the first banner to land.","default":"Launch video is ready."},{"id":"message2","type":"string","role":"content","label":"Message 2","description":"Subtitle of the second banner.","default":"All checks passed."},{"id":"message3","type":"string","role":"content","label":"Message 3","description":"Subtitle of the third banner.","default":"4K render done in 92s."},{"id":"message4","type":"string","role":"content","label":"Message 4","description":"Subtitle of the fourth banner.","default":"Published to the catalog."},{"id":"appName","type":"string","role":"content","label":"App name","description":"Small credit in the corner of every banner.","default":"HyperFrames"},{"id":"headlineTop","type":"string","role":"content","label":"End-card line 1","description":"First line of the closing card.","default":"SHIP VIDEO"},{"id":"headlineAccent","type":"string","role":"content","label":"End-card line 2","description":"Accented second line of the closing card.","default":"FROM HTML"},{"id":"footerText","type":"string","role":"content","label":"End-card footer","description":"URL or call to action under the headline.","default":"hyperframes.heygen.com"},{"id":"brandLogo","type":"image","role":"content","label":"Brand logo","description":"Transparent logo centered on the closing card.","default":"assets/hyperframes-logo-white.svg"}]} + variables={[{"id":"notifTitle","type":"string","role":"content","label":"Notification title","description":"Bold first line on every banner.","default":"New render"},{"id":"message1","type":"string","role":"content","label":"Message 1","description":"Subtitle of the first banner to land.","default":"Launch video is ready."},{"id":"message2","type":"string","role":"content","label":"Message 2","description":"Subtitle of the second banner.","default":"All checks passed."},{"id":"message3","type":"string","role":"content","label":"Message 3","description":"Subtitle of the third banner.","default":"4K render done in 92s."},{"id":"message4","type":"string","role":"content","label":"Message 4","description":"Subtitle of the fourth banner.","default":"Published to the catalog."},{"id":"appName","type":"string","role":"content","label":"App name","description":"Name of the brand this video promotes, credited in the corner of every banner. Use the real name of that brand.","portrays":["subject_name"],"default":"HyperFrames"},{"id":"headlineTop","type":"string","role":"content","label":"End-card line 1","description":"First line of the closing card.","default":"SHIP VIDEO"},{"id":"headlineAccent","type":"string","role":"content","label":"End-card line 2","description":"Accented second line of the closing card.","default":"FROM HTML"},{"id":"footerText","type":"string","role":"content","label":"End-card footer","description":"Domain of the brand this video promotes, under the end-card headline. Use the real domain, or a call to action when no domain fits.","portrays":["subject_domain"],"default":"hyperframes.heygen.com"},{"id":"brandLogo","type":"image","role":"content","label":"Brand logo","description":"Transparent mark of the brand this video promotes, centered on the closing card.","portrays":["subject_logo"],"default":"assets/hyperframes-logo-white.svg"}]} > ```html notification-cascade.html @@ -23,11 +23,11 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx"; { "id": "message2", "type": "string", "role": "content", "label": "Message 2", "description": "Subtitle of the second banner.", "default": "All checks passed." }, { "id": "message3", "type": "string", "role": "content", "label": "Message 3", "description": "Subtitle of the third banner.", "default": "4K render done in 92s." }, { "id": "message4", "type": "string", "role": "content", "label": "Message 4", "description": "Subtitle of the fourth banner.", "default": "Published to the catalog." }, - { "id": "appName", "type": "string", "role": "content", "label": "App name", "description": "Small credit in the corner of every banner.", "default": "HyperFrames" }, + { "id": "appName", "type": "string", "role": "content", "label": "App name", "description": "Name of the brand this video promotes, credited in the corner of every banner. Use the real name of that brand.", "portrays": ["subject_name"], "default": "HyperFrames" }, { "id": "headlineTop", "type": "string", "role": "content", "label": "End-card line 1", "description": "First line of the closing card.", "default": "SHIP VIDEO" }, { "id": "headlineAccent", "type": "string", "role": "content", "label": "End-card line 2", "description": "Accented second line of the closing card.", "default": "FROM HTML" }, - { "id": "footerText", "type": "string", "role": "content", "label": "End-card footer", "description": "URL or call to action under the headline.", "default": "hyperframes.heygen.com" }, - { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent logo centered on the closing card.", "default": "assets/hyperframes-logo-white.svg" } + { "id": "footerText", "type": "string", "role": "content", "label": "End-card footer", "description": "Domain of the brand this video promotes, under the end-card headline. Use the real domain, or a call to action when no domain fits.", "portrays": ["subject_domain"], "default": "hyperframes.heygen.com" }, + { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent mark of the brand this video promotes, centered on the closing card.", "portrays": ["subject_logo"], "default": "assets/hyperframes-logo-white.svg" } ]' > @@ -575,11 +575,11 @@ want to change on the element: | `message2` | `All checks passed.` | string | Subtitle of the second banner. | | `message3` | `4K render done in 92s.` | string | Subtitle of the third banner. | | `message4` | `Published to the catalog.` | string | Subtitle of the fourth banner. | -| `appName` | `HyperFrames` | string | Small credit in the corner of every banner. | +| `appName` | `HyperFrames` | string | Name of the brand this video promotes, credited in the corner of every banner. Use the real name of that brand. | | `headlineTop` | `SHIP VIDEO` | string | First line of the closing card. | | `headlineAccent` | `FROM HTML` | string | Accented second line of the closing card. | -| `footerText` | `hyperframes.heygen.com` | string | URL or call to action under the headline. | -| `brandLogo` | `assets/hyperframes-logo-white.svg` | image | Transparent logo centered on the closing card. | +| `footerText` | `hyperframes.heygen.com` | string | Domain of the brand this video promotes, under the end-card headline. Use the real domain, or a call to action when no domain fits. | +| `brandLogo` | `assets/hyperframes-logo-white.svg` | image | Transparent mark of the brand this video promotes, centered on the closing card. | Set them with `data-variable-values` on the element that mounts it. These are the defaults, so this behaves exactly like the preview above until you change one: @@ -606,11 +606,11 @@ defaults, so this behaves exactly like the preview above until you change one: { "id": "message2", "type": "string", "role": "content", "label": "Message 2", "description": "Subtitle of the second banner.", "default": "All checks passed." }, { "id": "message3", "type": "string", "role": "content", "label": "Message 3", "description": "Subtitle of the third banner.", "default": "4K render done in 92s." }, { "id": "message4", "type": "string", "role": "content", "label": "Message 4", "description": "Subtitle of the fourth banner.", "default": "Published to the catalog." }, - { "id": "appName", "type": "string", "role": "content", "label": "App name", "description": "Small credit in the corner of every banner.", "default": "HyperFrames" }, + { "id": "appName", "type": "string", "role": "content", "label": "App name", "description": "Name of the brand this video promotes, credited in the corner of every banner. Use the real name of that brand.", "portrays": ["subject_name"], "default": "HyperFrames" }, { "id": "headlineTop", "type": "string", "role": "content", "label": "End-card line 1", "description": "First line of the closing card.", "default": "SHIP VIDEO" }, { "id": "headlineAccent", "type": "string", "role": "content", "label": "End-card line 2", "description": "Accented second line of the closing card.", "default": "FROM HTML" }, - { "id": "footerText", "type": "string", "role": "content", "label": "End-card footer", "description": "URL or call to action under the headline.", "default": "hyperframes.heygen.com" }, - { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent logo centered on the closing card.", "default": "assets/hyperframes-logo-white.svg" } + { "id": "footerText", "type": "string", "role": "content", "label": "End-card footer", "description": "Domain of the brand this video promotes, under the end-card headline. Use the real domain, or a call to action when no domain fits.", "portrays": ["subject_domain"], "default": "hyperframes.heygen.com" }, + { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent mark of the brand this video promotes, centered on the closing card.", "portrays": ["subject_logo"], "default": "assets/hyperframes-logo-white.svg" } ]' > diff --git a/docs/catalog/blocks/share-sheet-carousel.mdx b/docs/catalog/blocks/share-sheet-carousel.mdx index c09397325..1894b7036 100644 --- a/docs/catalog/blocks/share-sheet-carousel.mdx +++ b/docs/catalog/blocks/share-sheet-carousel.mdx @@ -10,7 +10,7 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx"; previewSrc="/public/catalog/blocks/share-sheet-carousel.json" compositionId="share-sheet-carousel" compositionSrc="compositions/share-sheet-carousel.html" - variables={[{"id":"shareTitle","type":"string","role":"content","label":"Sheet title","description":"Heading at the top of the share sheet.","default":"Share"},{"id":"senderName","type":"string","role":"content","label":"Sender","description":"Bold name in the sheet subtitle.","default":"HyperFrames"},{"id":"itemLabel","type":"string","role":"content","label":"Item","description":"What the sender is offering, completing the sentence \"… would like to share …\".","default":"a video"},{"id":"stripText","type":"string","role":"content","label":"Brand strip","description":"Small-caps line under the preview, beside the logo.","default":"OPEN-SOURCE VIDEO ENGINE · SHIP FROM HTML"},{"id":"acceptLabel","type":"string","role":"content","label":"Accept label","description":"Right-hand action button.","default":"Accept"},{"id":"declineLabel","type":"string","role":"content","label":"Decline label","description":"Left-hand action button.","default":"Decline"},{"id":"slideImage1","type":"image","role":"content","label":"Slide image 1","description":"First carousel image; also fills the blurred background.","default":"assets/slide-01.jpg"},{"id":"slideImage2","type":"image","role":"content","label":"Slide image 2","description":"Second carousel image; also fills the blurred background.","default":"assets/slide-02.jpg"},{"id":"slideImage3","type":"image","role":"content","label":"Slide image 3","description":"Third carousel image; also fills the blurred background.","default":"assets/slide-03.jpg"},{"id":"slideImage4","type":"image","role":"content","label":"Slide image 4","description":"Fourth carousel image; also fills the blurred background.","default":"assets/slide-04.jpg"},{"id":"brandLogo","type":"image","role":"content","label":"Brand logo","description":"Transparent horizontal wordmark in the preview strip.","default":"assets/hyperframes-logo-black.svg"}]} + variables={[{"id":"shareTitle","type":"string","role":"content","label":"Sheet title","description":"Heading at the top of the share sheet.","default":"Share"},{"id":"senderName","type":"string","role":"content","label":"Sender","description":"Name of the brand this video promotes, shown as the sender offering the item. Use the real name of that brand.","portrays":["subject_name"],"default":"HyperFrames"},{"id":"itemLabel","type":"string","role":"content","label":"Item","description":"What the sender is offering, completing the sentence \"… would like to share …\".","default":"a video"},{"id":"stripText","type":"string","role":"content","label":"Brand strip","description":"Positioning line of the brand this video promotes, in small caps beside its logo.","portrays":["subject_tagline"],"default":"OPEN-SOURCE VIDEO ENGINE · SHIP FROM HTML"},{"id":"acceptLabel","type":"string","role":"content","label":"Accept label","description":"Right-hand action button.","default":"Accept"},{"id":"declineLabel","type":"string","role":"content","label":"Decline label","description":"Left-hand action button.","default":"Decline"},{"id":"slideImage1","type":"image","role":"content","label":"Slide image 1","description":"First carousel image; also fills the blurred background.","default":"assets/slide-01.jpg"},{"id":"slideImage2","type":"image","role":"content","label":"Slide image 2","description":"Second carousel image; also fills the blurred background.","default":"assets/slide-02.jpg"},{"id":"slideImage3","type":"image","role":"content","label":"Slide image 3","description":"Third carousel image; also fills the blurred background.","default":"assets/slide-03.jpg"},{"id":"slideImage4","type":"image","role":"content","label":"Slide image 4","description":"Fourth carousel image; also fills the blurred background.","default":"assets/slide-04.jpg"},{"id":"brandLogo","type":"image","role":"content","label":"Brand logo","description":"Transparent horizontal wordmark of the brand this video promotes, in the preview strip.","portrays":["subject_logo"],"default":"assets/hyperframes-logo-black.svg"}]} > ```html share-sheet-carousel.html @@ -19,16 +19,16 @@ import { VariablesExplorer } from "/snippets/variables-explorer.jsx"; lang="en" data-composition-variables='[ { "id": "shareTitle", "type": "string", "role": "content", "label": "Sheet title", "description": "Heading at the top of the share sheet.", "default": "Share" }, - { "id": "senderName", "type": "string", "role": "content", "label": "Sender", "description": "Bold name in the sheet subtitle.", "default": "HyperFrames" }, + { "id": "senderName", "type": "string", "role": "content", "label": "Sender", "description": "Name of the brand this video promotes, shown as the sender offering the item. Use the real name of that brand.", "portrays": ["subject_name"], "default": "HyperFrames" }, { "id": "itemLabel", "type": "string", "role": "content", "label": "Item", "description": "What the sender is offering, completing the sentence \"… would like to share …\".", "default": "a video" }, - { "id": "stripText", "type": "string", "role": "content", "label": "Brand strip", "description": "Small-caps line under the preview, beside the logo.", "default": "OPEN-SOURCE VIDEO ENGINE · SHIP FROM HTML" }, + { "id": "stripText", "type": "string", "role": "content", "label": "Brand strip", "description": "Positioning line of the brand this video promotes, in small caps beside its logo.", "portrays": ["subject_tagline"], "default": "OPEN-SOURCE VIDEO ENGINE · SHIP FROM HTML" }, { "id": "acceptLabel", "type": "string", "role": "content", "label": "Accept label", "description": "Right-hand action button.", "default": "Accept" }, { "id": "declineLabel", "type": "string", "role": "content", "label": "Decline label", "description": "Left-hand action button.", "default": "Decline" }, { "id": "slideImage1", "type": "image", "role": "content", "label": "Slide image 1", "description": "First carousel image; also fills the blurred background.", "default": "assets/slide-01.jpg" }, { "id": "slideImage2", "type": "image", "role": "content", "label": "Slide image 2", "description": "Second carousel image; also fills the blurred background.", "default": "assets/slide-02.jpg" }, { "id": "slideImage3", "type": "image", "role": "content", "label": "Slide image 3", "description": "Third carousel image; also fills the blurred background.", "default": "assets/slide-03.jpg" }, { "id": "slideImage4", "type": "image", "role": "content", "label": "Slide image 4", "description": "Fourth carousel image; also fills the blurred background.", "default": "assets/slide-04.jpg" }, - { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent horizontal wordmark in the preview strip.", "default": "assets/hyperframes-logo-black.svg" } + { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent horizontal wordmark of the brand this video promotes, in the preview strip.", "portrays": ["subject_logo"], "default": "assets/hyperframes-logo-black.svg" } ]' > @@ -443,16 +443,16 @@ want to change on the element: | Variable | Default | Accepts | What it does | | --- | --- | --- | --- | | `shareTitle` | `Share` | string | Heading at the top of the share sheet. | -| `senderName` | `HyperFrames` | string | Bold name in the sheet subtitle. | +| `senderName` | `HyperFrames` | string | Name of the brand this video promotes, shown as the sender offering the item. Use the real name of that brand. | | `itemLabel` | `a video` | string | What the sender is offering, completing the sentence "… would like to share …". | -| `stripText` | `OPEN-SOURCE VIDEO ENGINE · SHIP FROM HTML` | string | Small-caps line under the preview, beside the logo. | +| `stripText` | `OPEN-SOURCE VIDEO ENGINE · SHIP FROM HTML` | string | Positioning line of the brand this video promotes, in small caps beside its logo. | | `acceptLabel` | `Accept` | string | Right-hand action button. | | `declineLabel` | `Decline` | string | Left-hand action button. | | `slideImage1` | `assets/slide-01.jpg` | image | First carousel image; also fills the blurred background. | | `slideImage2` | `assets/slide-02.jpg` | image | Second carousel image; also fills the blurred background. | | `slideImage3` | `assets/slide-03.jpg` | image | Third carousel image; also fills the blurred background. | | `slideImage4` | `assets/slide-04.jpg` | image | Fourth carousel image; also fills the blurred background. | -| `brandLogo` | `assets/hyperframes-logo-black.svg` | image | Transparent horizontal wordmark in the preview strip. | +| `brandLogo` | `assets/hyperframes-logo-black.svg` | image | Transparent horizontal wordmark of the brand this video promotes, in the preview strip. | Set them with `data-variable-values` on the element that mounts it. These are the defaults, so this behaves exactly like the preview above until you change one: @@ -475,16 +475,16 @@ defaults, so this behaves exactly like the preview above until you change one: lang="en" data-composition-variables='[ { "id": "shareTitle", "type": "string", "role": "content", "label": "Sheet title", "description": "Heading at the top of the share sheet.", "default": "Share" }, - { "id": "senderName", "type": "string", "role": "content", "label": "Sender", "description": "Bold name in the sheet subtitle.", "default": "HyperFrames" }, + { "id": "senderName", "type": "string", "role": "content", "label": "Sender", "description": "Name of the brand this video promotes, shown as the sender offering the item. Use the real name of that brand.", "portrays": ["subject_name"], "default": "HyperFrames" }, { "id": "itemLabel", "type": "string", "role": "content", "label": "Item", "description": "What the sender is offering, completing the sentence \"… would like to share …\".", "default": "a video" }, - { "id": "stripText", "type": "string", "role": "content", "label": "Brand strip", "description": "Small-caps line under the preview, beside the logo.", "default": "OPEN-SOURCE VIDEO ENGINE · SHIP FROM HTML" }, + { "id": "stripText", "type": "string", "role": "content", "label": "Brand strip", "description": "Positioning line of the brand this video promotes, in small caps beside its logo.", "portrays": ["subject_tagline"], "default": "OPEN-SOURCE VIDEO ENGINE · SHIP FROM HTML" }, { "id": "acceptLabel", "type": "string", "role": "content", "label": "Accept label", "description": "Right-hand action button.", "default": "Accept" }, { "id": "declineLabel", "type": "string", "role": "content", "label": "Decline label", "description": "Left-hand action button.", "default": "Decline" }, { "id": "slideImage1", "type": "image", "role": "content", "label": "Slide image 1", "description": "First carousel image; also fills the blurred background.", "default": "assets/slide-01.jpg" }, { "id": "slideImage2", "type": "image", "role": "content", "label": "Slide image 2", "description": "Second carousel image; also fills the blurred background.", "default": "assets/slide-02.jpg" }, { "id": "slideImage3", "type": "image", "role": "content", "label": "Slide image 3", "description": "Third carousel image; also fills the blurred background.", "default": "assets/slide-03.jpg" }, { "id": "slideImage4", "type": "image", "role": "content", "label": "Slide image 4", "description": "Fourth carousel image; also fills the blurred background.", "default": "assets/slide-04.jpg" }, - { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent horizontal wordmark in the preview strip.", "default": "assets/hyperframes-logo-black.svg" } + { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent horizontal wordmark of the brand this video promotes, in the preview strip.", "portrays": ["subject_logo"], "default": "assets/hyperframes-logo-black.svg" } ]' > diff --git a/docs/catalog/blocks/slack-notification-ad.mdx b/docs/catalog/blocks/slack-notification-ad.mdx index 9140ef3d1..4df9d1bad 100644 --- a/docs/catalog/blocks/slack-notification-ad.mdx +++ b/docs/catalog/blocks/slack-notification-ad.mdx @@ -64,9 +64,9 @@ Move it in time with `data-start`. Put it on a different timeline row with { "id": "request10Text", "type": "string", "role": "content", "label": "Request 10 message", "description": "Tenth Slack notification message.", "default": "investor update needs a quick video walkthrough" }, { "id": "request11Title", "type": "string", "role": "content", "label": "Request 11 sender", "description": "Eleventh Slack notification sender or channel.", "default": "Ben Liu" }, { "id": "request11Text", "type": "string", "role": "content", "label": "Request 11 message", "description": "Eleventh Slack notification message.", "default": "one more: Product Hunt launch video" }, - { "id": "payoffTitle", "type": "string", "role": "content", "label": "Payoff sender", "description": "Final brand payoff title.", "default": "HeyGen" }, - { "id": "payoffText", "type": "string", "role": "content", "label": "Payoff message", "description": "Final brand payoff message.", "default": "Heard you needed some videos?" }, - { "id": "payoffIcon", "type": "image", "role": "content", "label": "Payoff logo", "description": "Logo shown only in the final payoff notification.", "default": "assets/img/heygen-mark.svg" } + { "id": "payoffTitle", "type": "string", "role": "content", "label": "Payoff sender", "description": "Sender of the final payoff notification: the brand this video promotes, by its real name.", "portrays": ["subject_name", "recommended_position"], "default": "HeyGen" }, + { "id": "payoffText", "type": "string", "role": "content", "label": "Payoff message", "description": "Payoff message from the brand this video promotes.", "default": "Heard you needed some videos?" }, + { "id": "payoffIcon", "type": "image", "role": "content", "label": "Payoff logo", "description": "Mark of the brand this video promotes, shown only in the final payoff notification.", "portrays": ["subject_logo"], "default": "assets/img/heygen-mark.svg" } ]' > diff --git a/docs/concepts/variables.mdx b/docs/concepts/variables.mdx index 72f5fa8b9..f6b9e62ad 100644 --- a/docs/concepts/variables.mdx +++ b/docs/concepts/variables.mdx @@ -75,6 +75,39 @@ Supported declared types are: The type lets Studio show the right control and lets rendering catch invalid values. +### Say what a slot means, not where it sits + +`label` and `description` are read by people and by agents. A description that +only gives a position ("small credit in the corner of every banner") leaves the +slot's meaning to be guessed; one that gives the meaning ("name of the brand this +video promotes") can be filled correctly without opening the HTML. + +For a template that will be remixed against someone else's brand, add +`portrays` — a list naming what the slot stands for. It is optional metadata, +ignored by rendering, and it tells an editing agent which slots carry identity +and must not be filled with invented copy: + +```html compositions/card.html +{"id":"appName","type":"string","label":"App name", + "description":"Name of the brand this video promotes, credited in the corner of every banner.", + "portrays":["subject_name"],"default":"HyperFrames"} +``` + +| Value | The slot stands for | +| ---------------------- | -------------------------------------------------------------------------- | +| `subject_name` | The promoted brand's own name | +| `subject_domain` | Its domain, URL, or handle | +| `subject_tagline` | Its own positioning line | +| `subject_logo` | Its mark or wordmark | +| `recommended_position` | The winning or featured slot in a ranking, so the promoted brand goes there | +| `competitor_name` | A real product other than the promoted brand | +| `authority_badge` | A claim of endorsement, ranking authority, rating, or source | +| `host_identity` | The depicted application's own identity, which a remix leaves alone | + +`portrays` is orthogonal to `role`: `role` says which aspect of the composition a +knob affects (`content`, `style`, `timing`, `motion`, `layout`), while `portrays` +says what the value means to a viewer. + ## Bind common values without a script Use direct bindings for the normal cases: diff --git a/packages/lint/src/rules/gsap.test.ts b/packages/lint/src/rules/gsap.test.ts index 33f3083c6..97511a7cf 100644 --- a/packages/lint/src/rules/gsap.test.ts +++ b/packages/lint/src/rules/gsap.test.ts @@ -3070,4 +3070,104 @@ describe("SVG draw-on rules", () => { ).toBeDefined(); }); }); + describe("gsap_timeline_return_used_as_tween", () => { + const composition = (body: string) => ` + +
+ +`; + + it("errors when a rebuild collects tl.to() returns to kill them later", async () => { + // The real shape this exists for: a caret-follow scroll rebuilt on document.fonts.ready. + // Every pushed value is `tl`, so the rebuild called kill() on the master timeline 49 times + // and then stacked its new keyframes on top of the ones it meant to replace. + const result = await lintHyperframeHtml( + composition(` + let scrollTweens = []; + const layout = () => { + scrollTweens.forEach((tw) => tw.kill()); + scrollTweens = []; + scrollTweens.push(tl.to("#typed", { x: -40, duration: 0.08 }, 1)); + }; + layout(); + document.fonts.ready.then(layout);`), + ); + const finding = result.findings.find((f) => f.code === "gsap_timeline_return_used_as_tween"); + expect(finding).toBeDefined(); + expect(finding?.severity).toBe("error"); + expect(finding?.message).toContain("returns THE TIMELINE ITSELF"); + }); + + it("errors when a bound tl.to() return is killed", async () => { + const result = await lintHyperframeHtml( + composition(` + const fade = tl.to("#card", { opacity: 1, duration: 0.5 }, 0); + document.fonts.ready.then(() => fade.kill());`), + ); + expect( + result.findings.find((f) => f.code === "gsap_timeline_return_used_as_tween"), + ).toBeDefined(); + }); + + it("accepts a nested timeline, which is what the fix hint prescribes", async () => { + // A nested child is a real object: killing it replaces exactly its own keyframes and + // cannot reach the parent. Adding it at 0 keeps every child's absolute time. + const result = await lintHyperframeHtml( + composition(` + let caretTl = null; + const layout = () => { + if (caretTl) caretTl.kill(); + caretTl = gsap.timeline(); + caretTl.to("#typed", { x: -40, duration: 0.08 }, 1); + tl.add(caretTl, 0); + }; + layout(); + document.fonts.ready.then(layout);`), + ); + expect( + result.findings.find((f) => f.code === "gsap_timeline_return_used_as_tween"), + ).toBeUndefined(); + }); + + it("accepts gsap.to(), which really does return a tween", async () => { + const result = await lintHyperframeHtml( + composition(` + const tween = gsap.to("#card", { opacity: 1, duration: 0.5, paused: true }); + tl.add(tween, 0); + document.fonts.ready.then(() => tween.kill());`), + ); + expect( + result.findings.find((f) => f.code === "gsap_timeline_return_used_as_tween"), + ).toBeUndefined(); + }); + + it("does not flag Array.from or a chained tl.to() that is never captured", async () => { + const result = await lintHyperframeHtml( + composition(` + const words = Array.from(document.querySelectorAll(".w")); + tl.to(words, { opacity: 1, duration: 0.3 }, 0).to(words, { y: 0, duration: 0.3 }, 0.3);`), + ); + expect( + result.findings.find((f) => f.code === "gsap_timeline_return_used_as_tween"), + ).toBeUndefined(); + }); + + it("does not flag a bound return that is never treated as a tween", async () => { + // Pointless but harmless: the value is just `tl` again, and nothing tween-scoped is + // aimed at it. Flagging this would be noise. + const result = await lintHyperframeHtml( + composition(` + const chain = tl.to("#card", { opacity: 1, duration: 0.5 }, 0); + chain.to("#card2", { opacity: 1, duration: 0.5 }, 1);`), + ); + expect( + result.findings.find((f) => f.code === "gsap_timeline_return_used_as_tween"), + ).toBeUndefined(); + }); + }); }); diff --git a/packages/lint/src/rules/gsap.ts b/packages/lint/src/rules/gsap.ts index c87d75e13..6cfa54b17 100644 --- a/packages/lint/src/rules/gsap.ts +++ b/packages/lint/src/rules/gsap.ts @@ -2156,6 +2156,61 @@ export const gsapRules: LintRule[] = [ return findings; }, + // gsap_timeline_return_used_as_tween — `tl.to()` returns the TIMELINE, not the tween it just + // created. `gsap.to()` returns a Tween, so the two read identically and behave nothing alike. + // Capturing the timeline's return and later treating it as an individual animation aims a + // tween-scoped call at the whole composition: `.kill()` on it interrupts the master timeline + // and detaches it from its parent, which stops a parent-driven seek dead while leaving an + // explicit `tl.progress()` still working -- so a render of the packaged defaults looks fine + // and only live playback breaks. A rebuild that collected these to discard them also leaves + // every previous keyframe in place and stacks the new ones on top. + ({ scripts }) => { + const findings: HyperframeLintFinding[] = []; + for (const script of scripts) { + const source = stripJsComments(script.content); + const timelineVars = collectTimelineVarNames(source); + if (timelineVars.length === 0) continue; + const receivers = timelineVars.map(escapeRegExp).join("|"); + // Two capture shapes: collecting into an array, or binding to a name. Keying on the + // known timeline handles is what keeps `gsap.to()` -- and `Array.from()` -- out of it. + const pattern = new RegExp( + String.raw`(?:\.push\s*\(\s*|(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*)(?:${receivers})\s*\.\s*(to|from|fromTo|set|call|add)\s*\(`, + "g", + ); + let match: RegExpExecArray | null; + while ((match = pattern.exec(source)) !== null) { + const boundName = match[1]; + const method = match[2]!; + // A bound name is only a problem once something tween-scoped is aimed at it; pushing + // into an array is already the collect-to-discard shape this exists to stop. + if (boundName) { + const treatedAsTween = new RegExp( + String.raw`\b${escapeRegExp(boundName)}\s*\.\s*(?:kill|revert|invalidate|pause|resume|restart|seek|progress|timeScale)\s*\(`, + ); + if (!treatedAsTween.test(source)) continue; + } + const contextStart = Math.max(0, match.index - 40); + findings.push({ + code: "gsap_timeline_return_used_as_tween", + severity: "error", + message: + `\`${match[0].includes(".push") ? "push" : boundName}\` captures the return of \`.${method}()\` on timeline \`${timelineVars[0]}\`, ` + + "but a timeline's `.to()`/`.from()`/`.set()` returns THE TIMELINE ITSELF, not the tween it created. " + + "Every captured value is the same master timeline, so a tween-scoped call on it — `.kill()` above all — " + + "hits the whole composition: it interrupts the timeline and detaches it from its parent, which freezes a " + + "parent-driven seek while an explicit `tl.progress()` keeps working. Only `gsap.to()` returns a tween.", + fixHint: + "To build a group of keyframes you can later discard, put them in a nested timeline: " + + "`const nested = gsap.timeline(); nested.to(...); tl.add(nested, 0);` — then `nested.kill()` " + + "replaces exactly those keyframes and cannot reach `tl`. Children keep their absolute times when the " + + "nest is added at 0. To hold a single real tween, create it with `gsap.to(...)` and place it with `tl.add(tween, at)`.", + snippet: truncateSnippet(source.slice(contextStart, match.index + match[0].length + 60)), + }); + } + } + return findings; + }, + // gsap_group_selector_keyframes ({ scripts }) => { const findings: HyperframeLintFinding[] = []; diff --git a/registry/blocks/ai-chat-reveal/TEMPLATE.md b/registry/blocks/ai-chat-reveal/TEMPLATE.md index 2d54e49d1..e757aa5f4 100644 --- a/registry/blocks/ai-chat-reveal/TEMPLATE.md +++ b/registry/blocks/ai-chat-reveal/TEMPLATE.md @@ -2,7 +2,7 @@ ## Surface ownership -This is a generic assistant conversation followed by a brandable closing card. The website brand may appear in the declared conversation and closing-card slots; the chat shell and motion remain template-owned. +This is a generic assistant conversation followed by a brandable closing card. The website brand appears in the declared conversation and closing-card slots; the chat shell and motion remain template-owned. The remix is an advertisement for that brand: the closing card carries its real name, its real domain, and its own mark, while the assistant answering in the conversation keeps its own name. ## Editable slots diff --git a/registry/blocks/ai-chat-reveal/ai-chat-reveal.html b/registry/blocks/ai-chat-reveal/ai-chat-reveal.html index 865646874..5cb85e990 100644 --- a/registry/blocks/ai-chat-reveal/ai-chat-reveal.html +++ b/registry/blocks/ai-chat-reveal/ai-chat-reveal.html @@ -2,19 +2,19 @@ diff --git a/registry/blocks/ai-chat-reveal/registry-item.json b/registry/blocks/ai-chat-reveal/registry-item.json index 0815d6362..cb9fe4037 100644 --- a/registry/blocks/ai-chat-reveal/registry-item.json +++ b/registry/blocks/ai-chat-reveal/registry-item.json @@ -17,7 +17,8 @@ "type": "string", "role": "content", "label": "Assistant name", - "description": "Title in the chat header.", + "description": "Name of the assistant answering in this mock chat. It belongs to the host product, never to the brand this video promotes.", + "portrays": ["host_identity"], "default": "Assistant" }, { @@ -33,7 +34,7 @@ "type": "string", "role": "content", "label": "Answer paragraph 1", - "description": "First streamed paragraph.", + "description": "First streamed paragraph. It names the brand this video promotes and says what that brand does.", "default": "You do not need an editor. HyperFrames renders the HTML you already write into deterministic, pixel-perfect video." }, { @@ -89,7 +90,8 @@ "type": "string", "role": "content", "label": "End-card subline", - "description": "Supporting sentence on the closing card.", + "description": "Positioning sentence of the brand this video promotes, on the closing card.", + "portrays": ["subject_tagline"], "default": "Open-source video rendering — write HTML, ship pixel-perfect video." }, { @@ -97,7 +99,8 @@ "type": "string", "role": "content", "label": "End-card CTA", - "description": "Label on the closing card button.", + "description": "Closing-card button label, which names the brand this video promotes. Use the real name of that brand.", + "portrays": ["subject_name"], "default": "Try HyperFrames" }, { @@ -105,7 +108,8 @@ "type": "string", "role": "content", "label": "End-card footer", - "description": "Letter-spaced line at the very bottom.", + "description": "Domain of the brand this video promotes, letter-spaced at the very bottom. Use the real domain.", + "portrays": ["subject_domain"], "default": "HYPERFRAMES.HEYGEN.COM" }, { @@ -113,7 +117,8 @@ "type": "image", "role": "content", "label": "Brand logo", - "description": "Transparent logo in the closing-card header.", + "description": "Transparent mark of the brand this video promotes, in the closing-card header.", + "portrays": ["subject_logo"], "default": "assets/hyperframes-logo-white.svg" } ], diff --git a/registry/blocks/chatgpt-exchange/TEMPLATE.md b/registry/blocks/chatgpt-exchange/TEMPLATE.md index 532a87f70..911e7986c 100644 --- a/registry/blocks/chatgpt-exchange/TEMPLATE.md +++ b/registry/blocks/chatgpt-exchange/TEMPLATE.md @@ -2,7 +2,7 @@ ## Surface ownership -This template depicts the ChatGPT application. OpenAI owns the surrounding application identity. The supplied website is only the subject discussed inside the conversation. +This template depicts the ChatGPT application. OpenAI owns the surrounding application identity. The supplied website is the subject discussed inside the conversation, never the application around it. The remix is an advertisement for that brand: it takes the recommended first row under its real name, and the remaining rows name real competing products. ## Editable slots diff --git a/registry/blocks/chatgpt-exchange/chatgpt-exchange.html b/registry/blocks/chatgpt-exchange/chatgpt-exchange.html index c3f4b8c03..e16c7b88c 100644 --- a/registry/blocks/chatgpt-exchange/chatgpt-exchange.html +++ b/registry/blocks/chatgpt-exchange/chatgpt-exchange.html @@ -10,21 +10,21 @@ { "id": "tableHeadTool", "type": "string", "role": "content", "label": "Table heading 2", "description": "Tool column heading.", "default": "Best tool" }, { "id": "tableHeadWhy", "type": "string", "role": "content", "label": "Table heading 3", "description": "Explanation column heading.", "default": "Why" }, { "id": "row1Use", "type": "string", "role": "content", "label": "Row 1 use case", "description": "First comparison use case.", "default": "Overall realism" }, - { "id": "row1Tool", "type": "string", "role": "content", "label": "Row 1 tool", "description": "First recommended product.", "default": "HeyGen" }, - { "id": "row1Why", "type": "string", "role": "content", "label": "Row 1 explanation", "description": "First recommendation explanation.", "default": "Most natural facial expressions, lip sync, gestures, voice cloning and localization. Benchmark for talking head videos." }, - { "id": "row1Chip", "type": "string", "role": "content", "label": "Row 1 source", "description": "First source-chip label.", "default": "Official A.I Ranking" }, + { "id": "row1Tool", "type": "string", "role": "content", "label": "Row 1 tool", "description": "Product in the recommended first row: the brand this video promotes, by its real name.", "portrays": ["subject_name", "recommended_position"], "default": "HeyGen" }, + { "id": "row1Why", "type": "string", "role": "content", "label": "Row 1 explanation", "description": "Why the recommended first-row product wins. Describe the promoted brand from what its own site claims.", "portrays": ["recommended_position"], "default": "Most natural facial expressions, lip sync, gestures, voice cloning and localization. Benchmark for talking head videos." }, + { "id": "row1Chip", "type": "string", "role": "content", "label": "Row 1 source", "description": "Source chip on the first row. It asserts where the ranking came from, so use a neutral label whenever the comparison is written for this remix.", "portrays": ["authority_badge"], "default": "Official A.I Ranking" }, { "id": "row2Use", "type": "string", "role": "content", "label": "Row 2 use case", "description": "Second comparison use case.", "default": "Enterprise/training" }, - { "id": "row2Tool", "type": "string", "role": "content", "label": "Row 2 tool", "description": "Second recommended product.", "default": "Synthesia" }, + { "id": "row2Tool", "type": "string", "role": "content", "label": "Row 2 tool", "description": "Product compared in the second row: a real product other than the brand this video promotes.", "portrays": ["competitor_name"], "default": "Synthesia" }, { "id": "row2Why", "type": "string", "role": "content", "label": "Row 2 explanation", "description": "Second recommendation explanation.", "default": "Better collaboration, SCORM, compliance, team workflows; less creator-focused." }, - { "id": "row2Chip", "type": "string", "role": "content", "label": "Row 2 source", "description": "Second source-chip label.", "default": "Official A.I Ranking" }, + { "id": "row2Chip", "type": "string", "role": "content", "label": "Row 2 source", "description": "Source chip on the second row. It asserts where the ranking came from, so use a neutral label whenever the comparison is written for this remix.", "portrays": ["authority_badge"], "default": "Official A.I Ranking" }, { "id": "row3Use", "type": "string", "role": "content", "label": "Row 3 use case", "description": "Third comparison use case.", "default": "Mobile UGC" }, - { "id": "row3Tool", "type": "string", "role": "content", "label": "Row 3 tool", "description": "Third recommended product.", "default": "Captions" }, + { "id": "row3Tool", "type": "string", "role": "content", "label": "Row 3 tool", "description": "Product compared in the third row: a real product other than the brand this video promotes.", "portrays": ["competitor_name"], "default": "Captions" }, { "id": "row3Why", "type": "string", "role": "content", "label": "Row 3 explanation", "description": "Third recommendation explanation.", "default": "Extremely fast mobile workflow and social editing. Great for Reels creators." }, - { "id": "row3Chip", "type": "string", "role": "content", "label": "Row 3 source", "description": "Third source-chip label.", "default": "Creator Stack" }, + { "id": "row3Chip", "type": "string", "role": "content", "label": "Row 3 source", "description": "Source chip on the third row. It asserts where the ranking came from, so use a neutral label whenever the comparison is written for this remix.", "portrays": ["authority_badge"], "default": "Creator Stack" }, { "id": "row4Use", "type": "string", "role": "content", "label": "Row 4 use case", "description": "Fourth comparison use case.", "default": "Real-time conversations" }, - { "id": "row4Tool", "type": "string", "role": "content", "label": "Row 4 tool", "description": "Fourth recommended product.", "default": "Tavus" }, + { "id": "row4Tool", "type": "string", "role": "content", "label": "Row 4 tool", "description": "Product compared in the fourth row: a real product other than the brand this video promotes.", "portrays": ["competitor_name"], "default": "Tavus" }, { "id": "row4Why", "type": "string", "role": "content", "label": "Row 4 explanation", "description": "Fourth recommendation explanation.", "default": "Interactive avatars that can hold live conversations." }, - { "id": "row4Chip", "type": "string", "role": "content", "label": "Row 4 source", "description": "Fourth source-chip label.", "default": "Creator Stack" } + { "id": "row4Chip", "type": "string", "role": "content", "label": "Row 4 source", "description": "Source chip on the fourth row. It asserts where the ranking came from, so use a neutral label whenever the comparison is written for this remix.", "portrays": ["authority_badge"], "default": "Creator Stack" } ]' > @@ -1369,6 +1369,75 @@ tRead + T.readbackDur - 0.35, ); + /* ---- caret-following scroll in the composer. + `prompt` is an editable slot, but `.comp-text` is a fixed-width single line with + `white-space: pre` inside a composer that clips, and the packaged prompt already reaches + the mic. A longer one therefore slides under the mic and the send button and loses its + tail, with the caret hidden behind them -- the remix looks like it typed into nowhere. + A real single-line input scrolls its text left to keep the caret in view; this does the + same, on the very times the reveal already uses. Shrinking the type instead was the other + option and it is worse: it would shrink the packaged prompt too, since that prompt has no + headroom, and it still cannot absorb a prompt twice as long without becoming unreadable. + A prompt that fits emits no keyframes at all, so the packaged composition is untouched. */ + const compTextEl = composer.querySelector("#cge-comp-text"); + // The obstacle is the opaque round button, not the mic: the packaged prompt's caret already + // sits 2px past the mic's left edge, so measuring to the mic would scroll the packaged + // composition. The caret has 35px of clearance to the button, which is the real boundary. + const compStopEl = composer.querySelector("#cge-blue-btn"); + const CARET_PAD = 8; + // The rebuild below has to discard exactly what the previous pass added, and `tl.to()` + // returns the TIMELINE, not the tween it just created -- so collecting those return values + // and killing them on the second pass kills the master timeline, and the composition stops + // dead. It only bites when there are keyframes to discard, i.e. an overflowing prompt: the + // exact remix this scroll exists for, and why a packaged-defaults render never saw it. + // One nested child timeline is a real object that can be killed and replaced, and killing + // it cannot reach `tl`. + let caretTl = null; + const layoutCaretScroll = () => { + if (caretTl) { + caretTl.kill(); + caretTl = null; + // Only ever touch typedEl when there was scrolling to undo. A zero translate — or even + // a clearProps on an untouched element — stamps and removes a transform, which changes + // how the text rasterizes and makes the packaged render differ for no reason. + gsap.set(typedEl, { clearProps: "transform" }); + } + if (!compTextEl || !compStopEl) return; + // Rects live in the scaled .screen space while offsets are layout px, so convert with the + // element's own ratio rather than reading --scale -- this then survives a change to it. + const box = compTextEl.getBoundingClientRect(); + const ratio = compTextEl.offsetWidth ? box.width / compTextEl.offsetWidth : 1; + const limit = + (compStopEl.getBoundingClientRect().left - box.left) / (ratio || 1) - CARET_PAD; + if (!(limit > 0)) return; + // Collect the steps first, so a prompt that fits leaves typedEl alone entirely. + const steps = []; + let shift = 0; + chars.forEach((c, i) => { + const caret = curEls[i + 1]; + if (!caret) return; + const want = Math.max(0, caret.offsetLeft - limit); + if (want > shift + 0.5) { + shift = want; + steps.push([charT[i], -shift]); + } + }); + if (!steps.length) return; + // Children sit at the same absolute times as before, and the nest is added at 0, so the + // nested timeline's local clock is the master's -- the motion is unchanged. + caretTl = gsap.timeline(); + steps.forEach(([at, x]) => { + caretTl.to(typedEl, { x, duration: 0.08, ease: "none" }, at); + }); + // Back to the start when the composer empties, so the collapse animates from x=0. + caretTl.to(typedEl, { x: 0, duration: 0.01 }, tGo); + tl.add(caretTl, 0); + }; + layoutCaretScroll(); + // This composition builds its timeline once, so the first pass measures whatever font was + // live then. Re-measure when the embedded faces are ready. + document.fonts.ready.then(layoutCaretScroll); + window.__timelines["chatgpt-exchange"] = tl; diff --git a/registry/blocks/claude-exchange/TEMPLATE.md b/registry/blocks/claude-exchange/TEMPLATE.md index 44a69af50..e59942442 100644 --- a/registry/blocks/claude-exchange/TEMPLATE.md +++ b/registry/blocks/claude-exchange/TEMPLATE.md @@ -2,7 +2,7 @@ ## Surface ownership -This template depicts the Claude application. Anthropic owns the surrounding application identity. The supplied website is only the subject discussed inside the conversation. +This template depicts the Claude application. Anthropic owns the surrounding application identity. The supplied website is the subject discussed inside the conversation, never the application around it. The remix is an advertisement for that brand: the answer names it as the recommended pick under its real name. ## Editable slots diff --git a/registry/blocks/claude-exchange/claude-exchange.html b/registry/blocks/claude-exchange/claude-exchange.html index 7dc0d1078..f4d514204 100644 --- a/registry/blocks/claude-exchange/claude-exchange.html +++ b/registry/blocks/claude-exchange/claude-exchange.html @@ -8,13 +8,13 @@ { "id": "lead", "type": "string", "role": "content", "label": "Search lead", "description": "Sentence introducing the search step.", "default": "I'll search for the current state of this space since AI avatar tools move fast." }, { "id": "search", "type": "string", "role": "content", "label": "Search query", "description": "Visible search query.", "default": "best AI avatar video generator 2026" }, { "id": "answer1", "type": "string", "role": "content", "label": "Answer paragraph 1", "description": "First answer paragraph.", "default": "It depends on what you're making, but the field has consolidated fast and one platform now covers most of it." }, - { "id": "answer2", "type": "string", "role": "content", "label": "Answer paragraph 2", "description": "Second answer paragraph.", "default": "**HeyGen** is where most teams land. Independent testing, not just vendor blogs, puts **Avatar IV** highest for talking-head realism, with facial micro-expressions and gesture control that hold up at a full-screen crop {HeyGen}. It also has the deepest enterprise adoption of the group, including a large share of the Fortune 100, which matters if you need the same presenter on brand across a year of campaigns." }, + { "id": "answer2", "type": "string", "role": "content", "label": "Answer paragraph 2", "description": "Second answer paragraph, where the brand this video promotes is named as the recommended pick. Keep any evidence claim to what that brand publishes.", "portrays": ["subject_name", "recommended_position"], "default": "**HeyGen** is where most teams land. Independent testing, not just vendor blogs, puts **Avatar IV** highest for talking-head realism, with facial micro-expressions and gesture control that hold up at a full-screen crop {HeyGen}. It also has the deepest enterprise adoption of the group, including a large share of the Fortune 100, which matters if you need the same presenter on brand across a year of campaigns." }, { "id": "answer3", "type": "string", "role": "content", "label": "Answer paragraph 3", "description": "Lead-in to the use-case list.", "default": "By use case, the pieces shake out roughly like this:" }, - { "id": "answer4", "type": "string", "role": "content", "label": "Answer bullet 1", "description": "First use-case bullet.", "default": "**Marketing / hyper-realistic talking heads** → Avatar IV" }, - { "id": "answer5", "type": "string", "role": "content", "label": "Answer bullet 2", "description": "Second use-case bullet.", "default": "**Enterprise training** → Video Translate for every locale" }, - { "id": "answer6", "type": "string", "role": "content", "label": "Answer bullet 3", "description": "Third use-case bullet.", "default": "**UGC-style performance ads** → Instant Avatar from one selfie, then batch the variants" }, - { "id": "answer7", "type": "string", "role": "content", "label": "Answer bullet 4", "description": "Fourth use-case bullet.", "default": "**Real-time conversational / interactive** → Interactive Avatar" }, - { "id": "answer8", "type": "string", "role": "content", "label": "Answer bullet 5", "description": "Fifth use-case bullet.", "default": "**Custom digital twin from a selfie** → Instant Avatar in about five minutes" }, + { "id": "answer4", "type": "string", "role": "content", "label": "Answer bullet 1", "description": "First use-case bullet, naming a product of the brand this video promotes.", "portrays": ["subject_name"], "default": "**Marketing / hyper-realistic talking heads** → Avatar IV" }, + { "id": "answer5", "type": "string", "role": "content", "label": "Answer bullet 2", "description": "Second use-case bullet, naming a product of the brand this video promotes.", "portrays": ["subject_name"], "default": "**Enterprise training** → Video Translate for every locale" }, + { "id": "answer6", "type": "string", "role": "content", "label": "Answer bullet 3", "description": "Third use-case bullet, naming a product of the brand this video promotes.", "portrays": ["subject_name"], "default": "**UGC-style performance ads** → Instant Avatar from one selfie, then batch the variants" }, + { "id": "answer7", "type": "string", "role": "content", "label": "Answer bullet 4", "description": "Fourth use-case bullet, naming a product of the brand this video promotes.", "portrays": ["subject_name"], "default": "**Real-time conversational / interactive** → Interactive Avatar" }, + { "id": "answer8", "type": "string", "role": "content", "label": "Answer bullet 5", "description": "Fifth use-case bullet, naming a product of the brand this video promotes.", "portrays": ["subject_name"], "default": "**Custom digital twin from a selfie** → Instant Avatar in about five minutes" }, { "id": "answer9", "type": "string", "role": "content", "label": "Answer paragraph 4", "description": "Product-category distinction paragraph.", "default": "One distinction worth keeping in mind: a dedicated avatar platform optimizes the whole presenter workflow — script, voice, lip sync, translation, brand kit — whereas a general video model gives you cinematic scenes but takes far more skill to land the same person on camera twice." }, { "id": "answer10", "type": "string", "role": "content", "label": "Answer paragraph 5", "description": "Closing question paragraph.", "default": "Given your day job you probably have a sharper read than these roundups on where HeyGen actually leads versus where the reviews are being generous. Is this for a specific project, or comparing for positioning?" } ]' @@ -1582,6 +1582,54 @@ // the minute rolls over between the source frames — keep that tl.to(document.getElementById("cle-clock-early"), { autoAlpha: 0, duration: 0.01 }, 8.4); tl.to(document.getElementById("cle-clock-late"), { autoAlpha: 1, duration: 0.01 }, 8.4); + + /* ---- caret-following scroll in the composer. + `prompt` is an editable slot, but `.comp-text` is a fixed-width single line with + `white-space: pre` inside a composer that clips, and the packaged prompt already + reaches the mic. A longer one therefore slides under the mic and the send button and + loses its tail, with the caret hidden behind them. A real single-line input scrolls its + text left to keep the caret in view; this does the same, on the very times the reveal + already uses. Shrinking the type instead would shrink the packaged prompt too, since + that prompt has no headroom, and still could not absorb a prompt twice as long without + becoming unreadable. A prompt that fits emits no keyframes, so the packaged + composition is untouched. Lives inside build(), which is re-run once the embedded + faces are ready, so the measurement is taken against the final font. */ + const compTextEl = composer.querySelector("#cle-comp-text"); + // The obstacle is the opaque round button, not the mic glyph the text may abut. Take the + // leftmost of the two that occupy that corner, since one swaps in for the other. + const compStops = ["#cle-voice-btn", "#cle-send-btn"] + .map((sel) => composer.querySelector(sel)) + .filter(Boolean); + const CARET_PAD = 8; + if (compTextEl && compStops.length) { + // Rects live in the scaled .screen space while offsets are layout px, so convert with + // the element's own ratio rather than reading --scale. + const box = compTextEl.getBoundingClientRect(); + const ratio = compTextEl.offsetWidth ? box.width / compTextEl.offsetWidth : 1; + const stopLeft = Math.min(...compStops.map((el) => el.getBoundingClientRect().left)); + const limit = (stopLeft - box.left) / (ratio || 1) - CARET_PAD; + if (limit > 0) { + // Collect the steps first, so a prompt that fits leaves typedEl alone entirely: even a + // zero translate stamps a transform, which changes how the text rasterizes and would + // make the packaged render differ from before for no reason. + const steps = []; + let shift = 0; + chars.forEach((c, i) => { + const caret = curEls[i + 1]; + if (!caret) return; + const want = Math.max(0, caret.offsetLeft - limit); + if (want > shift + 0.5) { + shift = want; + steps.push([charT[i], -shift]); + } + }); + if (steps.length) { + steps.forEach(([at, x]) => tl.to(typedEl, { x, duration: 0.08, ease: "none" }, at)); + // Back to the start when the composer empties, so the collapse animates from x=0. + tl.to(typedEl, { x: 0, duration: 0.01 }, tGo); + } + } + } } build(); diff --git a/registry/blocks/message-thread-reveal/TEMPLATE.md b/registry/blocks/message-thread-reveal/TEMPLATE.md index 2c769aeb1..093b34619 100644 --- a/registry/blocks/message-thread-reveal/TEMPLATE.md +++ b/registry/blocks/message-thread-reveal/TEMPLATE.md @@ -2,7 +2,7 @@ ## Surface ownership -This template depicts a phone messaging interface. The supplied website is the subject of the conversation, shared link, and closing card; it does not own the messaging application chrome. +This template depicts a phone messaging interface. The supplied website is the subject of the conversation, shared link, and closing card; it does not own the messaging application chrome. The remix is an advertisement for that brand: the shared link card and the closing card carry its real name and its real domain. ## Editable slots diff --git a/registry/blocks/message-thread-reveal/message-thread-reveal.html b/registry/blocks/message-thread-reveal/message-thread-reveal.html index 5d557c17d..8ac7d31b4 100644 --- a/registry/blocks/message-thread-reveal/message-thread-reveal.html +++ b/registry/blocks/message-thread-reveal/message-thread-reveal.html @@ -6,19 +6,19 @@ { "id": "questionMessage", "type": "string", "role": "content", "label": "Opening question", "description": "First incoming message that establishes what the conversation is about.", "default": "what r u using for the launch video??" }, { "id": "teaserMessage", "type": "string", "role": "content", "label": "Teaser reply", "description": "First typed outgoing reply before the shared link card.", "default": "wait look 👀" }, { "id": "cardImage", "type": "image", "role": "content", "label": "Link card image", "description": "Preview image on the shared link card.", "default": "assets/card-image.jpg" }, - { "id": "cardTitle", "type": "string", "role": "content", "label": "Link card title", "description": "Title on the shared link card.", "default": "HyperFrames | Write HTML, render pixel-perfect video" }, - { "id": "cardDomain", "type": "string", "role": "content", "label": "Link card domain", "description": "Domain line under the card title.", "default": "hyperframes.heygen.com" }, + { "id": "cardTitle", "type": "string", "role": "content", "label": "Link card title", "description": "Title of the shared link card. It leads with the real name of the brand this video promotes.", "portrays": ["subject_name"], "default": "HyperFrames | Write HTML, render pixel-perfect video" }, + { "id": "cardDomain", "type": "string", "role": "content", "label": "Link card domain", "description": "Domain of the brand this video promotes, under the card title. Use the real domain.", "portrays": ["subject_domain"], "default": "hyperframes.heygen.com" }, { "id": "reactionMessage", "type": "string", "role": "content", "label": "Link reaction", "description": "Incoming reaction immediately after the shared link.", "default": "OMG IT'S HTML" }, { "id": "reactionEmoji", "type": "string", "role": "content", "label": "Reaction emoji", "description": "Emoji-only reaction after the shared link.", "default": "🤯🤯🤯" }, { "id": "benefitMessage", "type": "string", "role": "content", "label": "Benefit reply", "description": "Typed outgoing reply that names the primary benefit.", "default": "renders in 4K. no editor" }, { "id": "discoveryMessage", "type": "string", "role": "content", "label": "Discovery question", "description": "Incoming question asking where the product was found.", "default": "where did u find this" }, - { "id": "sourceMessage", "type": "string", "role": "content", "label": "Source reply", "description": "Typed outgoing reply naming where to find the product.", "default": "on heygen" }, + { "id": "sourceMessage", "type": "string", "role": "content", "label": "Source reply", "description": "Typed outgoing reply naming where to find the brand this video promotes.", "default": "on heygen" }, { "id": "workflowMessage", "type": "string", "role": "content", "label": "Workflow reply", "description": "Typed outgoing reply describing how the product works.", "default": "u just write plain html tags" }, { "id": "ownershipMessage", "type": "string", "role": "content", "label": "Ownership reply", "description": "Typed outgoing reply that closes the product explanation.", "default": "the code is yours forever" }, { "id": "installMessage", "type": "string", "role": "content", "label": "Intent reply", "description": "Incoming message expressing intent to try the product.", "default": "installing rn" }, { "id": "thanksMessage", "type": "string", "role": "content", "label": "Closing reply", "description": "Final incoming message in the conversation.", "default": "ty bestie 💚" }, - { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent horizontal wordmark on the closing card.", "default": "assets/hyperframes-logo-black.svg" }, - { "id": "ecProof", "type": "string", "role": "content", "label": "End-card proof", "description": "Social-proof line under the stars.", "default": "12,000+ creators" }, + { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent horizontal wordmark of the brand this video promotes, on the closing card.", "portrays": ["subject_logo"], "default": "assets/hyperframes-logo-black.svg" }, + { "id": "ecProof", "type": "string", "role": "content", "label": "End-card proof", "description": "Social-proof line under the stars. Claim only a figure the brand itself publishes; use neutral copy when there is none.", "portrays": ["authority_badge"], "default": "12,000+ creators" }, { "id": "ecFeature1", "type": "string", "role": "content", "label": "End-card feature 1", "description": "First two-line benefit; use | to break the line.", "default": "Write|plain HTML" }, { "id": "ecFeature2", "type": "string", "role": "content", "label": "End-card feature 2", "description": "Second two-line benefit; use | to break the line.", "default": "Render|pixel-perfect" }, { "id": "ecFeature3", "type": "string", "role": "content", "label": "End-card feature 3", "description": "Third two-line benefit; use | to break the line.", "default": "Own|every line" }, diff --git a/registry/blocks/message-thread-reveal/registry-item.json b/registry/blocks/message-thread-reveal/registry-item.json index 0ed23dc1a..c28bd3d04 100644 --- a/registry/blocks/message-thread-reveal/registry-item.json +++ b/registry/blocks/message-thread-reveal/registry-item.json @@ -49,7 +49,8 @@ "type": "string", "role": "content", "label": "Link card title", - "description": "Title on the shared link card.", + "description": "Title of the shared link card. It leads with the real name of the brand this video promotes.", + "portrays": ["subject_name"], "default": "HyperFrames | Write HTML, render pixel-perfect video" }, { @@ -57,7 +58,8 @@ "type": "string", "role": "content", "label": "Link card domain", - "description": "Domain line under the card title.", + "description": "Domain of the brand this video promotes, under the card title. Use the real domain.", + "portrays": ["subject_domain"], "default": "hyperframes.heygen.com" }, { @@ -97,7 +99,7 @@ "type": "string", "role": "content", "label": "Source reply", - "description": "Typed outgoing reply naming where to find the product.", + "description": "Typed outgoing reply naming where to find the brand this video promotes.", "default": "on heygen" }, { @@ -137,7 +139,8 @@ "type": "image", "role": "content", "label": "Brand logo", - "description": "Transparent horizontal wordmark on the closing card.", + "description": "Transparent horizontal wordmark of the brand this video promotes, on the closing card.", + "portrays": ["subject_logo"], "default": "assets/hyperframes-logo-black.svg" }, { @@ -145,7 +148,8 @@ "type": "string", "role": "content", "label": "End-card proof", - "description": "Social-proof line under the stars.", + "description": "Social-proof line under the stars. Claim only a figure the brand itself publishes; use neutral copy when there is none.", + "portrays": ["authority_badge"], "default": "12,000+ creators" }, { diff --git a/registry/blocks/notes-reveal/TEMPLATE.md b/registry/blocks/notes-reveal/TEMPLATE.md index e7e092aba..4cce16346 100644 --- a/registry/blocks/notes-reveal/TEMPLATE.md +++ b/registry/blocks/notes-reveal/TEMPLATE.md @@ -2,17 +2,24 @@ ## Surface ownership -This template depicts a notes application and a hand-lettered checklist. The supplied website may provide the declared note and checklist copy; it does not own the notes application chrome. +This template depicts a notes application and a hand-lettered checklist. The supplied website provides the declared note and checklist copy; it does not own the notes application chrome. The remix is an advertisement for that brand, and the sign-off strip under the closing card is where that brand is identified: place its real mark in `brandLogo` and its real domain in `brandDomain`. + +**The seven note body lines are the video.** They are typed out over the first 20 of 24.9 seconds and are the only place the note says anything, so they carry the pitch. The packaged defaults tell HyperFrames' own story — "my videos sucked", "started HyperFrames three weeks ago" — and every one of them must be rewritten for the brand being advertised. Leaving them is shipping HyperFrames' marketing inside somebody else's ad. + +Write them as one person's note, in the first person, arcing from a problem to this brand solving it: three lines of the problem, a turn where the brand enters, then the payoff. Use the brand's own language from the captured page, in the language of that page. ## Editable slots Only defaults declared in `data-composition-variables` are editable: -- `titleL1`, `titleL2`, and `cardTop` +- `noteLine1` through `noteLine7` — the typed note body, the substance of the ad +- `titleL1` and `titleL2` — the note's title, two lines +- `cardTop`, `cardMid`, `cardBottom` — the three hand-lettered lines of the closing card. `cardTop`'s last word gets the underline and `cardBottom`'s first word gets the ring, so put the word worth emphasising in those positions. `cardMid` is a short connector ("OF", "FOR", "VOOR"): keep it to one or two words. - `check1Label` through `check3Label` - `check1Value` through `check3Value` +- `brandLogo` and `brandDomain` — the sign-off strip. `brandLogo` takes a transparent mark; leave it at its packaged default rather than substituting a nav icon or a generated image when no real mark is available, since an unidentified sign-off is better than a wrong one. -Typed copy is length-locked to within 20% of the original. +Length is not locked. Every text slot is fitted to its box at render time, shrinking the type only as far as it must and never past the designed size, so a value the length of the default renders exactly as drawn and a longer one still stays inside the card or the note. The declared character caps are only a backstop for absurd input. Typing speed adapts to the line: each body line is revealed inside the same frame window the packaged copy used, so the note always finishes before the card appears and the composition stays 24.867s whatever you write. ## Safe editing mechanics @@ -20,4 +27,4 @@ Call `set_template_variable_defaults` once with the existing variable ids and th ## Protected -Preserve notes chrome, paper treatment, fonts, colors, checklist geometry, scene structure, duration, timing, easing, handwriting motion, and reveal cadence. +Preserve notes chrome, paper treatment, fonts, colors, checklist geometry, sign-off strip geometry, scene structure, duration, timing, easing, handwriting motion, and reveal cadence. diff --git a/registry/blocks/notes-reveal/assets/hyperframes-logo-black.svg b/registry/blocks/notes-reveal/assets/hyperframes-logo-black.svg new file mode 100644 index 000000000..a103b1bad --- /dev/null +++ b/registry/blocks/notes-reveal/assets/hyperframes-logo-black.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/registry/blocks/notes-reveal/notes-reveal.html b/registry/blocks/notes-reveal/notes-reveal.html index 0b3d5fa44..ff46bd992 100644 --- a/registry/blocks/notes-reveal/notes-reveal.html +++ b/registry/blocks/notes-reveal/notes-reveal.html @@ -4,13 +4,24 @@ data-composition-variables='[ { "id": "titleL1", "type": "string", "role": "content", "label": "Note title line 1", "description": "First line of the note title.", "default": "Things nobody told me" }, { "id": "titleL2", "type": "string", "role": "content", "label": "Note title line 2", "description": "Second line of the note title.", "default": "about video." }, - { "id": "cardTop", "type": "string", "role": "content", "label": "Card headline top", "description": "First marker line on the closing card.", "default": "THE POWER" }, + { "id": "noteLine1", "type": "string", "role": "content", "label": "Note body line 1", "description": "First typed line of the note. The seven body lines are the bulk of the video: they must be about the brand this video promotes, not about video-making.", "default": "my videos sucked" }, + { "id": "noteLine2", "type": "string", "role": "content", "label": "Note body line 2", "description": "Second typed line of the note.", "default": "my tools cost $$$" }, + { "id": "noteLine3", "type": "string", "role": "content", "label": "Note body line 3", "description": "Third typed line of the note.", "default": "and re-edits never end." }, + { "id": "noteLine4", "type": "string", "role": "content", "label": "Note body line 4", "description": "Fourth typed line of the note — the turn, where the brand enters.", "default": "then i tried writing html only" }, + { "id": "noteLine5", "type": "string", "role": "content", "label": "Note body line 5", "description": "Fifth typed line of the note — name the brand here.", "default": "started HyperFrames three weeks ago" }, + { "id": "noteLine6", "type": "string", "role": "content", "label": "Note body line 6", "description": "Sixth typed line of the note.", "default": "the grind is gone, but more than that —" }, + { "id": "noteLine7", "type": "string", "role": "content", "label": "Note body line 7", "description": "Last typed line of the note — the payoff.", "default": "I ship videos weekly." }, + { "id": "cardTop", "type": "string", "role": "content", "label": "Card headline top", "description": "First marker line on the closing card. The underline follows its last word.", "default": "THE POWER" }, + { "id": "cardMid", "type": "string", "role": "content", "label": "Card headline middle", "description": "Short connector line between the two headline lines, centred on the card.", "default": "OF" }, + { "id": "cardBottom", "type": "string", "role": "content", "label": "Card headline bottom", "description": "Last marker line on the closing card. Its first word is ringed.", "default": "ONE FILE" }, { "id": "check1Label", "type": "string", "role": "content", "label": "Checklist 1 label", "description": "Typewriter label of the first checklist row.", "default": "WRITE" }, { "id": "check1Value", "type": "string", "role": "content", "label": "Checklist 1 value", "description": "Marker value of the first checklist row.", "default": "HTML" }, { "id": "check2Label", "type": "string", "role": "content", "label": "Checklist 2 label", "description": "Typewriter label of the second checklist row.", "default": "RENDER" }, { "id": "check2Value", "type": "string", "role": "content", "label": "Checklist 2 value", "description": "Marker value of the second checklist row.", "default": "IN 4K" }, { "id": "check3Label", "type": "string", "role": "content", "label": "Checklist 3 label", "description": "Typewriter label of the third checklist row.", "default": "SHIP" }, - { "id": "check3Value", "type": "string", "role": "content", "label": "Checklist 3 value", "description": "Marker value of the third checklist row.", "default": "TODAY" } + { "id": "check3Value", "type": "string", "role": "content", "label": "Checklist 3 value", "description": "Marker value of the third checklist row.", "default": "TODAY" }, + { "id": "brandLogo", "type": "image", "role": "content", "label": "Brand logo", "description": "Transparent mark of the brand this video promotes, in the sign-off strip under the card.", "portrays": ["subject_logo"], "default": "assets/hyperframes-logo-black.svg" }, + { "id": "brandDomain", "type": "string", "role": "content", "label": "Sign-off domain", "description": "Domain of the brand this video promotes, closing the sign-off strip. Use the real domain, or a call to action when no domain fits.", "portrays": ["subject_domain"], "default": "hyperframes.heygen.com" } ]' > @@ -126,6 +137,34 @@ background: #602640; opacity: 0.92; } + /* Sign-off strip: the card ends at y=1708, leaving 212px of dotted scene. The + advertiser's identity lives here rather than inside the card, so the hand-lettered + headline and checklist keep the geometry they were designed with. */ + #brand-strip { + position: absolute; + left: 0; + top: 1742px; + width: 1080px; + height: 132px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 14px; + } + #brand-mark { + display: block; + height: 52px; + width: auto; + } + #brand-domain { + font-family: "Courier Prime", monospace; + font-weight: 700; + font-size: 27px; + letter-spacing: 4px; + color: #602640; + opacity: 0.62; + } #nrv-video { position: absolute; left: 380px; @@ -235,7 +274,10 @@ font-weight: 400; letter-spacing: 0em; color: #191919; - white-space: nowrap; + /* `pre`, not `nowrap`: the per-character spans are built at runtime from the line + variables, and a leading or trailing space in a supplied line would collapse under + `nowrap` — the caret then stalls on a zero-width span. */ + white-space: pre; } #caret { position: absolute; @@ -330,7 +372,6 @@
ONEFILE + >ONEFILE
WRITE
@@ -431,6 +481,10 @@
+
+ +
hyperframes.heygen.com
+
@@ -612,16 +666,7 @@ data-layout-allow-overlap data-layout-allow-occlusion style="top: 283px" - > - my videos sucked -
+ >
- my tools cost $$$ -
+ >
- and re-edits never end. -
+ >
- then i tried writing html only -
+ >
- started HyperFrames three weeks ago -
+ >
- the grind is gone, but more than that -
+ >
- I ship videos weekly. -
+ >
@@ -1039,12 +993,19 @@