@celestia-island/hikari 0.55.338 → 0.55.340

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,16 +19,21 @@ import jaBundle from "../i18n/locales/ja/components.json";
19
19
  *
20
20
  * The component's whole reason to exist is the window contract, so the
21
21
  * cases below pin it from both sides:
22
- * · windowed lanes are the ONLY clipping site (full domain → positions
23
- * untouched; clipped → re-normalized; wholly-outside → absent), while
24
- * the overview keeps painting the full domain;
25
- * · the emitted window is ALWAYS in full-domain absolute fractions —
26
- * never relative to the currently zoomed sub-domain (the chest row
27
- * filter depends on exactly that);
22
+ * · clipping has exactly TWO sites: the window (windowed lanes: outside →
23
+ * absent, straddling → re-normalized) and the DOMAIN (`clipToDomain`:
24
+ * content outside the caller's axis paints nowhere on either layer),
25
+ * while the overview keeps painting the whole domain;
26
+ * · the emitted window is ALWAYS in absolute domain coordinates — never
27
+ * relative to the currently zoomed sub-domain (the chest row filter
28
+ * depends on exactly that);
28
29
  * · every reset path (button / dblclick / Escape / Home) lands null and
29
30
  * restores the full view;
30
31
  * · sub-minimum brushes, edge drags and keyboard nudges clamp instead of
31
- * crossing.
32
+ * crossing;
33
+ * · the two layers stay explicit (minimap / detail) over ONE caller
34
+ * `domain` axis (default [0, 1]): the minimap draws the domain, the
35
+ * detail draws the selected window, labels read domain percentages, and
36
+ * the deprecated `headroom` prop is the `[0, 1 + headroom]` alias.
32
37
  * A stylesheet contract at the bottom compiles the sheet and pins the
33
38
  * classes, geometry and the one animation (repo convention: vitest stubs
34
39
  * CSS, so the runtime cases can never catch a dead rule).
@@ -284,6 +289,7 @@ describe("HkRangeRibbon full domain", () => {
284
289
  { id: "far", lane: "cpu", start: -0.5, end: 0.25, label: "far" },
285
290
  { id: "nan", lane: "cpu", start: Number.NaN, end: 0.4, label: "nan" },
286
291
  { id: "orphan", lane: "gpu", start: 0.1, end: 0.2, label: "orphan" },
292
+ { id: "outside", lane: "cpu", start: 1.2, end: 1.4, label: "outside" },
287
293
  ];
288
294
  const root = mountRibbon({ window: null, segments });
289
295
  // Inverted → swapped, out-of-domain → clamped, zero-length kept as a tick.
@@ -298,6 +304,11 @@ describe("HkRangeRibbon full domain", () => {
298
304
  // …and a segment can never invent an undeclared lane.
299
305
  expect(segmentById(root, "orphan")).toBeNull();
300
306
  expect(root.querySelector('[data-segment-id="orphan"]')).toBeNull();
307
+ // Content wholly outside the axis paints NOTHING: the domain is a
308
+ // clipping rule, not a clamp to an edge tick (pre-domain content would
309
+ // have collapsed [1.2, 1.4] onto 100% and drawn a tick there).
310
+ expect(segmentById(root, "outside")).toBeNull();
311
+ expect(root.querySelector('[data-segment-id="outside"]')).toBeNull();
301
312
  });
302
313
 
303
314
  it("shares the label column with the lanes so both axes agree", () => {
@@ -323,7 +334,7 @@ describe("HkRangeRibbon window normalizer parity", () => {
323
334
  // component must keep CALLING the exported normalizer rather than
324
335
  // growing a private copy again.
325
336
  const tsx = readFileSync(resolve(componentDir, "HkRangeRibbon.tsx"), "utf8");
326
- expect(tsx).toMatch(/return normalizeHkRangeWindow\(source, minWidth\.value\)/);
337
+ expect(tsx).toMatch(/return normalizeHkRangeWindow\(source, minWidth\.value, domain\.value\)/);
327
338
  });
328
339
 
329
340
  it("renders exactly the window the exported helper returns", () => {
@@ -1357,6 +1368,732 @@ describe("HkRangeRibbon controlled contract", () => {
1357
1368
  });
1358
1369
  });
1359
1370
 
1371
+ // ── Domain-aware normalizer ─────────────────────────────────────────────
1372
+ describe("HkRangeRibbon domain normalizer", () => {
1373
+ it("clamps, expands and caps inside the caller's domain", () => {
1374
+ // Inverted and out-of-domain windows clamp to the axis.
1375
+ const inverted = normalizeHkRangeWindow([1.5, 0.5], 0.02, [0, 2])!;
1376
+ expect(inverted[0]).toBeCloseTo(0.5, 10);
1377
+ expect(inverted[1]).toBeCloseTo(1.5, 10);
1378
+ const clamped = normalizeHkRangeWindow([-1, 3], 0.02, [0, 2])!;
1379
+ expect(clamped[0]).toBeCloseTo(0, 10);
1380
+ expect(clamped[1]).toBeCloseTo(2, 10);
1381
+
1382
+ // The min width lives in domain units: 0.02 around a point on [0, 2].
1383
+ const point = normalizeHkRangeWindow([0.5, 0.5], 0.02, [0, 2])!;
1384
+ expect(point[0]).toBeCloseTo(0.49, 10);
1385
+ expect(point[1]).toBeCloseTo(0.51, 10);
1386
+
1387
+ // A min width wider than the axis caps at the span instead of escaping
1388
+ // the domain.
1389
+ const capped = normalizeHkRangeWindow([1, 1], 5, [0, 2])!;
1390
+ expect(capped[0]).toBeCloseTo(0, 10);
1391
+ expect(capped[1]).toBeCloseTo(2, 10);
1392
+
1393
+ // At the domain edges the expansion shifts inward, never outside.
1394
+ const leftEdge = normalizeHkRangeWindow([0, 0], 0.02, [0, 2])!;
1395
+ expect(leftEdge[0]).toBeCloseTo(0, 10);
1396
+ expect(leftEdge[1]).toBeCloseTo(0.02, 10);
1397
+ const rightEdge = normalizeHkRangeWindow([2, 2], 0.02, [0, 2])!;
1398
+ expect(rightEdge[0]).toBeCloseTo(1.98, 10);
1399
+ expect(rightEdge[1]).toBeCloseTo(2, 10);
1400
+ });
1401
+
1402
+ it("treats a malformed domain as the unit axis (two-argument parity)", () => {
1403
+ // Hosts calling with two arguments are byte-identical to the old contract.
1404
+ expect(normalizeHkRangeWindow([0.2, 0.4])).toEqual([0.2, 0.4]);
1405
+ expect(normalizeHkRangeWindow([0.2, 0.4], 0.02)).toEqual([0.2, 0.4]);
1406
+ expect(normalizeHkRangeWindow([0.2, 0.4], 0.02, null)).toEqual([0.2, 0.4]);
1407
+ expect(normalizeHkRangeWindow([0.2, 0.4], 0.02, "nope" as unknown as number[])).toEqual([
1408
+ 0.2, 0.4,
1409
+ ]);
1410
+ // A zero-width domain has no interior: the unit axis takes over.
1411
+ expect(normalizeHkRangeWindow([0.2, 0.4], 0.02, [0.5, 0.5])).toEqual([0.2, 0.4]);
1412
+ // An inverted domain is sorted, so it behaves like the same ordered pair.
1413
+ expect(normalizeHkRangeWindow([0.2, 0.4], 0.02, [1, 0])).toEqual([0.2, 0.4]);
1414
+ });
1415
+ });
1416
+
1417
+ // ── Dual-layer waterfall + caller domain axis ───────────────────────────
1418
+ describe("HkRangeRibbon dual-layer waterfall", () => {
1419
+ /** The minimap's x for a fraction on the deprecated headroom ALIAS axis
1420
+ * `[0, 1 + headroom]`, in percent. */
1421
+ function axisPercent(fraction: number, headroom: number): number {
1422
+ return (fraction / (1 + headroom)) * 100;
1423
+ }
1424
+
1425
+ /** The minimap's x of a DOMAIN coordinate / of a domain LENGTH, in percent. */
1426
+ function domainPercent(value: number, domain: [number, number]): number {
1427
+ return ((value - domain[0]) / (domain[1] - domain[0])) * 100;
1428
+ }
1429
+
1430
+ function domainWidthPercent(length: number, domain: [number, number]): number {
1431
+ return (length / (domain[1] - domain[0])) * 100;
1432
+ }
1433
+
1434
+ /** Inline box assertions at the geometry tolerance. `percent()` rounds to
1435
+ * four decimals OF A PERCENT (~1e-6 fraction), so a four-decimal
1436
+ * `toBeCloseTo` sits exactly on that rounding boundary; 3 keeps a margin
1437
+ * while still pinning well below a percent. */
1438
+ function expectBox(
1439
+ element: HTMLElement | null,
1440
+ expected: { left: number; width: number },
1441
+ label: string,
1442
+ ): void {
1443
+ expect(element, label).toBeTruthy();
1444
+ expect(boxOf(element!).left, `${label} left`).toBeCloseTo(expected.left, 3);
1445
+ expect(boxOf(element!).width, `${label} width`).toBeCloseTo(expected.width, 3);
1446
+ }
1447
+
1448
+ it("marks both layers explicitly and keeps the lane markup intact", () => {
1449
+ const root = mountRibbon({ window: [0.35, 0.67] });
1450
+ const minimap = root.querySelector<HTMLElement>(".hk-range-ribbon-overview");
1451
+ expect(minimap, "the minimap layer renders").toBeTruthy();
1452
+ expect(minimap!.getAttribute("data-layer")).toBe("minimap");
1453
+ // The strip (brush surface + window chrome host) is the minimap's own.
1454
+ expect(minimap!.querySelector(".hk-range-ribbon-strip")).toBeTruthy();
1455
+ expect(minimap!.querySelector(".hk-range-ribbon-overview-gutter")).toBeTruthy();
1456
+ expect(minimap!.querySelectorAll(".hk-range-ribbon-overview-row")).toHaveLength(2);
1457
+
1458
+ const detail = root.querySelector<HTMLElement>(".hk-range-ribbon-detail");
1459
+ expect(detail, "the detail layer renders").toBeTruthy();
1460
+ expect(detail!.getAttribute("data-layer")).toBe("detail");
1461
+ // The lane list keeps its markup verbatim and lives INSIDE the wrapper.
1462
+ const lanes = detail!.querySelector<HTMLElement>(".hk-range-ribbon-lanes");
1463
+ expect(lanes, "the lane list is nested in the detail layer").toBeTruthy();
1464
+ expect(lanes!.parentElement).toBe(detail);
1465
+ expect(detail!.querySelectorAll(".hk-range-ribbon-lane")).toHaveLength(2);
1466
+ expect(detail!.querySelectorAll(".hk-range-ribbon-lane-track")).toHaveLength(2);
1467
+ // Exactly two layers, in reading order (minimap above detail).
1468
+ expect(Array.from(root.children).map((child) => child.getAttribute("data-layer"))).toEqual([
1469
+ "minimap",
1470
+ "detail",
1471
+ ]);
1472
+ // Minimal mode stays a bare bar: no detail layer at all.
1473
+ expect(mountRibbon({ minimal: true }).querySelector(".hk-range-ribbon-detail")).toBeNull();
1474
+ });
1475
+
1476
+ it("keeps the unit axis when no domain is supplied (backward compatible)", () => {
1477
+ // Omitting `domain` (and the alias) is exactly [0, 1]: same boxes, same
1478
+ // aria bounds as an explicit unit axis.
1479
+ const implicit = mountRibbon({ window: [0.2, 0.6] });
1480
+ const explicit = mountRibbon({ domain: [0, 1], window: [0.2, 0.6] });
1481
+ for (const selector of [
1482
+ ".hk-range-ribbon-window",
1483
+ '.hk-range-ribbon-scrim[data-side="after"]',
1484
+ '.hk-range-ribbon-overview-segment[data-segment-id="write"]',
1485
+ '.hk-range-ribbon-lane-track .hk-range-ribbon-segment[data-segment-id="write"]',
1486
+ ]) {
1487
+ const a = implicit.querySelector<HTMLElement>(selector)!;
1488
+ const b = explicit.querySelector<HTMLElement>(selector)!;
1489
+ expect(a.style.left, `${selector} left`).toBe(b.style.left);
1490
+ expect(a.style.width, `${selector} width`).toBe(b.style.width);
1491
+ }
1492
+ const strip = implicit.querySelector<HTMLElement>(".hk-range-ribbon-strip")!;
1493
+ expect(strip.getAttribute("aria-valuemin")).toBe("0");
1494
+ expect(strip.getAttribute("aria-valuemax")).toBe("100");
1495
+ });
1496
+
1497
+ it("draws the minimap x as (f - domain[0]) / span across caller axes", () => {
1498
+ // Hard anchor first: on [0, 2] half the strip is coordinate 1.0.
1499
+ const wide = mountRibbon({ domain: [0, 2], window: [0.5, 1.5] });
1500
+ expectBox(
1501
+ wide.querySelector<HTMLElement>(".hk-range-ribbon-window"),
1502
+ { left: 25, width: 50 },
1503
+ "frame [0.5, 1.5] of [0, 2]",
1504
+ );
1505
+ expectBox(
1506
+ wide.querySelector<HTMLElement>('.hk-range-ribbon-overview-segment[data-segment-id="parse"]'),
1507
+ { left: 15, width: 7.5 },
1508
+ "parse 0.3..0.45 of [0, 2]",
1509
+ );
1510
+ expectBox(
1511
+ wide.querySelector<HTMLElement>('.hk-range-ribbon-scrim[data-side="before"]'),
1512
+ { left: 0, width: 25 },
1513
+ "before scrim covers the domain before the window",
1514
+ );
1515
+ expectBox(
1516
+ wide.querySelector<HTMLElement>('.hk-range-ribbon-scrim[data-side="after"]'),
1517
+ { left: 75, width: 25 },
1518
+ "after scrim runs to the domain end",
1519
+ );
1520
+ expect(
1521
+ parseFloat(wide.querySelector<HTMLElement>('.hk-range-ribbon-handle[data-edge="start"]')!.style.left),
1522
+ ).toBeCloseTo(25, 3);
1523
+ expect(
1524
+ parseFloat(wide.querySelector<HTMLElement>('.hk-range-ribbon-handle[data-edge="end"]')!.style.left),
1525
+ ).toBeCloseTo(75, 3);
1526
+
1527
+ // Formula sweep over the shapes a caller may declare: negative start,
1528
+ // trailing budget and a narrower window of interest. A window at 10%..70%
1529
+ // of each axis must land at the same strip percentages.
1530
+ const cases: Array<[number, number]> = [
1531
+ [-0.1, 1],
1532
+ [0.2, 0.8],
1533
+ [-0.5, 1.5],
1534
+ ];
1535
+ for (const domain of cases) {
1536
+ const span = domain[1] - domain[0];
1537
+ const window: [number, number] = [domain[0] + 0.1 * span, domain[0] + 0.7 * span];
1538
+ const root = mountRibbon({ domain, window });
1539
+ expectBox(
1540
+ root.querySelector<HTMLElement>(".hk-range-ribbon-window"),
1541
+ { left: 10, width: 60 },
1542
+ `${domain} frame is 10%..70% of the axis`,
1543
+ );
1544
+ expectBox(
1545
+ root.querySelector<HTMLElement>('.hk-range-ribbon-scrim[data-side="before"]'),
1546
+ { left: 0, width: 10 },
1547
+ `${domain} before scrim`,
1548
+ );
1549
+ expectBox(
1550
+ root.querySelector<HTMLElement>('.hk-range-ribbon-scrim[data-side="after"]'),
1551
+ { left: 70, width: 30 },
1552
+ `${domain} after scrim reaches the axis end`,
1553
+ );
1554
+ expect(
1555
+ parseFloat(
1556
+ root.querySelector<HTMLElement>('.hk-range-ribbon-handle[data-edge="start"]')!.style.left,
1557
+ ),
1558
+ `${domain} start grip`,
1559
+ ).toBeCloseTo(10, 3);
1560
+ expect(
1561
+ parseFloat(
1562
+ root.querySelector<HTMLElement>('.hk-range-ribbon-handle[data-edge="end"]')!.style.left,
1563
+ ),
1564
+ `${domain} end grip`,
1565
+ ).toBeCloseTo(70, 3);
1566
+ }
1567
+ });
1568
+
1569
+ it("maps the detail track to the selected WINDOW in domain coordinates", () => {
1570
+ // Domain [0, 2], window [0.5, 1.5]: the detail is 0..100% of that window,
1571
+ // so a bar starting at coordinate 1.0 lands halfway.
1572
+ const segments: RibbonSegment[] = [
1573
+ { id: "lead", lane: "cpu", start: 0.5, end: 0.9, label: "lead" },
1574
+ { id: "mid", lane: "cpu", start: 1, end: 1.2, label: "mid" },
1575
+ { id: "tail", lane: "cpu", start: 1.4, end: 1.8, label: "tail" },
1576
+ ];
1577
+ const root = mountRibbon({ domain: [0, 2], window: [0.5, 1.5], segments });
1578
+ expectBox(segmentById(root, "lead"), { left: 0, width: 40 }, "lead 0.5..0.9");
1579
+ expectBox(segmentById(root, "mid"), { left: 50, width: 20 }, "a bar at 1.0 starts halfway");
1580
+ expectBox(segmentById(root, "tail"), { left: 90, width: 10 }, "tail clipped at the window end");
1581
+
1582
+ // The same window on a negative-start axis keeps its relative mapping:
1583
+ // [-0.1, 1] with window [0.2, 0.6] puts parse (0.3..0.45) at 25%..62.5%.
1584
+ const negative = mountRibbon({ domain: [-0.1, 1], window: [0.2, 0.6] });
1585
+ expectBox(segmentById(negative, "parse"), { left: 25, width: 37.5 }, "parse inside a negative axis");
1586
+ });
1587
+
1588
+ it("paints the no-window detail on the domain axis (the full view)", () => {
1589
+ // With no window the detail IS the full view, so its axis is the domain:
1590
+ // on [0, 2] a segment at 0.3..0.45 lands at 15%..22.5%, and the minimap
1591
+ // twin agrees — the full-domain view is where the two layers line up.
1592
+ const root = mountRibbon({ window: null, domain: [0, 2] });
1593
+ const parse = segmentById(root, "parse")!;
1594
+ expect(boxOf(parse).left, "0.3 on a [0, 2] axis").toBeCloseTo(15, 3);
1595
+ expect(boxOf(parse).width).toBeCloseTo(7.5, 3);
1596
+ const overview = root.querySelector<HTMLElement>(
1597
+ '.hk-range-ribbon-overview-segment[data-segment-id="parse"]',
1598
+ )!;
1599
+ expect(boxOf(overview).left, "both layers share the domain axis").toBeCloseTo(
1600
+ boxOf(parse).left,
1601
+ 3,
1602
+ );
1603
+ expect(boxOf(overview).width).toBeCloseTo(boxOf(parse).width, 3);
1604
+
1605
+ // The deprecated alias drives the no-window detail the same way: it IS a
1606
+ // domain, not a minimap-only tail.
1607
+ const alias = mountRibbon({ window: null, headroom: 0.1 });
1608
+ expect(boxOf(segmentById(alias, "parse")!).left).toBeCloseTo(axisPercent(0.3, 0.1), 3);
1609
+ });
1610
+
1611
+ it("maps the same absolute fraction onto the alias minimap and the window-relative detail", () => {
1612
+ // parse covers 30%..45% of the alias axis [0, 1.1]; the window is
1613
+ // 35%..67% of that same axis.
1614
+ const root = mountRibbon({ window: [0.35, 0.67], headroom: 0.1 });
1615
+ expectBox(
1616
+ root.querySelector<HTMLElement>('.hk-range-ribbon-overview-segment[data-segment-id="parse"]'),
1617
+ { left: axisPercent(0.3, 0.1), width: axisPercent(0.15, 0.1) },
1618
+ "alias minimap parse",
1619
+ );
1620
+
1621
+ // The detail ignores the axis entirely and re-normalizes the window to
1622
+ // its own 0..100% track.
1623
+ const parseDetail = segmentById(root, "parse")!;
1624
+ expect(boxOf(parseDetail).left, "detail starts at the window start").toBeCloseTo(0, 3);
1625
+ expect(boxOf(parseDetail).width, "detail width is window-relative").toBeCloseTo(
1626
+ ((0.45 - 0.35) / (0.67 - 0.35)) * 100,
1627
+ 3,
1628
+ );
1629
+ const writeDetail = segmentById(root, "write")!;
1630
+ expect(
1631
+ boxOf(writeDetail).left + boxOf(writeDetail).width,
1632
+ "detail ends at the window end",
1633
+ ).toBeCloseTo(100, 3);
1634
+
1635
+ // A detail box under the alias is byte-identical to its box with no
1636
+ // alias at all — the window, not the axis, is the detail's unit.
1637
+ const noAlias = mountRibbon({ window: [0.35, 0.67] });
1638
+ expect(boxOf(segmentById(root, "write")!)).toEqual(boxOf(segmentById(noAlias, "write")!));
1639
+ // …while the window frame is axis-projected on the minimap.
1640
+ expectBox(
1641
+ root.querySelector<HTMLElement>(".hk-range-ribbon-window"),
1642
+ { left: axisPercent(0.35, 0.1), width: axisPercent(0.32, 0.1) },
1643
+ "alias frame",
1644
+ );
1645
+ });
1646
+
1647
+ it("dims the unselected alias axis all the way to its end", () => {
1648
+ // window [0.2, 1]: content 100% is selected, so the whole 1.0..1.1 end of
1649
+ // the alias axis is unselected and must stay dimmed.
1650
+ const root = mountRibbon({ window: [0.2, 1], headroom: 0.1 });
1651
+ expectBox(
1652
+ root.querySelector<HTMLElement>('.hk-range-ribbon-scrim[data-side="before"]'),
1653
+ { left: 0, width: axisPercent(0.2, 0.1) },
1654
+ "before scrim",
1655
+ );
1656
+ const after = root.querySelector<HTMLElement>('.hk-range-ribbon-scrim[data-side="after"]')!;
1657
+ expect(boxOf(after).left, "after scrim starts at content 100%").toBeCloseTo(90.9091, 3);
1658
+ expect(boxOf(after).width, "after scrim covers to the alias end").toBeCloseTo(9.0909, 3);
1659
+ expect(
1660
+ boxOf(after).left + boxOf(after).width,
1661
+ "the scrim reaches the axis end",
1662
+ ).toBeCloseTo(100, 3);
1663
+
1664
+ expectBox(
1665
+ root.querySelector<HTMLElement>(".hk-range-ribbon-window"),
1666
+ { left: axisPercent(0.2, 0.1), width: axisPercent(0.8, 0.1) },
1667
+ "alias frame",
1668
+ );
1669
+ const start = root.querySelector<HTMLElement>('.hk-range-ribbon-handle[data-edge="start"]')!;
1670
+ const end = root.querySelector<HTMLElement>('.hk-range-ribbon-handle[data-edge="end"]')!;
1671
+ expect(parseFloat(start.style.left)).toBeCloseTo(axisPercent(0.2, 0.1), 3);
1672
+ expect(parseFloat(end.style.left)).toBeCloseTo(axisPercent(1, 0.1), 3);
1673
+ // The a11y readings are coordinates, not axis percentages.
1674
+ expect(end.getAttribute("aria-valuenow")).toBe("100");
1675
+ expect(end.getAttribute("aria-valuetext")).toBe("100%");
1676
+ expect(
1677
+ root.querySelector<HTMLElement>(".hk-range-ribbon-strip")!.getAttribute("aria-valuetext"),
1678
+ ).toBe("20% – 100%");
1679
+ });
1680
+
1681
+ it("sanitizes the deprecated headroom alias into a real axis", () => {
1682
+ const cases: Array<[number, number]> = [
1683
+ [0, 1],
1684
+ [-0.5, 1],
1685
+ [Number.NaN, 1],
1686
+ [Number.NEGATIVE_INFINITY, 1],
1687
+ [Number.POSITIVE_INFINITY, 1],
1688
+ [2, 2],
1689
+ ];
1690
+ for (const [headroom, span] of cases) {
1691
+ const root = mountRibbon({ window: null, headroom });
1692
+ const parse = root.querySelector<HTMLElement>(
1693
+ '.hk-range-ribbon-overview-segment[data-segment-id="parse"]',
1694
+ )!;
1695
+ expect(parse, `headroom ${headroom} still paints`).toBeTruthy();
1696
+ expect(boxOf(parse).left, `headroom ${headroom} maps through span ${span}`).toBeCloseTo(
1697
+ (0.3 / span) * 100,
1698
+ 3,
1699
+ );
1700
+ }
1701
+ });
1702
+
1703
+ it("treats headroom as the [0, 1 + headroom] alias, and a domain wins over it", () => {
1704
+ // Equivalence: the alias and its explicit domain draw the same geometry.
1705
+ const alias = mountRibbon({ headroom: 0.1, window: [0.2, 0.6] });
1706
+ const explicit = mountRibbon({ domain: [0, 1.1], window: [0.2, 0.6] });
1707
+ const compared: Array<{ selector: string; width: boolean }> = [
1708
+ { selector: ".hk-range-ribbon-window", width: true },
1709
+ { selector: '.hk-range-ribbon-scrim[data-side="before"]', width: true },
1710
+ { selector: '.hk-range-ribbon-scrim[data-side="after"]', width: true },
1711
+ { selector: '.hk-range-ribbon-handle[data-edge="start"]', width: false },
1712
+ { selector: '.hk-range-ribbon-handle[data-edge="end"]', width: false },
1713
+ { selector: '.hk-range-ribbon-overview-segment[data-segment-id="parse"]', width: true },
1714
+ ];
1715
+ for (const { selector, width } of compared) {
1716
+ const fromAlias = alias.querySelector<HTMLElement>(selector)!;
1717
+ const fromDomain = explicit.querySelector<HTMLElement>(selector)!;
1718
+ expect(parseFloat(fromAlias.style.left), `${selector} left`).toBeCloseTo(
1719
+ parseFloat(fromDomain.style.left),
1720
+ 10,
1721
+ );
1722
+ if (width) {
1723
+ expect(parseFloat(fromAlias.style.width), `${selector} width`).toBeCloseTo(
1724
+ parseFloat(fromDomain.style.width),
1725
+ 10,
1726
+ );
1727
+ }
1728
+ }
1729
+ // The alias announces its own end (110%), never the unit axis.
1730
+ expect(
1731
+ alias.querySelector<HTMLElement>(".hk-range-ribbon-strip")!.getAttribute("aria-valuemax"),
1732
+ ).toBe("110");
1733
+
1734
+ // A valid `domain` wins over a supplied headroom.
1735
+ const both = mountRibbon({ domain: [0, 2], headroom: 0.1 });
1736
+ expect(
1737
+ boxOf(
1738
+ both.querySelector<HTMLElement>(
1739
+ '.hk-range-ribbon-overview-segment[data-segment-id="parse"]',
1740
+ )!,
1741
+ ).left,
1742
+ "domain [0, 2] wins: parse at 15%",
1743
+ ).toBeCloseTo(15, 3);
1744
+ expect(
1745
+ both.querySelector<HTMLElement>(".hk-range-ribbon-strip")!.getAttribute("aria-valuemax"),
1746
+ ).toBe("200");
1747
+ });
1748
+
1749
+ it("treats the headroom alias as a real axis: selectable tail, axis-end rest state", async () => {
1750
+ // The deprecated `headroom` alias is SOURCE-compatible with the old prop,
1751
+ // not behaviour-compatible: [0, 1.1] is the coordinate system, so the old
1752
+ // "padding, not selectable progress" tail is gone. This case pins the
1753
+ // deltas a headroom host would observe.
1754
+ const host = mountHost([0, 1], { headroom: 0.1 });
1755
+ const root = host.root;
1756
+ // [0, 1] is a SUB-view of a [0, 1.1] axis, so there IS something to reset
1757
+ // (the old component treated content 100% as the rest state).
1758
+ expect(
1759
+ root.querySelector(".hk-range-ribbon-reset"),
1760
+ "a sub-view of the alias axis has a reset chip",
1761
+ ).toBeTruthy();
1762
+ keydown(root, "Escape");
1763
+ await nextTick();
1764
+ expect(lastWindow(host.windowChange), "Escape clears to the full axis").toBeNull();
1765
+
1766
+ // The tail is selectable axis: a press at strip 95% reads 1.045 (the old
1767
+ // component clamped it to content 1.0).
1768
+ const { strip, x } = stubStrip(root);
1769
+ await drag(strip, 0.95, 0.5, x);
1770
+ expectWindow(lastWindow(host.windowChange), 0.55, 1.045);
1771
+
1772
+ // Shift+ArrowLeft from the full axis contracts it by 5% of the SPAN
1773
+ // (0.055), not the unit axis's 0.05.
1774
+ keydown(root, "Escape");
1775
+ await nextTick();
1776
+ expect(lastWindow(host.windowChange), "back on the full axis").toBeNull();
1777
+ keydown(root.querySelector<HTMLElement>(".hk-range-ribbon-strip")!, "ArrowLeft", {
1778
+ shiftKey: true,
1779
+ });
1780
+ await nextTick();
1781
+ expectWindow(lastWindow(host.windowChange), 0, 1.045);
1782
+ });
1783
+
1784
+ it("caps minWindow at the axis span when the domain is narrower", () => {
1785
+ // A 0.01 span cannot honour the default 0.02 floor, so the component caps
1786
+ // the floor at the span. The cap is visible in the handle bounds: an
1787
+ // uncapped 0.02 floor would announce 49% / 52% instead of 50% / 51%.
1788
+ const root = mountRibbon({ domain: [0.5, 0.51], window: [0.505, 0.505] });
1789
+ expectBox(
1790
+ root.querySelector<HTMLElement>(".hk-range-ribbon-window"),
1791
+ { left: 0, width: 100 },
1792
+ "a point expands to the whole narrow axis",
1793
+ );
1794
+ expect(
1795
+ root.querySelector<HTMLElement>('[data-edge="start"]')!.getAttribute("aria-valuemax"),
1796
+ "the floor is the span, not the 0.02 default",
1797
+ ).toBe("50");
1798
+ expect(
1799
+ root.querySelector<HTMLElement>('[data-edge="end"]')!.getAttribute("aria-valuemin"),
1800
+ "end floor = start + the capped min",
1801
+ ).toBe("51");
1802
+ });
1803
+
1804
+ it("maps pointer input onto the domain and clamps to its edges", async () => {
1805
+ // Domain [0, 2]: half the strip is coordinate 1.0, so a brush from axis
1806
+ // 50% to the far right is [1, 2]. A read that ignored the domain would
1807
+ // answer [0.5, 1].
1808
+ const wide = mountHost(null, { domain: [0, 2] });
1809
+ const wideStrip = stubStrip(wide.root);
1810
+ await drag(wideStrip.strip, 0.5, 1, wideStrip.x);
1811
+ expectWindow(lastWindow(wide.windowChange), 1, 2);
1812
+
1813
+ // Negative start, [-0.1, 1]: axis 50% is coordinate 0.45, and a press
1814
+ // past the right edge still clamps to the domain end 1.
1815
+ const negative = mountHost(null, { domain: [-0.1, 1] });
1816
+ const neg = stubStrip(negative.root);
1817
+ await drag(neg.strip, 0.5, 1.25, neg.x);
1818
+ expectWindow(lastWindow(negative.windowChange), 0.45, 1);
1819
+
1820
+ // The handle path reads the same axis: with domain [0, 2] the end grip
1821
+ // dragged to axis 95% is coordinate 1.9 (a read that ignored the domain
1822
+ // would leave the window at 0.95).
1823
+ await wide.setWindow([0.5, 1.5]);
1824
+ const end = wide.root.querySelector<HTMLElement>('.hk-range-ribbon-handle[data-edge="end"]')!;
1825
+ expect(end, "the end grip renders for the sub-window").toBeTruthy();
1826
+ await drag(end, 0.75, 0.95, wideStrip.x);
1827
+ expectWindow(lastWindow(wide.windowChange), 0.5, 1.9);
1828
+ });
1829
+
1830
+ it("reads labels and ARIA in domain units (negative and above 100%)", () => {
1831
+ // Negative start: the full view announces its own axis, not 0%..100%.
1832
+ const negative = mountRibbon({ domain: [-0.1, 0.5] });
1833
+ const strip = negative.querySelector<HTMLElement>(".hk-range-ribbon-strip")!;
1834
+ expect(strip.getAttribute("aria-valuemin")).toBe("-10");
1835
+ expect(strip.getAttribute("aria-valuemax")).toBe("50");
1836
+ expect(strip.getAttribute("aria-valuenow"), "full view = domain start").toBe("-10");
1837
+ expect(strip.getAttribute("aria-valuetext")).toBe("-10% – 50%");
1838
+
1839
+ // A window that itself starts below zero keeps its sign on both the strip
1840
+ // and the start grip.
1841
+ const windowed = mountRibbon({ domain: [-0.1, 1], window: [-0.05, 0.4] });
1842
+ const windowedStrip = windowed.querySelector<HTMLElement>(".hk-range-ribbon-strip")!;
1843
+ expect(windowedStrip.getAttribute("aria-valuenow")).toBe("-5");
1844
+ expect(windowedStrip.getAttribute("aria-valuetext")).toBe("-5% – 40%");
1845
+ expect(
1846
+ windowed
1847
+ .querySelector<HTMLElement>('.hk-range-ribbon-handle[data-edge="start"]')!
1848
+ .getAttribute("aria-valuetext"),
1849
+ ).toBe("-5%");
1850
+
1851
+ // Above 100%: a 200% budget reads as 0..200, and a window ending at 150%
1852
+ // announces 150 rather than a rescaled 75.
1853
+ const wide = mountRibbon({ domain: [0, 2], window: [0.5, 1.5] });
1854
+ const wideStrip = wide.querySelector<HTMLElement>(".hk-range-ribbon-strip")!;
1855
+ expect(wideStrip.getAttribute("aria-valuemin")).toBe("0");
1856
+ expect(wideStrip.getAttribute("aria-valuemax")).toBe("200");
1857
+ expect(wideStrip.getAttribute("aria-valuetext")).toBe("50% – 150%");
1858
+ const wideEnd = wide.querySelector<HTMLElement>('.hk-range-ribbon-handle[data-edge="end"]')!;
1859
+ expect(wideEnd.getAttribute("aria-valuenow")).toBe("150");
1860
+ expect(wideEnd.getAttribute("aria-valuetext")).toBe("150%");
1861
+ expect(wideEnd.getAttribute("aria-valuemax")).toBe("200");
1862
+ });
1863
+
1864
+ it("sanitizes the domain: inverted sorts, unusable values fall back to [0, 1]", () => {
1865
+ // An inverted pair is a caller typo, not a different axis: it is sorted.
1866
+ const inverted = mountRibbon({ domain: [0.6, 0.2] });
1867
+ expect(
1868
+ boxOf(
1869
+ inverted.querySelector<HTMLElement>(
1870
+ '.hk-range-ribbon-overview-segment[data-segment-id="parse"]',
1871
+ )!,
1872
+ ).left,
1873
+ "parse 0.3 on the sorted [0.2, 0.6] axis",
1874
+ ).toBeCloseTo(25, 3);
1875
+
1876
+ const unusable: unknown[] = [
1877
+ [0.5, 0.5],
1878
+ [Number.NaN, 1],
1879
+ [0.2],
1880
+ [],
1881
+ null,
1882
+ [0.2, Number.POSITIVE_INFINITY],
1883
+ ];
1884
+ for (const domain of unusable) {
1885
+ const root = mountRibbon({ domain });
1886
+ const strip = root.querySelector<HTMLElement>(".hk-range-ribbon-strip")!;
1887
+ expect(strip.getAttribute("aria-valuemax"), `${JSON.stringify(domain)} falls back`).toBe(
1888
+ "100",
1889
+ );
1890
+ expect(
1891
+ boxOf(
1892
+ root.querySelector<HTMLElement>(
1893
+ '.hk-range-ribbon-overview-segment[data-segment-id="parse"]',
1894
+ )!,
1895
+ ).left,
1896
+ ).toBeCloseTo(30, 3);
1897
+ }
1898
+ });
1899
+
1900
+ it("clips overview segments to the domain and drops points outside it", () => {
1901
+ // Domain [0.2, 0.8]: boot ends exactly at the domain start and flush
1902
+ // starts past its end, so neither paints. An explicit segment list is
1903
+ // needed here: the shared fixture straddles the axis differently.
1904
+ const segments: RibbonSegment[] = [
1905
+ { id: "boot", lane: "cpu", start: 0.05, end: 0.2, label: "boot" },
1906
+ { id: "write", lane: "io", start: 0.5, end: 0.9, label: "write" },
1907
+ { id: "flush", lane: "io", start: 0.95, end: 1, label: "flush" },
1908
+ { id: "edge", lane: "cpu", start: 0.2, end: 0.2, label: "edge" },
1909
+ { id: "gone", lane: "cpu", start: 0.9, end: 0.9, label: "gone" },
1910
+ ];
1911
+ const root = mountRibbon({ window: null, domain: [0.2, 0.8], segments });
1912
+ expect(root.querySelector('[data-segment-id="boot"]'), "ends at the domain start").toBeNull();
1913
+ expect(root.querySelector('[data-segment-id="flush"]'), "starts past the domain end").toBeNull();
1914
+ // A zero-length point ON the domain start is kept as a tick; one past the
1915
+ // domain end is gone (the domain is the clipping rule, not a clamp).
1916
+ expect(
1917
+ root.querySelector('.hk-range-ribbon-overview-segment[data-segment-id="edge"]'),
1918
+ "a point on the domain edge paints",
1919
+ ).toBeTruthy();
1920
+ expect(root.querySelector('[data-segment-id="gone"]'), "a point outside is gone").toBeNull();
1921
+ // A bar straddling the domain end is clipped, not rescaled away.
1922
+ expectBox(
1923
+ root.querySelector<HTMLElement>(
1924
+ '.hk-range-ribbon-overview-segment[data-segment-id="write"]',
1925
+ ),
1926
+ { left: 50, width: 50 },
1927
+ "write clipped at the domain end",
1928
+ );
1929
+
1930
+ // The START clip anchors at the caller's d0, not at absolute 0: on
1931
+ // [-0.5, 1.5] a segment [-1, 0] straddles the domain start and paints
1932
+ // 0%..25% (clipping at 0 would paint 25%..0%: an empty box at the wrong
1933
+ // place). The no-window detail reuses the same domain box.
1934
+ const straddling = mountRibbon({
1935
+ window: null,
1936
+ domain: [-0.5, 1.5],
1937
+ segments: [{ id: "straddle", lane: "cpu", start: -1, end: 0, label: "straddle" }],
1938
+ });
1939
+ expectBox(
1940
+ straddling.querySelector<HTMLElement>(
1941
+ '.hk-range-ribbon-overview-segment[data-segment-id="straddle"]',
1942
+ ),
1943
+ { left: 0, width: 25 },
1944
+ "the overview start clip anchors at d0",
1945
+ );
1946
+ expectBox(
1947
+ segmentById(straddling, "straddle"),
1948
+ { left: 0, width: 25 },
1949
+ "the no-window detail shares that domain box",
1950
+ );
1951
+ });
1952
+
1953
+ it("treats a window covering the whole domain as the unfiltered state", () => {
1954
+ // The reset chip keys off the DOMAIN: [0, 2] on a [0, 2] axis is the rest
1955
+ // state (nothing to reset), while [0, 1] is a real sub-selection.
1956
+ const full = mountRibbon({ domain: [0, 2], window: [0, 2] });
1957
+ expect(
1958
+ full.querySelector(".hk-range-ribbon-reset"),
1959
+ "the whole axis selected = nothing to reset",
1960
+ ).toBeNull();
1961
+ const sub = mountRibbon({ domain: [0, 2], window: [0, 1] });
1962
+ expect(sub.querySelector(".hk-range-ribbon-reset"), "a sub-window shows reset").toBeTruthy();
1963
+ });
1964
+
1965
+ it("nudges the window by 5% of the visible span from the keyboard", async () => {
1966
+ // Domain [0, 2]: one arrow moves the window 0.1 (5% of the span), not the
1967
+ // unit axis's 0.05, so the visual step stays 5% of the strip.
1968
+ const host = mountHost([0.5, 1], { domain: [0, 2] });
1969
+ const strip = host.root.querySelector<HTMLElement>(".hk-range-ribbon-strip")!;
1970
+ keydown(strip, "ArrowRight");
1971
+ await nextTick();
1972
+ expectWindow(lastWindow(host.windowChange), 0.6, 1.1);
1973
+ // Shift+Arrow resizes from the trailing edge by the same step.
1974
+ keydown(strip, "ArrowRight", { shiftKey: true });
1975
+ await nextTick();
1976
+ expectWindow(lastWindow(host.windowChange), 0.6, 1.2);
1977
+ // The handle slider nudges its own edge by the same step.
1978
+ const start = host.root.querySelector<HTMLElement>('.hk-range-ribbon-handle[data-edge="start"]')!;
1979
+ keydown(start, "ArrowRight");
1980
+ await nextTick();
1981
+ expectWindow(lastWindow(host.windowChange), 0.7, 1.2);
1982
+ });
1983
+
1984
+ it("keeps the keyboard range entry inside the caller's domain", async () => {
1985
+ // Shift+ArrowLeft on the full view opens a window from the domain start,
1986
+ // stopped at `max(d0 + minWindow, d1 - step)` — a COORDINATE floor. The
1987
+ // bare-width version went dead on axes whose end sits below minWindow
1988
+ // and emitted sub-minimum windows on tight axes.
1989
+ const open = async (domain: [number, number]): Promise<[number, number] | null> => {
1990
+ const host = mountHost(null, { domain });
1991
+ keydown(host.root.querySelector<HTMLElement>(".hk-range-ribbon-strip")!, "ArrowLeft", {
1992
+ shiftKey: true,
1993
+ });
1994
+ await nextTick();
1995
+ return host.windowChange.mock.calls.length === 0 ? null : lastWindow(host.windowChange);
1996
+ };
1997
+
1998
+ // (a) An axis whose END is below the 0.02 floor: the entry must still
1999
+ // work (the old floor exceeded the domain end and returned early).
2000
+ expectWindow(await open([-1, 0]), -1, -0.05);
2001
+ // (b) A tight axis: the emitted width may never sit below minWindow (the
2002
+ // bare-width floor emitted 0.01995 against a 0.02 floor).
2003
+ const tight = await open([0.5, 0.521]);
2004
+ expectWindow(tight, 0.5, 0.52);
2005
+ expect(tight![1] - tight![0], "never narrower than minWindow").toBeGreaterThanOrEqual(0.02);
2006
+ // (c) The floor anchors at d0 + minWindow (-0.45), not at absolute 0.02.
2007
+ expectWindow(await open([-0.47, 0.03]), -0.47, 0.005);
2008
+ });
2009
+
2010
+ it("keeps a live brush inside a domain that changed mid-gesture", async () => {
2011
+ // The anchor is captured at pointerdown. If the host narrows the axis
2012
+ // while the pointer is down, the anchor must be re-clamped before the
2013
+ // window is built — otherwise the committed window escapes the new axis.
2014
+ const container = document.createElement("div");
2015
+ document.body.appendChild(container);
2016
+ const liveDomain = ref<[number, number]>([0, 1]);
2017
+ const win = ref<[number, number] | null>(null);
2018
+ const windowChange = vi.fn();
2019
+ const Wrapper = defineComponent({
2020
+ setup() {
2021
+ return () =>
2022
+ h(HkRangeRibbon, {
2023
+ lanes: LANES,
2024
+ segments: SEGMENTS,
2025
+ domain: liveDomain.value,
2026
+ window: win.value,
2027
+ onWindowChange: (next: [number, number] | null) => {
2028
+ windowChange(next);
2029
+ win.value = next;
2030
+ },
2031
+ });
2032
+ },
2033
+ });
2034
+ const app = createApp(Wrapper);
2035
+ app.mount(container);
2036
+ mounts.push({ app, container });
2037
+ const root = container.querySelector<HTMLElement>(".hk-range-ribbon")!;
2038
+ const { strip, x } = stubStrip(root);
2039
+
2040
+ // Press at 95% of the unit axis, then narrow the axis to [0, 0.5].
2041
+ pointer("pointerdown", x(0.95), strip);
2042
+ liveDomain.value = [0, 0.5];
2043
+ await nextTick();
2044
+ // Moving to half of the NEW axis must build [0.25, 0.5] (a stale 0.95
2045
+ // anchor would have built [0.25, 0.95], outside the new axis).
2046
+ pointer("pointermove", x(0.5), strip);
2047
+ pointer("pointerup", x(0.5), strip);
2048
+ await nextTick();
2049
+ expectWindow(lastWindow(windowChange), 0.25, 0.5);
2050
+ });
2051
+
2052
+ it("draws the caller's domain on minimal mode's bare bar", async () => {
2053
+ // Minimal mode suppresses the minimap lane rows and the whole detail
2054
+ // layer; the bare bar still lives in the overview container, and its axis
2055
+ // is the caller's domain.
2056
+ const rest = mountRibbon({ minimal: true, domain: [0, 2], window: [0, 2] });
2057
+ const restStart = rest.querySelector<HTMLElement>('.hk-range-ribbon-handle[data-edge="start"]')!;
2058
+ const restEnd = rest.querySelector<HTMLElement>('.hk-range-ribbon-handle[data-edge="end"]')!;
2059
+ expect(parseFloat(restStart.style.left), "domain start grip").toBeCloseTo(0, 3);
2060
+ expect(parseFloat(restEnd.style.left), "domain end grip").toBeCloseTo(100, 3);
2061
+ expect(restStart.getAttribute("aria-valuetext"), "labels read domain percents").toBe("0%");
2062
+ expect(restEnd.getAttribute("aria-valuetext")).toBe("200%");
2063
+
2064
+ // A sub-window is projected by the domain: [0.5, 1.5] of [0, 2] is
2065
+ // 25%..75%, and the scrims cover the domain the caller did not select.
2066
+ const sub = mountRibbon({ minimal: true, domain: [0, 2], window: [0.5, 1.5] });
2067
+ expect(
2068
+ parseFloat(
2069
+ sub.querySelector<HTMLElement>('.hk-range-ribbon-handle[data-edge="start"]')!.style.left,
2070
+ ),
2071
+ ).toBeCloseTo(25, 3);
2072
+ expect(
2073
+ parseFloat(
2074
+ sub.querySelector<HTMLElement>('.hk-range-ribbon-handle[data-edge="end"]')!.style.left,
2075
+ ),
2076
+ ).toBeCloseTo(75, 3);
2077
+ expectBox(
2078
+ sub.querySelector<HTMLElement>('.hk-range-ribbon-scrim[data-side="before"]'),
2079
+ { left: 0, width: 25 },
2080
+ "before scrim",
2081
+ );
2082
+ expectBox(
2083
+ sub.querySelector<HTMLElement>('.hk-range-ribbon-scrim[data-side="after"]'),
2084
+ { left: 75, width: 25 },
2085
+ "after scrim",
2086
+ );
2087
+
2088
+ // The pointer maps onto the same domain: axis 50% is coordinate 1.0, and
2089
+ // a press past the right edge clamps to the domain end 2.0.
2090
+ const host = mountHost([0.5, 1.5], { minimal: true, domain: [0, 2] });
2091
+ const { strip, x } = stubStrip(host.root);
2092
+ await drag(strip, 0.5, 1.4, x);
2093
+ expectWindow(lastWindow(host.windowChange), 1, 2);
2094
+ });
2095
+ });
2096
+
1360
2097
  // ── Stylesheet contract ─────────────────────────────────────────────────
1361
2098
  const componentDir = dirname(fileURLToPath(import.meta.url));
1362
2099
 
@@ -1511,6 +2248,17 @@ describe("HkRangeRibbon stylesheet contract", () => {
1511
2248
  expect(css).toContain(".hk-range-ribbon-overview-gutter");
1512
2249
  });
1513
2250
 
2251
+ it("makes the detail layer a plain column container for the lane list", () => {
2252
+ const detail = ruleBody(css, ".hk-range-ribbon-detail");
2253
+ expect(declaration(detail, "display")).toBe("flex");
2254
+ expect(declaration(detail, "flex-direction"), "the lane list keeps its own column").toBe(
2255
+ "column",
2256
+ );
2257
+ // The wrapper adds no rhythm of its own: the lanes keep their gap.
2258
+ expect(detail, "the wrapper owns no gap").not.toContain("gap:");
2259
+ expect(declaration(ruleBody(css, ".hk-range-ribbon-lanes"), "gap")).toBe("var(--space-2)");
2260
+ });
2261
+
1514
2262
  it("anchors the tip inward at the track edges", () => {
1515
2263
  const tip = ruleBody(css, ".hk-range-ribbon-tip");
1516
2264
  expect(declaration(tip, "text-align")).toBe("start");
@@ -1525,12 +2273,12 @@ describe("HkRangeRibbon stylesheet contract", () => {
1525
2273
  });
1526
2274
 
1527
2275
  // Wave (2026-11-06 raw-records five): MINIMAL MODE - the bare dual-handle
1528
- // bar. Contract: data-minimal on the root, strip present, ZERO lane chrome
1529
- // (no lanes block, no lane rows, no overview minimap, no gutter probe),
1530
- // and the standard mode keeps its lane chrome + alignment probe. The
1531
- // uncontrolled default window is NULL here — the grips need a controlled
1532
- // non-null window (the chest raw-records adapter feeds [0, 1] for its
1533
- // "default = everything selected" reading); the aria readings below come
2276
+ // bar. Contract: data-minimal on the root, strip present, no lane rows and
2277
+ // no detail layer (the bar still lives in the overview container); the
2278
+ // shared label gutter is still rendered and only display:none in this mode
2279
+ // (the strip spans the full width). The standard mode keeps its lane chrome
2280
+ // + alignment probe. The uncontrolled default window is NULL here — the
2281
+ // grips need a controlled non-null window; the aria readings below come
1534
2282
  // from the strip's null-window fallbacks, not from a real window.
1535
2283
  it("minimal mode renders the bare dual-handle bar (no lane chrome)", () => {
1536
2284
  const root = mountRibbon({ minimal: true });