mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 16:42:27 +00:00
fix(cli): keep overlap waivers local to marked text (#3464)
* fix(cli): scope overlap waiver to marked text * fix(skills): guard changelog caption rail * fix(skills): densify changelog caption checks * test(skills): satisfy strict seek typing
This commit is contained in:
@@ -145,9 +145,10 @@ scaffold.
|
||||
|
||||
### 6 · Gates (all green before presenting)
|
||||
|
||||
1. `bun run --cwd packages/cli hyperframes check` (or the installed
|
||||
1. `bun run --cwd packages/cli hyperframes check --caption-zone "x0=0;y0=.90;x1=1;y1=1;severity=error;seek=.02,.06,.10,.14,.18,.22,.26,.30,.34,.38,.42,.46,.50,.54,.58,.62,.66,.70,.74,.78,.82,.86,.90,.94,.98"` (or the installed
|
||||
`hyperframes` CLI from the repo-local `skills/hyperframes-cli/` skill) —
|
||||
0 errors (contrast: dim text ≥ .66 alpha). Do NOT reach for
|
||||
0 errors (contrast: dim text ≥ .66 alpha; scene content stays above the
|
||||
caption rail). Do NOT reach for
|
||||
`npx hyperframes@latest`; the tracked repo-local CLI is the source of
|
||||
truth for the composition contract this skill produces against.
|
||||
2. `seam-gate.mjs verify` — 0 fail.
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<div id="bg-scrim"></div>
|
||||
<div id="kicker-chip" class="chrome">HYPERFRAMES WEEKLY <span style="color:rgba(245,246,244,.66)">· <RANGE></span></div>
|
||||
<div id="progress" class="chrome"><span class="dot" id="dot1"></span><!-- one per theme --></div>
|
||||
<div id="cap-line" class="chrome" data-layout-allow-overlap></div>
|
||||
<div id="cap-line" class="chrome" data-layout-allow-caption-zone></div>
|
||||
|
||||
<!-- Title (film open — authors its OWN entry; exit is stamped) -->
|
||||
<div id="s-title" class="slide clip" data-start="0" data-duration="<CUT1>" data-track-index="1">
|
||||
@@ -57,8 +57,9 @@
|
||||
|
||||
<!-- One slide per theme. data-start = EXACTLY the ledger cut time.
|
||||
Shell composed at local t=0; internal beats ≥0.4s after the cut,
|
||||
done ≥0.45s before the next. data-layout-allow-overlap on mock slides. -->
|
||||
<div id="s-t1" class="slide clip" data-start="<CUT1>" data-duration="<DUR1>" data-track-index="1" data-layout-allow-overlap>
|
||||
done ≥0.45s before the next. Put data-layout-allow-overlap only on
|
||||
the specific text blocks that deliberately layer. -->
|
||||
<div id="s-t1" class="slide clip" data-start="<CUT1>" data-duration="<DUR1>" data-track-index="1">
|
||||
<div class="sec-chip">01 · <THEME></div>
|
||||
<div class="sec-head"><HEADLINE></div>
|
||||
<!-- the mock, from references/visualization-registry.md, y ∈ [288, 944] -->
|
||||
|
||||
@@ -102,8 +102,8 @@ otherwise) and must stay flat 2D (no 3D ancestors).
|
||||
## Lint/check gotchas (all hit before, all pre-solved)
|
||||
|
||||
- Mock containers with intentional stacking: `data-layout-allow-overlap` on
|
||||
the slide root; elements a playhead/line crosses:
|
||||
`data-layout-allow-occlusion`.
|
||||
each text block that deliberately participates, never on the slide root;
|
||||
elements a playhead/line crosses: `data-layout-allow-occlusion`.
|
||||
- Dim text: `rgba(245,246,244,.66)` minimum (contrast gate).
|
||||
- Audio: every `<audio>` carries an `id`. BGM: the house track ships at
|
||||
`<SKILL_DIR>/assets/bgm.mp3` (159s instrumental) — copy it to the project
|
||||
|
||||
@@ -145,9 +145,10 @@ scaffold.
|
||||
|
||||
### 6 · Gates (all green before presenting)
|
||||
|
||||
1. `bun run --cwd packages/cli hyperframes check` (or the installed
|
||||
1. `bun run --cwd packages/cli hyperframes check --caption-zone "x0=0;y0=.90;x1=1;y1=1;severity=error;seek=.02,.06,.10,.14,.18,.22,.26,.30,.34,.38,.42,.46,.50,.54,.58,.62,.66,.70,.74,.78,.82,.86,.90,.94,.98"` (or the installed
|
||||
`hyperframes` CLI from the repo-local `skills/hyperframes-cli/` skill) —
|
||||
0 errors (contrast: dim text ≥ .66 alpha). Do NOT reach for
|
||||
0 errors (contrast: dim text ≥ .66 alpha; scene content stays above the
|
||||
caption rail). Do NOT reach for
|
||||
`npx hyperframes@latest`; the tracked repo-local CLI is the source of
|
||||
truth for the composition contract this skill produces against.
|
||||
2. `seam-gate.mjs verify` — 0 fail.
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<div id="bg-scrim"></div>
|
||||
<div id="kicker-chip" class="chrome">HYPERFRAMES WEEKLY <span style="color:rgba(245,246,244,.66)">· <RANGE></span></div>
|
||||
<div id="progress" class="chrome"><span class="dot" id="dot1"></span><!-- one per theme --></div>
|
||||
<div id="cap-line" class="chrome" data-layout-allow-overlap></div>
|
||||
<div id="cap-line" class="chrome" data-layout-allow-caption-zone></div>
|
||||
|
||||
<!-- Title (film open — authors its OWN entry; exit is stamped) -->
|
||||
<div id="s-title" class="slide clip" data-start="0" data-duration="<CUT1>" data-track-index="1">
|
||||
@@ -57,8 +57,9 @@
|
||||
|
||||
<!-- One slide per theme. data-start = EXACTLY the ledger cut time.
|
||||
Shell composed at local t=0; internal beats ≥0.4s after the cut,
|
||||
done ≥0.45s before the next. data-layout-allow-overlap on mock slides. -->
|
||||
<div id="s-t1" class="slide clip" data-start="<CUT1>" data-duration="<DUR1>" data-track-index="1" data-layout-allow-overlap>
|
||||
done ≥0.45s before the next. Put data-layout-allow-overlap only on
|
||||
the specific text blocks that deliberately layer. -->
|
||||
<div id="s-t1" class="slide clip" data-start="<CUT1>" data-duration="<DUR1>" data-track-index="1">
|
||||
<div class="sec-chip">01 · <THEME></div>
|
||||
<div class="sec-head"><HEADLINE></div>
|
||||
<!-- the mock, from references/visualization-registry.md, y ∈ [288, 944] -->
|
||||
|
||||
@@ -102,8 +102,8 @@ otherwise) and must stay flat 2D (no 3D ancestors).
|
||||
## Lint/check gotchas (all hit before, all pre-solved)
|
||||
|
||||
- Mock containers with intentional stacking: `data-layout-allow-overlap` on
|
||||
the slide root; elements a playhead/line crosses:
|
||||
`data-layout-allow-occlusion`.
|
||||
each text block that deliberately participates, never on the slide root;
|
||||
elements a playhead/line crosses: `data-layout-allow-occlusion`.
|
||||
- Dim text: `rgba(245,246,244,.66)` minimum (contrast gate).
|
||||
- Audio: every `<audio>` carries an `id`. BGM: the house track ships at
|
||||
`<SKILL_DIR>/assets/bgm.mp3` (159s instrumental) — copy it to the project
|
||||
|
||||
@@ -646,7 +646,10 @@ When the audit is wrong because the layout is deliberate, say so in the HTML and
|
||||
re-run. Put `data-layout-allow-overflow` on a planned off-canvas entrance,
|
||||
`data-layout-allow-overlap` on text you meant to stack, `data-layout-allow-occlusion`
|
||||
on text you meant to hide behind a prop, and `data-layout-ignore` on decoration
|
||||
that should not be audited at all. Each is inherited, so an ancestor works.
|
||||
that should not be audited at all. Overflow, occlusion, and ignore markers are
|
||||
inherited, so an ancestor works. The overlap marker is intentionally local: put it
|
||||
on the specific text block participating in the layering, never on a scene/root
|
||||
wrapper, so unrelated descendant collisions remain auditable.
|
||||
|
||||
For deliberate lower-third copy under `--caption-zone`, use
|
||||
`data-layout-allow-caption-zone`. It silences `caption_zone_collision` and
|
||||
|
||||
@@ -64,6 +64,9 @@ legitimate — a caption designed to sit behind a matted subject needs one.
|
||||
|
||||
Two things about them are worth knowing before you reach for either:
|
||||
|
||||
- **`data-layout-allow-overlap` is local to the marked text block.** Do not put it on a
|
||||
scene/root wrapper to waive a whole mock slide. Mark only the text that deliberately
|
||||
participates in the layering; every unrelated descendant collision remains auditable.
|
||||
- **`data-layout-allow-occlusion` also silences the WCAG contrast gate for that whole
|
||||
subtree.** Validating this guide's confetti example, moving the attribute onto a cluster
|
||||
root took contrast coverage from 73 checks to 13 — and a deliberately near-invisible
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// @vitest-environment node
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const REPO_ROOT = join(fileURLToPath(new URL(".", import.meta.url)), "..", "..", "..", "..");
|
||||
const read = (...parts: string[]): string => readFileSync(join(REPO_ROOT, ...parts), "utf8");
|
||||
|
||||
describe("changelog-video layout contract", () => {
|
||||
const buildSpec = read(".claude", "skills", "changelog-video", "references", "build-spec.md");
|
||||
const skill = read(".claude", "skills", "changelog-video", "SKILL.md");
|
||||
const skeleton = read(".claude", "skills", "changelog-video", "examples", "master-skeleton.html");
|
||||
|
||||
it("keeps overlap waivers local instead of disabling a whole slide audit", () => {
|
||||
expect(buildSpec).not.toMatch(/data-layout-allow-overlap` on\s+the slide root/);
|
||||
expect(buildSpec).toContain("never on the slide root");
|
||||
expect(skeleton).not.toMatch(/class="slide[^"]*"[^>]*data-layout-allow-overlap/);
|
||||
});
|
||||
|
||||
it("checks scene content against the caption rail while exempting the rail itself", () => {
|
||||
const seekList = /--caption-zone "[^"]*seek=([.\d,]+)"/.exec(skill)?.[1];
|
||||
const seeks = seekList?.split(",").map(Number) ?? [];
|
||||
expect(seeks).toHaveLength(25);
|
||||
expect(
|
||||
Math.max(...seeks.slice(1).map((seek, index) => seek - (seeks[index] ?? Number.NaN))) * 60,
|
||||
).toBeLessThanOrEqual(2.401);
|
||||
expect(skeleton).toMatch(/id="cap-line"[^>]*data-layout-allow-caption-zone/);
|
||||
});
|
||||
});
|
||||
@@ -530,6 +530,49 @@ it("skips caption_zone_collision when data-layout-allow-caption-zone is set", as
|
||||
expect(report.layout.findings).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps overlap waivers from suppressing changelog caption-rail collisions", async () => {
|
||||
const collectGeometryCandidates = vi.fn(async (time: number) => [
|
||||
geometryCandidate({
|
||||
kind: "text",
|
||||
tag: "div",
|
||||
text: "Release card copy",
|
||||
selector: "#release-card",
|
||||
rect: fixtureRect(120, 970, 840, 118),
|
||||
time,
|
||||
dataAttributes: { "data-layout-allow-overlap": "" },
|
||||
}),
|
||||
geometryCandidate({
|
||||
kind: "text",
|
||||
tag: "div",
|
||||
text: "Intentional caption rail",
|
||||
selector: "#cap-line",
|
||||
rect: fixtureRect(0, 990, 1080, 52),
|
||||
time,
|
||||
dataAttributes: { "data-layout-allow-caption-zone": "" },
|
||||
}),
|
||||
]);
|
||||
const { report } = await runScenario(
|
||||
fakeDriver({
|
||||
getCanvas: vi.fn(async () => ({ width: 1080, height: 1080 })),
|
||||
collectGeometryCandidates,
|
||||
}),
|
||||
{
|
||||
samples: 1,
|
||||
contrast: false,
|
||||
captionZone: { x0: 0, y0: 0.9, x1: 1, y1: 1, severity: "error" },
|
||||
},
|
||||
);
|
||||
|
||||
expect(report.layout.findings).toEqual([
|
||||
expect.objectContaining({
|
||||
code: "caption_zone_collision",
|
||||
severity: "error",
|
||||
selector: "#release-card",
|
||||
}),
|
||||
]);
|
||||
expect(report.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("filters caption candidates by the element box while centering the text rect", async () => {
|
||||
const collectGeometryCandidates = vi.fn(async (time: number) => [
|
||||
geometryCandidate({
|
||||
|
||||
@@ -545,7 +545,10 @@
|
||||
}
|
||||
|
||||
function hasAllowOverlapFlag(element) {
|
||||
return !!element.closest("[data-layout-allow-overlap]");
|
||||
// Overlap intent belongs to the text block that participates in the
|
||||
// layering. Inheriting this marker from a scene/root would turn one local
|
||||
// waiver into a blanket exemption for every collision in that subtree.
|
||||
return element.hasAttribute("data-layout-allow-overlap");
|
||||
}
|
||||
|
||||
function isTransparentColor(color) {
|
||||
|
||||
@@ -1195,6 +1195,16 @@ describe("layout-audit.browser content overlap", () => {
|
||||
expectExemptFromOverlap({ attrs: "data-layout-allow-overlap" });
|
||||
});
|
||||
|
||||
it("does not let a parent allow-overlap marker disable every descendant collision", () => {
|
||||
const issues = auditOverlapScene({
|
||||
rootAttrs: "data-layout-allow-overlap",
|
||||
a: { textRect: rect({ left: 100, top: 100, width: 400, height: 100 }) },
|
||||
b: { textRect: rect({ left: 300, top: 120, width: 400, height: 100 }) },
|
||||
});
|
||||
|
||||
expect(issues.some((issue) => issue.code === "content_overlap")).toBe(true);
|
||||
});
|
||||
|
||||
// A typewriter span clipped to nothing (clip-path: inset(0 100% 0 0)) keeps a
|
||||
// normal box but paints zero pixels; overlapping it must not flag the visible
|
||||
// block beneath. The clipped element is unreachable by elementFromPoint, which
|
||||
@@ -1609,11 +1619,12 @@ function expectExemptFromOverlap(aOverrides: { color?: string; attrs?: string })
|
||||
}
|
||||
|
||||
function auditOverlapScene(options: {
|
||||
rootAttrs?: string;
|
||||
a: { textRect: DOMRect | DOMRect[]; color?: string; attrs?: string; clipPath?: string };
|
||||
b: { textRect: DOMRect | DOMRect[]; color?: string; attrs?: string; clipPath?: string };
|
||||
}): ReturnType<typeof runAudit> {
|
||||
document.body.innerHTML = `
|
||||
<div id="root" data-composition-id="main" data-width="1920" data-height="1080">
|
||||
<div id="root" data-composition-id="main" data-width="1920" data-height="1080" ${options.rootAttrs ?? ""}>
|
||||
<div id="a" ${options.a.attrs ?? ""}>Block A copy</div>
|
||||
<div id="b" ${options.b.attrs ?? ""}>Block B copy</div>
|
||||
</div>
|
||||
@@ -1631,6 +1642,16 @@ function auditOverlapScene(options: {
|
||||
b: normalizeTextRects(options.b.textRect),
|
||||
};
|
||||
|
||||
installOverlapStyles(colors, clipPaths);
|
||||
installOverlapGeometry(textRects);
|
||||
installAuditScript();
|
||||
return runAudit();
|
||||
}
|
||||
|
||||
function installOverlapStyles(
|
||||
colors: Record<string, string>,
|
||||
clipPaths: Record<string, string>,
|
||||
): void {
|
||||
vi.spyOn(window, "getComputedStyle").mockImplementation((element) => {
|
||||
const id = (element as Element).id;
|
||||
return {
|
||||
@@ -1649,7 +1670,9 @@ function auditOverlapScene(options: {
|
||||
if (!isFullyClipped(clipPaths.a ?? "none")) return document.getElementById("a");
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
function installOverlapGeometry(textRects: Record<string, DOMRect[]>): void {
|
||||
for (const element of Array.from(document.querySelectorAll("*"))) {
|
||||
vi.spyOn(element, "getBoundingClientRect").mockReturnValue(
|
||||
boundingTextRect(textRects[element.id]) ??
|
||||
@@ -1674,9 +1697,6 @@ function auditOverlapScene(options: {
|
||||
detach() {},
|
||||
} as unknown as Range;
|
||||
});
|
||||
|
||||
installAuditScript();
|
||||
return runAudit();
|
||||
}
|
||||
|
||||
function normalizeTextRects(value: DOMRect | DOMRect[]): DOMRect[] {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"files": 7
|
||||
},
|
||||
"hyperframes-cli": {
|
||||
"hash": "3fa884269c43d7df",
|
||||
"hash": "5d02a1713635e7e7",
|
||||
"files": 11
|
||||
},
|
||||
"hyperframes-core": {
|
||||
|
||||
@@ -55,7 +55,7 @@ Every finding carries a selector, the element's `data-*` identity, the compositi
|
||||
**Escape hatches** (mark intent in the HTML, then re-run):
|
||||
|
||||
- `data-layout-allow-overflow` — overflow is intentional (entrance/exit travel).
|
||||
- `data-layout-allow-overlap` — deliberate text layering (e.g. a demo cursor label over a heading).
|
||||
- `data-layout-allow-overlap` — deliberate text layering (e.g. a demo cursor label over a heading). Applies only to the marked text block; it is not inherited. Mark the specific layering participant, never a scene/root wrapper, so unrelated descendant collisions remain auditable.
|
||||
- `data-layout-allow-occlusion` — an element is meant to cover text.
|
||||
- `data-layout-allow-caption-zone` — intentional lower-third / caption-band copy under `--caption-zone`. Applies to the marked element and every descendant (`closest`); silences only `caption_zone_collision` (not overflow/overlap/occlusion). Prefer the narrowest wrapper that owns the intentional band copy.
|
||||
- `data-layout-ignore` — decorative element that should never be audited.
|
||||
|
||||
Reference in New Issue
Block a user