docs: correct composition offset rationale; pin nav fix; label-in-name

- html-schema: the previous clause claimed a composition host's data-media-start is
  never read. It is: readElementPlaybackStart (media.ts:16) resolves
  data-playback-start ?? data-media-start and timeline.ts calls it on composition
  clips. Rewrote to the accurate reason both reviewers gave — composition hosts are
  only inspected by the playback-start-first readers, media-start works as a
  fallback, but playback-start is what Studio writes/normalises to.
- motion test: pin the HoverVideo click-suppression (preventDefault +
  stopPropagation). Removing it left the gate 12/12; now it fails. This is the bug
  that escaped static review and only surfaced by driving the live preview.
- replica-compare: fold the visible 'Sound off/on' text into the aria-label so the
  accessible name contains it (WCAG 2.5.3, Rames).

Round-5 findings from Magi and Rames.
This commit is contained in:
ukimsanov
2026-08-05 11:32:21 -07:00
parent 83db94e75e
commit fed2baf278
3 changed files with 18 additions and 5 deletions
+1 -1
View File
@@ -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. Two groups of readers disagree, so the right name depends on the element. **Read only `data-media-start`:** the timing compiler, the HTML parser, `hyperframes validate` (which only inspects `<audio>`), and the engine's audio mixer (which feeds ffmpeg `-ss`). **Read `data-playback-start` first, falling back to `data-media-start`:** the runtime player, Studio (which also writes it), and `hyperframes snapshot`. Because the audio mixer reads only `data-media-start`, a `<video>` authored with just `data-playback-start` renders a trimmed picture over untrimmed audio. Set the name by kind: **`<video>` / `<audio>` → `data-media-start`**; **nested composition → `data-playback-start`** — none of the media-start-only readers inspect a composition host, so its offset is only ever read as `data-playback-start` (Studio writes it; it is the [child-timeline offset](/concepts/compositions)). |
| `data-media-start` / `data-playback-start` | Video, audio, nested composition | Offset into the source file, used by trim and split. Two groups of readers disagree, so the right name depends on the element. **Read only `data-media-start`:** the timing compiler, the HTML parser, `hyperframes validate` (which only inspects `<audio>`), and the engine's audio mixer (which feeds ffmpeg `-ss`). **Read `data-playback-start` first, falling back to `data-media-start`:** the runtime player, Studio (which also writes it), and `hyperframes snapshot`. Because the audio mixer reads only `data-media-start`, a `<video>` authored with just `data-playback-start` renders a trimmed picture over untrimmed audio. Set the name by kind: **`<video>` / `<audio>` → `data-media-start`**; **nested composition → `data-playback-start`** — composition hosts are inspected only by the playback-start-first readers (the media-start-only ones are all `<video>`/`<audio>`-scoped). `data-media-start` still works there as a fallback, but `data-playback-start` is the canonical name Studio writes and normalises to for new composition hosts, so the other name works until an edit rewrites it (it is the [child-timeline offset](/concepts/compositions)). |
| `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 |
+4 -4
View File
@@ -158,11 +158,11 @@ export const ReplicaCompare = ({
aria-label={
muted
? reduced
? "Play the comparison with sound"
: "Unmute the reference"
? "Sound off — play the comparison with sound"
: "Sound off — unmute the reference"
: reduced
? "Pause the comparison"
: "Mute the reference"
? "Sound on — pause the comparison"
: "Sound on — mute the reference"
}
className="absolute left-3 top-3 z-10 flex items-center gap-1.5 rounded-full bg-black/60 px-3 py-1.5 text-xs font-medium text-white backdrop-blur transition hover:bg-black/75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
@@ -156,3 +156,16 @@ test("ReplicaCompare's control starts both films and does not gate sync on reduc
assert.ok(teardown, "found the offscreen teardown");
assert.match(teardown[1], /setMuted\(true\)/, "offscreen release resets muted state");
});
// 40 of HoverVideo's 53 uses sit inside a link (11 of 23 examples cards are
// <a href>, 29 of 30 thirty-days cards are <Card href>). Its control's click must
// not bubble to that link, or pressing it — mouse or keyboard — navigates away
// instead of toggling sound. This bug escaped static review and the gate; it only
// surfaced by driving the live preview, so pin it.
test("HoverVideo's control does not bubble its click to a wrapping link", () => {
const source = readFileSync(join(here, "../docs/snippets/hover-video.jsx"), "utf8");
const onClick = source.match(/onClick=\{\(e\) => \{([\s\S]*?)\n {8}\}\}/);
assert.ok(onClick, "found the control's click handler");
assert.match(onClick[1], /e\.preventDefault\(\)/, "default navigation is prevented");
assert.match(onClick[1], /e\.stopPropagation\(\)/, "the click does not bubble to a wrapping link");
});