From 630c8900f5866d5cd51eb3da96293981e41fc971 Mon Sep 17 00:00:00 2001 From: ukimsanov Date: Wed, 5 Aug 2026 10:41:34 -0700 Subject: [PATCH] docs(examples): reduced-motion play starts both films; precise media-start layers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - replica-compare: the voluntary control now starts and pauses BOTH films (not just the reference), and the replica-sync effect attaches in view regardless of the preference, so a reduced-motion visitor who presses play sees the whole synchronized pair. Added a focused source-level assertion to the motion-check suite (the repo has no React runtime harness for snippets). - html-schema: describe each layer precisely instead of grouping the CLI — timing compiler, HTML parser, producer audio, and 'hyperframes validate' read only data-media-start; runtime, Studio and 'hyperframes snapshot' read data-playback-start first (Studio also writes it). Round-3 findings from Magi. --- docs/reference/html-schema.mdx | 2 +- docs/snippets/replica-compare.jsx | 23 +++++++++++++++++++--- scripts/check-docs-snippet-motion.test.mjs | 22 +++++++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/docs/reference/html-schema.mdx b/docs/reference/html-schema.mdx index 178a72eb8..c9c6495a4 100644 --- a/docs/reference/html-schema.mdx +++ b/docs/reference/html-schema.mdx @@ -122,7 +122,7 @@ Audio has no visual lifecycle. | Attribute | Applies to | Meaning | | --- | --- | --- | -| `data-media-start` / `data-playback-start` | Video, audio, nested composition | Offset into the source file, used by trim and split. The two names are read by different layers with different precedence: the timing compiler and HTML parser read only `data-media-start`, while the runtime player, Studio, and the CLI read `data-playback-start` first and fall back to `data-media-start`. Studio writes `data-playback-start` for nested compositions and any element authored with it. Set **one**; authoring both is what causes the two layers to diverge. | +| `data-media-start` / `data-playback-start` | Video, audio, nested composition | Offset into the source file, used by trim and split. The two names are read by different layers, and not all of them agree. **Read only `data-media-start`:** the timing compiler, the HTML parser, the producer's audio extraction, and `hyperframes validate`. **Read `data-playback-start` first, falling back to `data-media-start`:** the runtime player, Studio, and `hyperframes snapshot` — and Studio also writes `data-playback-start` for nested compositions and any element authored with it. Set **one** value; authoring both is what makes these two groups diverge. | | `data-playback-rate` | Video, audio, nested composition | Playback multiplier from `0.1` to `5` | | `data-volume` | Video and audio | Static volume from `0` to `1` | | `data-has-audio="true"` | Video | Declares that the video contributes audio | diff --git a/docs/snippets/replica-compare.jsx b/docs/snippets/replica-compare.jsx index f3f09ee01..60fe6fb99 100644 --- a/docs/snippets/replica-compare.jsx +++ b/docs/snippets/replica-compare.jsx @@ -65,11 +65,13 @@ export const ReplicaCompare = ({ } }, [reduced, inView]); - // Keep the replica (right) locked to the reference (left) clock. + // Keep the replica (right) locked to the reference (left) clock. Attached + // whenever the pair is in view — including under reduced motion — so that once + // the reference starts (autoplay, or a voluntary press) the replica follows. useEffect(() => { const a = refVideo.current; const b = repVideo.current; - if (!a || !b || reduced || !inView) return; + if (!a || !b || !inView) return; const resync = () => { if (Number.isFinite(a.currentTime) && Math.abs((b.currentTime || 0) - a.currentTime) > 0.15) { @@ -95,13 +97,28 @@ export const ReplicaCompare = ({ }; }, [reduced, inView]); + // Start/pause both films together — the sync effect keeps the replica locked to + // the reference clock once the reference is playing. + const startBoth = () => { + refVideo.current?.play().catch(() => {}); + repVideo.current?.play().catch(() => {}); + }; + const pauseBoth = () => { + refVideo.current?.pause(); + repVideo.current?.pause(); + }; + const toggleSound = () => { const a = refVideo.current; if (!a) return; const next = !muted; setMuted(next); a.muted = next; // only the reference carries audio; replica stays silent - if (!next) a.play().catch(() => {}); + if (!next) { + startBoth(); // voluntary play — starts the whole pair, incl. under reduced motion + } else if (reduced) { + pauseBoth(); // no autoplay to fall back to under reduced motion + } }; const active = inView && !reduced; diff --git a/scripts/check-docs-snippet-motion.test.mjs b/scripts/check-docs-snippet-motion.test.mjs index 3bbd4e29e..821c66004 100644 --- a/scripts/check-docs-snippet-motion.test.mjs +++ b/scripts/check-docs-snippet-motion.test.mjs @@ -1,5 +1,8 @@ import { strict as assert } from "node:assert"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; import { test } from "node:test"; +import { fileURLToPath } from "node:url"; import { auditSnippets, @@ -8,6 +11,8 @@ import { splitComponents, } from "./check-docs-snippet-motion.mjs"; +const here = dirname(fileURLToPath(import.meta.url)); + const GUARDED = `export const Grid = () => { const [reducedMotion, setReducedMotion] = useState( () => @@ -121,3 +126,20 @@ test("forwarding a caller's autoPlay prop does not make a component owe the guar test("every autoplaying component in docs/snippets currently satisfies the guard", () => { assert.deepEqual(auditSnippets(), []); }); + +// Reduced motion disables autoplay, but the visible control must still start the +// whole comparison — not just the reference — or a reduced-motion visitor who +// presses it sees half the pair. Source-level, since the repo has no React +// runtime harness for docs snippets (this whole check is source-level for that +// reason). +test("ReplicaCompare's control starts both films and does not gate sync on reduced motion", () => { + const source = readFileSync(join(here, "../docs/snippets/replica-compare.jsx"), "utf8"); + // The voluntary-play path starts both the reference and the replica. + assert.match(source, /refVideo\.current\?\.play\(\)/, "reference is started"); + assert.match(source, /repVideo\.current\?\.play\(\)/, "replica is started too"); + // The replica-sync effect attaches in view regardless of the preference, so a + // voluntary play under reduced motion still pulls the replica along. + const syncGate = source.match(/const b = repVideo\.current;\s*\n\s*if \(([^)]*)\) return;/); + assert.ok(syncGate, "found the replica-sync guard"); + assert.doesNotMatch(syncGate[1], /reduced/, "sync must not early-return on reduced motion"); +});