Files
hyperframes/docs/public/catalog/components/sheet-spring-up.json
Miguel Ángel 406bf316a3 fix(catalog): make component previews answer their variables panel (#3323)
* fix(catalog): make component previews answer their variables panel

Every control on a component's catalog page did nothing. Asking
caption-camera-follow for a violet accent rendered gold, and so did green
and blue, on 166 of the 168 components that declare variables.

A component ships a snippet, which is what the page hands you to paste and
which carries the declaration plus the script that turns a chosen value
into a CSS custom property, and a demo.html which stages and animates it.
The preview is built from the demo, and the demo was authored as a copy of
the snippet rather than a reference to it. The copies drifted until almost
none of them carried the declaration or the reader, so the payload for that
page never contained the word violet at all.

Components come in two shapes, so the repair does too.

123 ship a snippet that registers its own paused timeline. That snippet is
a whole piece, so their preview is now built from it and carries markup,
variables and motion together.

45 are markup plus a commented recipe, where the demo owns the motion.
Those demos now carry the snippet's declaration, reader and var-driven CSS
in the registry itself, written by scripts/catalog/sync-demo-variables.ts.
Nothing is patched in at build time.

A test runs that tool in dry mode and fails when a demo has drifted again,
naming the command that repairs it. It also asserts it inspected more than
a hundred components, because a check that silently matches nothing is how
this rotted in the first place.

Measured by rendering every payload in a real player rather than by reading
markup: payloads declaring their variables go from 2 of 168 to 168 of 168,
previews that animate go from 166 to 167, and nothing that moved stopped
moving. ascii-render-pass and star-rating-fill render a still frame when
built from their snippet, so they keep the demo path as a recorded
exception and stay in the state they were already in.

* refactor(catalog): give the preview pipeline one lookup and one entrypoint guard

Follow-up on the same branch, no behaviour change: 42 tests still pass and
`sync-demo-variables --check` still reports all 168 components clean.

The payload generator and the demo sync had each grown their own copy of
"given a component directory, find the snippet and the demo". Both now call
`componentFiles`, which is the same duplication-by-copying that broke the
previews in the first place.

Both catalog generators also carried a byte-identical 12-line guard for
"only run main() when this file is the entrypoint". That clone was already
in the tree, but nothing had touched both files at once before, so it had
never surfaced. It is now `runAsCommand`, and the sync script's variant of
the same condition is `isEntrypoint`.

The rest is flattening: the layering guards read as a table of conditions
instead of a chain, the reporting splits by what it reports, and the entry
resolution comes out of `buildPayload` rather than being spliced into it.

Also runs the formatter over the demos this branch rewrote. Whitespace only,
and `notes-typing` is the only component demo that renders pre-formatted
text, which this does not touch.
2026-08-18 01:37:46 -04:00

1 line
18 KiB
JSON

{"html":"<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=1920, height=1080\" />\n <title>Sheet Spring Up</title>\n <script src=\"https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js\"></script>\n <style>\n html,\n body {\n margin: 0;\n width: 1920px;\n height: 1080px;\n overflow: hidden;\n }\n body {\n background: #101413;\n font-family:\n ui-sans-serif,\n -apple-system,\n \"Segoe UI\",\n sans-serif;\n }\n .hf-ui-demo-canvas {\n position: relative;\n width: 1920px;\n height: 1080px;\n overflow: hidden;\n background:\n radial-gradient(\n ellipse 70% 90% at 26% 30%,\n rgba(60, 230, 172, 0.07) 0%,\n rgba(60, 230, 172, 0) 60%\n ),\n linear-gradient(160deg, #1a201d 0%, #121614 55%, #0e1210 100%);\n }\n /* the device */\n .spec-phone {\n position: absolute;\n left: 430px;\n top: 158px;\n width: 438px;\n height: 764px;\n border-radius: 64px;\n background: #3a3d42;\n box-shadow:\n inset 0 2px 3px rgba(255, 255, 255, 0.22),\n 0 34px 80px rgba(0, 0, 0, 0.55);\n }\n .spec-screen {\n position: absolute;\n left: 14px;\n top: 14px;\n width: 410px;\n height: 736px;\n border-radius: 52px;\n overflow: hidden;\n background: #000;\n }\n /* interior is authored at 1080x1920 and scaled into the screen */\n .spec-int {\n position: absolute;\n left: 0;\n top: 0;\n width: 1080px;\n height: 1920px;\n transform: scale(0.3796);\n transform-origin: 0 0;\n }\n /* the caption column */\n .spec-cap {\n position: absolute;\n left: 1010px;\n top: 300px;\n width: 730px;\n }\n .spec-cap .name {\n font:\n 600 46px/1.1 ui-monospace,\n \"SF Mono\",\n Menlo,\n monospace;\n color: #3ce6ac;\n letter-spacing: -0.5px;\n }\n .spec-cap .rule {\n width: 84px;\n height: 3px;\n background: #3ce6ac;\n margin: 26px 0;\n }\n .spec-cap .desc {\n font:\n 400 21px/1.6 ui-sans-serif,\n -apple-system,\n sans-serif;\n color: #b9c2bd;\n max-width: 620px;\n }\n .spec-cap .stats {\n margin-top: 30px;\n display: grid;\n gap: 12px;\n }\n .spec-cap .stat {\n font:\n 500 16px/1.5 ui-monospace,\n \"SF Mono\",\n Menlo,\n monospace;\n color: #78827d;\n }\n .spec-cap .stat b {\n color: #e8ece9;\n font-weight: 600;\n }\n\n .ssu-ground {\n position: absolute;\n inset: -80px;\n background: linear-gradient(165deg, #0f3a2c 0%, #0a9c6b 70%, #3ce6ac 120%);\n filter: blur(0px);\n }\n .ssu-blob1 {\n position: absolute;\n left: 60px;\n top: 200px;\n width: 600px;\n height: 600px;\n border-radius: 50%;\n background: radial-gradient(circle, rgba(60, 230, 172, 0.5) 0%, rgba(60, 230, 172, 0) 70%);\n filter: blur(30px);\n }\n .ssu-blob2 {\n position: absolute;\n right: -100px;\n top: 900px;\n width: 700px;\n height: 700px;\n border-radius: 50%;\n background: radial-gradient(circle, rgba(233, 171, 85, 0.35) 0%, rgba(233, 171, 85, 0) 70%);\n filter: blur(30px);\n }\n .ssu-card {\n position: absolute;\n left: 106px;\n top: 473px;\n width: 868px;\n height: 1100px;\n background: #f9f9f9;\n border-radius: 44px;\n box-shadow: 0 18px 42px rgba(0, 0, 0, 0.35);\n opacity: 0;\n }\n .ssu-title {\n position: absolute;\n top: 60px;\n left: 0;\n width: 100%;\n text-align: center;\n font-size: 54px;\n font-weight: 600;\n color: #101010;\n }\n .ssu-sub {\n position: absolute;\n top: 145px;\n left: 0;\n width: 100%;\n text-align: center;\n font-size: 36px;\n color: #313131;\n }\n .ssu-sub b {\n font-weight: 700;\n }\n .ssu-prev {\n position: absolute;\n left: 56px;\n top: 235px;\n width: 756px;\n height: 620px;\n border-radius: 30px;\n overflow: hidden;\n background: linear-gradient(165deg, #0f3a2c 0%, #0a9c6b 78%, #3ce6ac 100%);\n }\n .ssu-play {\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n width: 140px;\n height: 140px;\n border-radius: 50%;\n background: rgba(253, 253, 253, 0.94);\n }\n .ssu-play svg {\n position: absolute;\n left: 50px;\n top: 42px;\n }\n .ssu-div {\n position: absolute;\n left: 0;\n top: 920px;\n width: 100%;\n height: 2px;\n background: #dfdfdf;\n }\n .ssu-btn {\n position: absolute;\n top: 922px;\n bottom: 0;\n width: 50%;\n display: grid;\n place-items: center;\n font-size: 42px;\n }\n .ssu-dec {\n left: 0;\n color: #6f6f73;\n }\n .ssu-acc {\n right: 0;\n color: #087a57;\n font-weight: 600;\n }\n </style>\n </head>\n <body>\n <div\n id=\"hf-ssu-demo-root\"\n class=\"hf-ui-demo-canvas\"\n data-composition-id=\"sheet-spring-up-demo\"\n data-start=\"0\"\n data-width=\"1920\"\n data-height=\"1080\"\n data-duration=\"4\"\n data-composition-variables='[\n { \"id\": \"width\", \"type\": \"number\", \"role\": \"style\", \"label\": \"Width\", \"description\": \"Sheet width in pixels.\", \"default\": 668, \"min\": 400, \"max\": 900, \"step\": 4, \"unit\": \"px\" },\n { \"id\": \"radius\", \"type\": \"number\", \"role\": \"style\", \"label\": \"Radius\", \"description\": \"Sheet corner radius.\", \"default\": 34, \"min\": 16, \"max\": 48, \"step\": 1, \"unit\": \"px\" },\n { \"id\": \"tone\", \"type\": \"enum\", \"role\": \"style\", \"label\": \"Tone\", \"description\": \"Light is the reference near-white sheet; dark is the raised surface token.\", \"default\": \"light\", \"options\": [{ \"value\": \"light\", \"label\": \"Light\" }, { \"value\": \"dark\", \"label\": \"Dark\" }] }\n ]'>\n <section\n class=\"clip\"\n data-start=\"0\"\n data-duration=\"4\"\n data-track-index=\"1\"\n style=\"position: absolute; inset: 0\"\n >\n <div class=\"spec-phone\">\n <div class=\"spec-screen\">\n <div class=\"spec-int\">\n <div class=\"ssu-ground\"></div>\n <div class=\"ssu-blob1\"></div>\n <div class=\"ssu-blob2\"></div>\n <div class=\"ssu-card\" id=\"ssu-sheet\">\n <div class=\"ssu-title\">Share</div>\n <div class=\"ssu-sub\"><b>HyperFrames</b> would like to share a video</div>\n <div class=\"ssu-prev\">\n <div class=\"ssu-play\">\n <svg width=\"48\" height=\"54\" viewBox=\"0 0 44 48\" fill=\"none\">\n <path\n d=\"M40.5 20.6 C43.2 22.1 43.2 25.9 40.5 27.4 L6.2 46.6 C3.5 48.1 0.2 46.2 0.2 43.2 L0.2 4.8 C0.2 1.8 3.5 -0.1 6.2 1.4 Z\"\n fill=\"#0d0d0d\"\n />\n </svg>\n </div>\n </div>\n <div class=\"ssu-div\"></div>\n <div class=\"ssu-btn ssu-dec\">Decline</div>\n <div class=\"ssu-btn ssu-acc\">Accept</div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"spec-cap\">\n <div class=\"name\">sheet-spring-up</div>\n <div class=\"rule\"></div>\n <div class=\"desc\">\n A bottom sheet rushes up from below the frame on a real spring sampled frame by frame —\n overshoot, then a long soft settle. No ease approximates it; the table ships.\n </div>\n <div class=\"stats\">\n <div class=\"stat\"><b>22 steps</b> — the sampled y/alpha table</div>\n <div class=\"stat\"><b>192px</b> — overshoot past rest</div>\n <div class=\"stat\"><b>0.72s</b> — launch to settle</div>\n </div>\n </div>\n </section>\n </div>\n\n <script>\n (function () {\n \"use strict\";\n\n var vars =\n window.__hyperframes && window.__hyperframes.getVariables\n ? window.__hyperframes.getVariables()\n : {};\n\n // Unrecognised overrides return to their declared defaults.\n function pick(table, value, fallback) {\n return Object.prototype.hasOwnProperty.call(table, value) ? value : fallback;\n }\n\n function clampNumber(value, min, max, fallback) {\n var n = typeof value === \"number\" ? value : parseFloat(value);\n if (!isFinite(n)) {\n return fallback;\n }\n return Math.min(max, Math.max(min, n));\n }\n\n var tones = {\n light: { surface: \"#f9f9f9\", ink: \"#101010\", inkMuted: \"#313131\" },\n dark: {\n surface: \"var(--_hf-ui-surface-raised)\",\n ink: \"var(--_hf-ui-text-strong)\",\n inkMuted: \"var(--_hf-ui-text-muted)\",\n },\n };\n\n var width = clampNumber(vars.width, 400, 900, 668);\n var radius = clampNumber(vars.radius, 16, 48, 34);\n var tone = tones[pick(tones, vars.tone, \"light\")];\n\n var roots = document.querySelectorAll(\".hf-ui-sheet-spring-up\");\n for (var i = 0; i < roots.length; i += 1) {\n roots[i].style.setProperty(\"--hf-ssu-width\", width + \"px\");\n roots[i].style.setProperty(\"--hf-ssu-radius\", radius + \"px\");\n roots[i].style.setProperty(\"--hf-ssu-surface\", tone.surface);\n roots[i].style.setProperty(\"--hf-ssu-ink\", tone.ink);\n roots[i].style.setProperty(\"--hf-ssu-ink-muted\", tone.inkMuted);\n }\n })();\n</script>\n<script>\n window.__timelines = window.__timelines || {};\n var tl = gsap.timeline({ paused: true });\n /* the measured spring, verbatim */\n var SPRING = [\n [0.0, 1450, 0.0],\n [0.057, 1198, 0.13],\n [0.09, 903, 0.21],\n [0.123, 648, 0.36],\n [0.157, 437, 0.43],\n [0.19, 262, 0.5],\n [0.223, 120, 0.62],\n [0.257, 8, 0.78],\n [0.29, -76, 0.89],\n [0.323, -138, 1.0],\n [0.357, -175, null],\n [0.39, -191, null],\n [0.423, -192, null],\n [0.457, -181, null],\n [0.49, -159, null],\n [0.523, -131, null],\n [0.557, -100, null],\n [0.59, -68, null],\n [0.623, -41, null],\n [0.657, -18, null],\n [0.69, -5, null],\n [0.723, 0, null],\n ];\n var START = 0.6;\n SPRING.forEach(function (s) {\n var p = { y: s[1] };\n if (s[2] !== null) p.autoAlpha = s[2];\n tl.set(\"#ssu-sheet\", p, START + s[0]);\n });\n /* accept press beat */\n tl.set(\"#ssu-sheet\", { y: 3 }, 3.0);\n tl.set(\"#ssu-sheet\", { y: 0 }, 3.33);\n tl.set({}, {}, 4);\n window.__timelines[\"sheet-spring-up-demo\"] = tl;\n </script>\n <style>\n /* hf-ui:tokens:start */\n /* prettier-ignore */\n [data-hf-ui-root] {\n --_hf-ui-accent: var(--hf-ui-accent, #3ce6ac);\n --_hf-ui-action: var(--hf-ui-action, #eeeeee);\n --_hf-ui-action-contrast: var(--hf-ui-action-contrast, #111111);\n --_hf-ui-border: var(--hf-ui-border, #2a2a2a);\n --_hf-ui-border-strong: var(--hf-ui-border-strong, #383838);\n --_hf-ui-canvas: var(--hf-ui-canvas, #0a0a0a);\n --_hf-ui-control-border: var(--hf-ui-control-border, #686868);\n --_hf-ui-control-border-hover: var(--hf-ui-control-border-hover, #777777);\n --_hf-ui-control-height: var(--hf-ui-control-height, 40px);\n --_hf-ui-danger: var(--hf-ui-danger, #ff7878);\n --_hf-ui-data-1: var(--hf-ui-data-1, #3ce6ac);\n --_hf-ui-data-2: var(--hf-ui-data-2, #63a5ff);\n --_hf-ui-data-3: var(--hf-ui-data-3, #e9ab55);\n --_hf-ui-data-4: var(--hf-ui-data-4, #a78bfa);\n --_hf-ui-data-5: var(--hf-ui-data-5, #ff8a75);\n --_hf-ui-duration-drawer-enter: var(--hf-ui-duration-drawer-enter, 360ms);\n --_hf-ui-duration-drawer-exit: var(--hf-ui-duration-drawer-exit, 240ms);\n --_hf-ui-duration-modal-enter: var(--hf-ui-duration-modal-enter, 280ms);\n --_hf-ui-duration-modal-exit: var(--hf-ui-duration-modal-exit, 180ms);\n --_hf-ui-duration-overlay-enter: var(--hf-ui-duration-overlay-enter, 210ms);\n --_hf-ui-duration-overlay-exit: var(--hf-ui-duration-overlay-exit, 140ms);\n --_hf-ui-duration-press: var(--hf-ui-duration-press, 120ms);\n --_hf-ui-duration-state: var(--hf-ui-duration-state, 160ms);\n --_hf-ui-ease-enter: var(--hf-ui-ease-enter, cubic-bezier(0.32, 0.72, 0, 1));\n --_hf-ui-ease-exit: var(--hf-ui-ease-exit, cubic-bezier(0.23, 1, 0.32, 1));\n --_hf-ui-ease-response: var(--hf-ui-ease-response, cubic-bezier(0.23, 1, 0.32, 1));\n --_hf-ui-font-mono: var(--hf-ui-font-mono, ui-monospace, \"SFMono-Regular\", Menlo, Consolas, monospace);\n --_hf-ui-font-sans: var(--hf-ui-font-sans, ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif);\n --_hf-ui-hit-target: var(--hf-ui-hit-target, 44px);\n --_hf-ui-radius-control: var(--hf-ui-radius-control, 5px);\n --_hf-ui-radius-overlay: var(--hf-ui-radius-overlay, 10px);\n --_hf-ui-radius-surface: var(--hf-ui-radius-surface, 7px);\n --_hf-ui-shadow-contact: var(--hf-ui-shadow-contact, 0 8px 18px rgba(0, 0, 0, 0.38));\n --_hf-ui-shadow-inset: var(--hf-ui-shadow-inset, inset 0 1px 0 rgba(255, 255, 255, 0.035));\n --_hf-ui-shadow-modal: var(--hf-ui-shadow-modal, 0 18px 46px rgba(0, 0, 0, 0.52));\n --_hf-ui-space-1: var(--hf-ui-space-1, 4px);\n --_hf-ui-space-12: var(--hf-ui-space-12, 48px);\n --_hf-ui-space-16: var(--hf-ui-space-16, 64px);\n --_hf-ui-space-2: var(--hf-ui-space-2, 8px);\n --_hf-ui-space-3: var(--hf-ui-space-3, 12px);\n --_hf-ui-space-4: var(--hf-ui-space-4, 16px);\n --_hf-ui-space-6: var(--hf-ui-space-6, 24px);\n --_hf-ui-space-8: var(--hf-ui-space-8, 32px);\n --_hf-ui-surface: var(--hf-ui-surface, #141414);\n --_hf-ui-surface-inset: var(--hf-ui-surface-inset, #101010);\n --_hf-ui-surface-raised: var(--hf-ui-surface-raised, #1a1a1a);\n --_hf-ui-text: var(--hf-ui-text, #e5e5e5);\n --_hf-ui-text-faint: var(--hf-ui-text-faint, #7e7e7e);\n --_hf-ui-text-muted: var(--hf-ui-text-muted, #929292);\n --_hf-ui-text-strong: var(--hf-ui-text-strong, #f3f3f3);\n --_hf-ui-warning: var(--hf-ui-warning, #e9ab55);\n --_hf-ui-weight-medium: var(--hf-ui-weight-medium, 500);\n --_hf-ui-weight-regular: var(--hf-ui-weight-regular, 400);\n --_hf-ui-weight-semibold: var(--hf-ui-weight-semibold, 600);\n box-sizing: border-box;\n color-scheme: dark;\n font-family: var(--_hf-ui-font-sans);\n }\n /* prettier-ignore */\n [data-hf-ui-root] *,\n [data-hf-ui-root] *::before,\n [data-hf-ui-root] *::after {\n box-sizing: border-box;\n }\n /* prettier-ignore */\n [data-hf-theme=\"light\"] [data-hf-ui-root],\n [data-hf-ui-root][data-hf-theme=\"light\"] {\n --_hf-ui-accent: var(--hf-ui-accent, #087a57);\n --_hf-ui-action: var(--hf-ui-action, #171717);\n --_hf-ui-action-contrast: var(--hf-ui-action-contrast, #ffffff);\n --_hf-ui-border: var(--hf-ui-border, #e0dfdb);\n --_hf-ui-border-strong: var(--hf-ui-border-strong, #cbc9c3);\n --_hf-ui-canvas: var(--hf-ui-canvas, #f6f5f1);\n --_hf-ui-control-border: var(--hf-ui-control-border, #888888);\n --_hf-ui-control-border-hover: var(--hf-ui-control-border-hover, #6f6e68);\n --_hf-ui-danger: var(--hf-ui-danger, #a61b1b);\n --_hf-ui-data-1: var(--hf-ui-data-1, #087a57);\n --_hf-ui-data-2: var(--hf-ui-data-2, #245ea8);\n --_hf-ui-data-3: var(--hf-ui-data-3, #8a4b00);\n --_hf-ui-data-4: var(--hf-ui-data-4, #6d3fc0);\n --_hf-ui-data-5: var(--hf-ui-data-5, #a83d2d);\n --_hf-ui-shadow-contact: var(--hf-ui-shadow-contact, 0 7px 16px rgba(26, 24, 20, 0.13));\n --_hf-ui-shadow-inset: var(--hf-ui-shadow-inset, inset 0 1px 0 rgba(255, 255, 255, 0.78));\n --_hf-ui-shadow-modal: var(--hf-ui-shadow-modal, 0 18px 46px rgba(26, 24, 20, 0.18));\n --_hf-ui-surface: var(--hf-ui-surface, #ffffff);\n --_hf-ui-surface-inset: var(--hf-ui-surface-inset, #f0efeb);\n --_hf-ui-surface-raised: var(--hf-ui-surface-raised, #ffffff);\n --_hf-ui-text: var(--hf-ui-text, #171717);\n --_hf-ui-text-faint: var(--hf-ui-text-faint, #6f6e68);\n --_hf-ui-text-muted: var(--hf-ui-text-muted, #5f5f5a);\n --_hf-ui-text-strong: var(--hf-ui-text-strong, #0a0a0a);\n --_hf-ui-warning: var(--hf-ui-warning, #8a4b00);\n color-scheme: light;\n }\n /* prettier-ignore */\n [data-hf-ui-root]:focus-visible {\n outline: 2px solid var(--_hf-ui-accent);\n outline-offset: 3px;\n }\n /* hf-ui:tokens:end */\n\n .hf-ui-sheet-spring-up {\n position: relative;\n }\n .hf-ssu-sheet {\n position: relative;\n width: var(--hf-ssu-width, 668px);\n border-radius: var(--hf-ssu-radius, 34px);\n background: var(--hf-ssu-surface, #f9f9f9);\n color: var(--hf-ssu-ink, #101010);\n box-shadow: 0 18px 42px rgba(0, 0, 0, 0.28);\n padding: 49px 43px 43px;\n will-change: opacity, transform;\n }\n .hf-ssu-title {\n text-align: center;\n font-size: 44px;\n font-weight: var(--_hf-ui-weight-semibold);\n line-height: 1;\n letter-spacing: 0.2px;\n }\n .hf-ssu-body {\n margin-top: 24px;\n text-align: center;\n font-size: 29px;\n color: var(--hf-ssu-ink-muted, #313131);\n }\n</style>\n</body>\n</html>\n"}