Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .agents/skills/changelog-video/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 4 additions & 3 deletions .agents/skills/changelog-video/examples/master-skeleton.html
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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] -->
Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/changelog-video/references/build-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions .claude/skills/changelog-video/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 4 additions & 3 deletions .claude/skills/changelog-video/examples/master-skeleton.html
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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] -->
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/changelog-video/references/build-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/prompting/rules-and-anti-patterns.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/commands/changelogSkillContent.test.ts
Original file line number Diff line number Diff line change
@@ -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/);
});
});
43 changes: 43 additions & 0 deletions packages/cli/src/commands/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/commands/layout-audit.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
28 changes: 24 additions & 4 deletions packages/cli/src/commands/layout-audit.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>
Expand All @@ -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 {
Expand All @@ -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]) ??
Expand All @@ -1674,9 +1697,6 @@ function auditOverlapScene(options: {
detach() {},
} as unknown as Range;
});

installAuditScript();
return runAudit();
}

function normalizeTextRects(value: DOMRect | DOMRect[]): DOMRect[] {
Expand Down
2 changes: 1 addition & 1 deletion skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"files": 7
},
"hyperframes-cli": {
"hash": "3fa884269c43d7df",
"hash": "5d02a1713635e7e7",
"files": 11
},
"hyperframes-core": {
Expand Down
2 changes: 1 addition & 1 deletion skills/hyperframes-cli/references/lint-validate-inspect.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading