Skip to content

Commit 2a06843

Browse files
committed
fix(docs): keep catalog previews wired to their payloads
1 parent 2d98d5c commit 2a06843

7 files changed

Lines changed: 159 additions & 7 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,8 @@ jobs:
302302
- name: Keep catalog source PRs source-only
303303
if: github.event_name == 'pull_request'
304304
run: node scripts/check-catalog-source-pr.mjs origin/main
305+
- name: Check published catalog preview wiring
306+
run: node scripts/check-catalog-detail-previews.mjs
305307
- name: Generate and validate catalog
306308
env:
307309
PUBLISH_PR: ${{ (github.head_ref || github.ref_name) == 'bot/catalog-publish' }}

‎docs/catalog/blocks/lottie-character-walk.mdx‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@ import { CatalogDetail, CatalogSlot } from "/snippets/catalog-detail.jsx";
88
import { InstallCommand } from "/snippets/install-command.jsx";
99

1010
<CatalogDetail
11+
previewSrc="/public/catalog/blocks/lottie-character-walk.json"
1112
compositionId="lottie-character-walk"
1213
compositionSrc="compositions/lottie-character-walk/lottie-character-walk.html"
1314
title="Lottie Character Walk"
1415
description="A jointed flat character walks in on planted feet, stops and points at a card; the body acting is a Lottie file and the stage is GSAP."
15-
variables={[]}
16-
meta={{"duration":7.5,"width":1920,"height":1080,"category":"Blocks","badge":"Stable","codeLines":137}}
16+
variables={[{"id":"title","type":"string","label":"Card title","default":"Meet your guide","maxLength":28},{"id":"body","type":"string","label":"Card text","default":"Walks in on planted feet, then points the way.","maxLength":90}]}
17+
meta={{"duration":7.5,"width":1920,"height":1080,"category":"Blocks","badge":"Stable","codeLines":142}}
1718
attribution={{"path":"registry/blocks/lottie-character-walk","tags":["character","character-animation","mascot","walk-cycle","gesture","pointing","puppet","lottie","explainer"]}}
18-
video="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lottie-character-walk.mp4"
19-
poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lottie-character-walk.png"
2019
hasCode
2120
rawUrl="https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry/blocks/lottie-character-walk/lottie-character-walk.html"
2221
>
@@ -126,14 +125,19 @@ import { InstallCommand } from "/snippets/install-command.jsx";
126125
if (vars.title) document.getElementById("lcw-title").textContent = vars.title;
127126
if (vars.body) document.getElementById("lcw-body").textContent = vars.body;
128127
128+
// Beside this file when mounted; a runtime without assetUrl keeps the plain path.
129+
const assetUrl = (path) =>
130+
typeof __hyperframes !== "undefined" && __hyperframes.assetUrl
131+
? __hyperframes.assetUrl(path)
132+
: path;
129133
// The character's body acting (walk, stop, point, idle) lives in the Lottie file.
130134
// The runtime seeks every player on window.__hfLottie to composition time.
131135
var mascot = lottie.loadAnimation({
132136
container: document.getElementById("lcw-mascot"),
133137
renderer: "svg",
134138
loop: false,
135139
autoplay: false,
136-
path: "assets/mascot.json",
140+
path: assetUrl("assets/mascot.json"),
137141
});
138142
window.__hfLottie = window.__hfLottie || [];
139143
window.__hfLottie.push(mascot);

‎docs/catalog/components/colorama-wipe.mdx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import { CatalogDetail, CatalogSlot } from "/snippets/catalog-detail.jsx";
88
import { InstallCommand } from "/snippets/install-command.jsx";
99

