Skip to content

Commit 3cf268e

Browse files
Merge pull request #2960 from heygen-com/fix/registry-transform-motion
fix(registry): animate mk card offsets with transforms, not top/left
2 parents 7e95630 + ce7d75d commit 3cf268e

4 files changed

Lines changed: 137 additions & 9 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
"generate:catalog-previews": "tsx scripts/generate-catalog-previews.ts",
5454
"package:codex-plugin": "node scripts/package-codex-plugin.mjs",
5555
"upload:docs-images": "bash scripts/upload-docs-images.sh",
56-
"prepare": "test -d .git && lefthook install || true"
56+
"prepare": "test -d .git && lefthook install || true",
57+
"lint:registry-items": "node scripts/lint-registry-items.mjs"
5758
},
5859
"devDependencies": {
5960
"@commitlint/cli": "^20.5.0",

registry/blocks/mk-background/mk-background.html

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,13 +189,16 @@
189189
);
190190
}
191191

192-
/* bar-mask demo: full-bleed -> rounded card over the stage -> full-bleed */
192+
/* bar-mask demo: full-bleed -> rounded card over the stage -> full-bleed.
193+
Offset via x/y transforms, not top/left: layout properties snap to
194+
integer device pixels and stutter under seek-by-frame capture. The
195+
card's CSS base is top:0/left:0, so the transform values match 1:1. */
193196
if (CONFIG.bar.demo) {
194197
tl.to(
195198
card,
196199
{
197-
top: CONFIG.bar.y,
198-
left: CONFIG.bar.x,
200+
y: CONFIG.bar.y,
201+
x: CONFIG.bar.x,
199202
width: CONFIG.bar.width,
200203
height: CONFIG.bar.height,
201204
borderRadius: CONFIG.bar.roundness,
@@ -207,8 +210,8 @@
207210
tl.to(
208211
card,
209212
{
210-
top: 0,
211-
left: 0,
213+
y: 0,
214+
x: 0,
212215
width: W,
213216
height: H,
214217
borderRadius: 0,

registry/blocks/mk-clone-wall-transition/mk-clone-wall-transition.html

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,16 @@
182182
/* The wall sits in place from t=0, fully hidden beneath the
183183
full-bleed card — the card's shrink IS the wall's reveal. */
184184

185-
/* 0.2–0.95 outgoing frame shrinks to a rounded card on the wall */
185+
/* 0.2–0.95 outgoing frame shrinks to a rounded card on the wall.
186+
x/y rather than left/top — layout properties snap to integer device
187+
pixels under seek-by-frame capture. Base is top:0/left:0, so the
188+
values carry over unchanged, and the later scale composes cleanly. */
186189
if (CONFIG.card.enabled) {
187190
tl.to(
188191
card,
189192
{
190-
top: 330,
191-
left: 640,
193+
y: 330,
194+
x: 640,
192195
width: 640,
193196
height: 420,
194197
borderRadius: 40,

scripts/lint-registry-items.mjs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Lint registry blocks/components the way a user actually receives them.
4+
*
5+
* `hyperframes lint <dir>` needs an `index.html`, but registry items ship as
6+
* `<name>.html`, so pointing the linter at an item directory fails with "No
7+
* composition found" — which means registry items were never linted at all.
8+
* Two `gsap_non_transform_motion` errors reached main that way.
9+
*
10+
* This mounts each item into a throwaway project (exactly where `hyperframes
11+
* add` would put it) and lints that, reporting only findings for the item's
12+
* own file so the host scaffold's noise is ignored.
13+
*
14+
* Usage:
15+
* node scripts/lint-registry-items.mjs # every item
16+
* node scripts/lint-registry-items.mjs mk-background … # named items
17+
*/
18+
import {
19+
readdirSync,
20+
existsSync,
21+
mkdtempSync,
22+
mkdirSync,
23+
copyFileSync,
24+
writeFileSync,
25+
rmSync,
26+
} from "node:fs";
27+
import { join, resolve, dirname } from "node:path";
28+
import { tmpdir } from "node:os";
29+
import { fileURLToPath } from "node:url";
30+
import { spawnSync } from "node:child_process";
31+
32+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
33+
const cli = join(repoRoot, "packages/cli/src/cli.ts");
34+
35+
// A deliberately boring host: one clip, one registered timeline, so the only
36+
// findings that can appear are the item's own.
37+
const HOST = `<!doctype html>
38+
<html lang="en">
39+
<head>
40+
<meta charset="UTF-8" />
41+
<meta name="viewport" content="width=1920, height=1080" />
42+
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
43+
</head>
44+
<body>
45+
<div id="root" data-composition-id="main" data-start="0" data-width="1920" data-height="1080" data-duration="5">
46+
<section id="host-slot" class="clip" data-start="0" data-duration="5" data-track-index="1"></section>
47+
</div>
48+
<script>
49+
window.__timelines = window.__timelines || {};
50+
const tl = gsap.timeline({ paused: true });
51+
tl.to("#host-slot", { opacity: 1, duration: 0.1 }, 0);
52+
window.__timelines["main"] = tl;
53+
</script>
54+
</body>
55+
</html>
56+
`;
57+
58+
/**
59+
* Rules that assume a standalone composition. Many components ship as
60+
* paste-able fragments with no root element at all, so these fire on ~47
61+
* long-standing items and would drown the signal. The host provides the root;
62+
* the item is not supposed to.
63+
*/
64+
const IGNORED = [
65+
"root_missing_composition_id",
66+
"root_missing_dimensions",
67+
"multiple_root_compositions",
68+
];
69+
70+
function discover() {
71+
const out = [];
72+
for (const kind of ["blocks", "components"]) {
73+
const dir = join(repoRoot, "registry", kind);
74+
if (!existsSync(dir)) continue;
75+
for (const name of readdirSync(dir)) {
76+
const html = join(dir, name, `${name}.html`);
77+
if (existsSync(html)) out.push({ name, kind, html });
78+
}
79+
}
80+
return out;
81+
}
82+
83+
const only = process.argv.slice(2);
84+
const items = discover().filter((i) => only.length === 0 || only.includes(i.name));
85+
if (items.length === 0) {
86+
console.error(
87+
only.length ? `No registry item matches: ${only.join(", ")}` : "No registry items found.",
88+
);
89+
process.exit(1);
90+
}
91+
92+
let failed = 0;
93+
for (const item of items) {
94+
const proj = mkdtempSync(join(tmpdir(), `hf-lint-${item.name}-`));
95+
try {
96+
mkdirSync(join(proj, "compositions"), { recursive: true });
97+
writeFileSync(join(proj, "index.html"), HOST);
98+
copyFileSync(item.html, join(proj, "compositions", `${item.name}.html`));
99+
100+
const res = spawnSync("bun", [cli, "lint", proj], { encoding: "utf-8" });
101+
const lines = `${res.stdout ?? ""}${res.stderr ?? ""}`.split("\n");
102+
// Only the item's own file — the host scaffold is not under review.
103+
const hits = lines.filter(
104+
(l) =>
105+
l.includes("✗") && l.includes(`${item.name}.html`) && !IGNORED.some((r) => l.includes(r)),
106+
);
107+
if (hits.length) {
108+
failed++;
109+
console.error(`\n✗ ${item.kind}/${item.name}`);
110+
for (const h of hits) console.error(` ${h.trim()}`);
111+
}
112+
} finally {
113+
rmSync(proj, { recursive: true, force: true });
114+
}
115+
}
116+
117+
if (failed) {
118+
console.error(`\n${failed} registry item(s) have lint errors.`);
119+
process.exit(1);
120+
}
121+
console.log(`Registry item lint passed — ${items.length} item(s), 0 errors.`);

0 commit comments

Comments
 (0)