1010
<CatalogDetail
11+
previewSrc="/public/catalog/components/colorama-wipe.json"
1112
compositionId="colorama-wipe"
1213
compositionSrc="compositions/components/colorama-wipe.html"
1314
title="Colorama Wipe"
1415
description="Headline reveal through a feathered, leaning wipe front that is blurred and colour-mapped as it passes, then settles to clean type and wipes off again — the After Effects masked-text + Compound Blur + Colorama 'shiny wipe' as a deterministic canvas effect on any text element"
1516
variables={[]}
16-
meta={{"category":"Typography & Text","badge":"Stable","codeLines":352}}
17+
meta={{"category":"Typography & Text","badge":"Stable","codeLines":358}}
1718
attribution={{"path":"registry/components/colorama-wipe","tags":["text","reveal","wipe","colorama","gradient","blur","headline","saas"]}}
1819
hasCode
1920
rawUrl="https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry/components/colorama-wipe/colorama-wipe.html"
@@ -354,6 +355,11 @@ import { InstallCommand } from "/snippets/install-command.jsx";
354355
// one linear clock tween at `at`; at progress 0 the mask shows nothing, at progress 1 the exit has closed,
355356
// so a seek anywhere outside the window still paints an empty stage
356357
var clock = { t: 0 };
358+
function cleanup() {
359+
stage.remove();
360+
el.classList.remove("cw-src");
361+
delete el.__cw;
362+
}
357363
draw(0);
358364
tl.fromTo(
359365
clock,
@@ -365,6 +371,7 @@ import { InstallCommand } from "/snippets/install-command.jsx";
365371
onUpdate: function () {
366372
draw(clock.t);
367373
},
374+
onInterrupt: cleanup,
368375
},
369376
o.at,
370377
);

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"player:perf": "bun run --filter @hyperframes/player perf",
5353
"format:check": "oxfmt --check .",
5454
"knip": "knip",
55-
"test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-registry-set-delta.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-comment-citations.test.mjs scripts/comments-workflow.test.mjs scripts/comment-ratchet.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-media-use-copy-parity.test.mjs scripts/check-svg-sanitize-parity.test.mjs scripts/check-media-use-svg-sanitize-generated.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/prune-fallow-audit-caches.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/clawhub-sync.test.mjs scripts/publish-workflow.test.mjs scripts/pr-edit-concurrency.test.mjs scripts/merge-queue-workflows.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/backfill-block-previews.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-publication.test.mjs scripts/ci/resolve-workflow-pr.test.mjs scripts/ci/base-file.test.mjs scripts/check-catalog-source-pr.test.mjs scripts/generate-registry-items.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-skill-files.test.ts scripts/creator-editing-capabilities.test.mjs scripts/generate-catalog-previews.test.ts scripts/registry-primitive-payloads.test.ts registry/components/pan-stations/pan-stations.test.mjs && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
55+
"test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-registry-set-delta.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-comment-citations.test.mjs scripts/comments-workflow.test.mjs scripts/comment-ratchet.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-media-use-copy-parity.test.mjs scripts/check-svg-sanitize-parity.test.mjs scripts/check-media-use-svg-sanitize-generated.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/prune-fallow-audit-caches.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/clawhub-sync.test.mjs scripts/publish-workflow.test.mjs scripts/pr-edit-concurrency.test.mjs scripts/merge-queue-workflows.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/backfill-block-previews.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-publication.test.mjs scripts/ci/resolve-workflow-pr.test.mjs scripts/ci/base-file.test.mjs scripts/check-catalog-source-pr.test.mjs scripts/generate-registry-items.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/catalog-detail-preview.test.mjs scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-skill-files.test.ts scripts/creator-editing-capabilities.test.mjs scripts/generate-catalog-previews.test.ts scripts/registry-primitive-payloads.test.ts registry/components/pan-stations/pan-stations.test.mjs && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
5656
"typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json",
5757
"test:skills": "HYPERFRAMES_MEDIA_HOME_REQUIRED=1 node --import ./packages/cli/scripts/test-home.mjs --test 'skills/**/*.test.mjs' 'packages/cli/src/media-use/**/*.test.mjs'",
5858
"generate:previews": "tsx scripts/generate-template-previews.ts",
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import assert from "node:assert/strict";
2+
import { test } from "node:test";
3+
import { spawnSync } from "node:child_process";
4+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
5+
import { tmpdir } from "node:os";
6+
import { join } from "node:path";
7+
import { fileURLToPath } from "node:url";
8+
import { assertDetailPreview } from "./docs-catalog-shared.mjs";
9+
10+
const source = "/public/catalog/blocks/character.json";
11+
const payload = { html: "<html><body>character</body></html>" };
12+
13+
test("a playable payload must be wired into its detail page", () => {
14+
assert.throws(
15+
() => assertDetailPreview('<CatalogDetail\n video="character.mp4"\n>', payload, source),
16+
/detail previewSrc must match/,
17+
);
18+
});
19+
20+
test("a detail page cannot mount another item's payload", () => {
21+
assert.throws(
22+
() => assertDetailPreview('<CatalogDetail\n previewSrc="/other.json"\n>', payload, source),
23+
/detail previewSrc must match/,
24+
);
25+
});
26+
27+
test("a recorded clip cannot preempt a playable non-WebGPU composition", () => {
28+
assert.throws(
29+
() =>
30+
assertDetailPreview(
31+
`<CatalogDetail\n previewSrc="${source}"\n video="character.mp4"\n>`,
32+
payload,
33+
source,
34+
),
35+
/recorded video hides/,
36+
);
37+
});
38+
39+
test("a matching playable preview is accepted", () => {
40+
assertDetailPreview(`<CatalogDetail\n previewSrc="${source}"\n>`, payload, source);
41+
});
42+
43+
test("a WebGPU preview can keep its recorded fallback", () => {
44+
assertDetailPreview(
45+
`<CatalogDetail\n previewSrc="${source}"\n video="character.mp4"\n webgpu\n>`,
46+
{ html: "navigator.gpu.requestAdapter()" },
47+
source,
48+
);
49+
});
50+
51+
test("a WebGPU recorded fallback must enable the adapter check", () => {
52+
assert.throws(
53+
() =>
54+
assertDetailPreview(
55+
`<CatalogDetail\n previewSrc="${source}"\n video="character.mp4"\n>`,
56+
{ html: "navigator.gpu.requestAdapter()" },
57+
source,
58+
),
59+
/recorded video hides/,
60+
);
61+
});
62+
63+
test("an unsupported preview keeps its recorded clip without mounting a payload", () => {
64+
assertDetailPreview(
65+
'<CatalogDetail\n video="character.mp4"\n>',
66+
{ unsupported: "canvas-draw-element" },
67+
source,
68+
);
69+
});
70+
71+
test("a page cannot mount a missing payload", () => {
72+
assert.throws(
73+
() => assertDetailPreview(`<CatalogDetail\n previewSrc="${source}"\n>`, {}, source),
74+
/detail previewSrc must match/,
75+
);
76+
});
77+
78+
test("the CI command rejects a stale published page and accepts its repaired wiring", (t) => {
79+
const docs = mkdtempSync(join(tmpdir(), "catalog-preview-check-"));
80+
t.after(() => rmSync(docs, { recursive: true, force: true }));
81+
for (const directory of ["catalog/blocks", "catalog/components", "public/catalog/blocks"]) {
82+
mkdirSync(join(docs, directory), { recursive: true });
83+
}
84+
const page = join(docs, "catalog/blocks/character.mdx");
85+
writeFileSync(join(docs, source.slice(1)), JSON.stringify(payload));
86+
writeFileSync(page, '<CatalogDetail\n video="character.mp4"\n>');
87+
const check = () =>
88+
spawnSync(
89+
process.execPath,
90+
[fileURLToPath(new URL("./check-catalog-detail-previews.mjs", import.meta.url)), docs],
91+
{ encoding: "utf8" },
92+
);
93+
const stale = check();
94+
assert.equal(stale.status, 1);
95+
assert.match(stale.stderr, /detail previewSrc must match/);
96+
writeFileSync(page, `<CatalogDetail\n previewSrc="${source}"\n>`);
97+
const repaired = check();
98+
assert.equal(repaired.status, 0, repaired.stderr);
99+
assert.match(repaired.stdout, /PASS 1 catalog detail previews/);
100+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { existsSync, readdirSync, readFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
import { assertDetailPreview, readJson, resolveDocsRoot } from "./docs-catalog-shared.mjs";
4+
5+
const { docs } = resolveDocsRoot(process.argv[2]);
6+
let checked = 0;
7+
for (const kind of ["blocks", "components"]) {
8+
const pages = join(docs, "catalog", kind);
9+
for (const name of readdirSync(pages).filter((name) => name.endsWith(".mdx"))) {
10+
const source = `/public/catalog/${kind}/${name.slice(0, -4)}.json`;
11+
const payload = join(docs, source.slice(1));
12+
assertDetailPreview(
13+
readFileSync(join(pages, name), "utf8"),
14+
existsSync(payload) ? readJson(payload) : {},
15+
source,
16+
);
17+
checked += 1;
18+
}
19+
}
20+
console.log(`PASS ${checked} catalog detail previews match their published payloads.`);

‎scripts/docs-catalog-shared.mjs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "node:fs";
22
import path from "node:path";
33
import { fileURLToPath } from "node:url";
4+
import assert from "node:assert/strict";
45

56
export function resolveDocsRoot(argv2) {
67
const root = fileURLToPath(new URL("..", import.meta.url));
@@ -11,6 +12,24 @@ export function readJson(p) {
1112
return JSON.parse(fs.readFileSync(p, "utf8"));
1213
}
1314

15+
export function assertDetailPreview(page, payload, source) {
16+
const opening = page.match(/<CatalogDetail\b([\s\S]*?)\n>/)?.[1];
17+
assert.ok(opening !== undefined, `${source}: missing CatalogDetail opening`);
18+
const previewSrc = opening.match(/\bpreviewSrc="([^"]*)"/)?.[1];
19+
const playable = typeof payload.html === "string" && payload.html.length > 0;
20+
assert.equal(
21+
previewSrc,
22+
playable ? source : undefined,
23+
`${source}: detail previewSrc must match its playable payload`,
24+
);
25+
if (playable && /\n\s+video=/.test(opening)) {
26+
assert.ok(
27+
usesWebgpu(payload.html) && /\n\s+webgpu(?:\s|$)/.test(opening),
28+
`${source}: recorded video hides the live payload without a WebGPU fallback`,
29+
);
30+
}
31+
}
32+
1433
export function readCatalogGalleryData(docs) {
1534
const raw = fs.readFileSync(path.join(docs, "snippets/catalog-gallery-data.mdx"), "utf8");
1635
return JSON.parse(raw.replace(/^export const catalogGalleryData = /, "").replace(/;\s*$/, ""));

0 commit comments

Comments
 (0)