@kitlangton/knobs 0.4.0-next.2 → 0.4.0-next.4

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.
package/dist/ui-react.js CHANGED
@@ -571,12 +571,20 @@ var compareCss = `
571
571
  display:flex;flex-direction:column;gap:6px;width:100%;height:100%;min-height:160px;outline:none;user-select:none;-webkit-user-select:none;
572
572
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
573
573
  :where(.knobs-ui) :is(.knobs-cmp-mode,.knobs-cmp-toggle){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;margin:0;padding:0;outline:none;box-shadow:none}
574
- .knobs-cmp-bar{flex:none;display:flex;align-items:center;gap:2px;height:24px;padding:0 2px}
575
- .knobs-cmp-modes{display:flex;align-items:center;gap:2px}
576
- .knobs-ui .knobs-cmp-mode,.knobs-ui .knobs-cmp-toggle{height:22px;padding:0 7px;border-radius:max(0px,calc(var(--_k-radius) - 2px));color:var(--_k-fg-3);cursor:pointer;white-space:nowrap;
577
- transition:color .15s,background .15s}
578
- .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible),.knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash)}
579
- .knobs-ui .knobs-cmp-mode[aria-checked="true"],.knobs-ui .knobs-cmp-toggle[aria-pressed="true"]{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
574
+ .knobs-cmp-bar{flex:none;display:flex;align-items:center;gap:var(--_k-gap);height:var(--_k-control-h)}
575
+ /* The modes: one fill, the selection pill on the one picked; a mode isn't brighter for being
576
+ picked: the pill's bright copy is. */
577
+ .knobs-cmp-modes{position:relative;display:inline-flex;height:var(--_k-control-h);border-radius:var(--_k-radius);background:var(--_k-fill)}
578
+ .knobs-cmp-modes[hidden]{display:none}
579
+ .knobs-ui .knobs-cmp-mode{display:inline-flex;align-items:center;height:100%;padding:0 8px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:pointer;white-space:nowrap;
580
+ transition:color calc(var(--_k-hover-out) * 1ms)}
581
+ .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible){color:var(--_k-fg)}
582
+ /* Toggles: icon buttons, the accent's wash when on. */
583
+ .knobs-ui .knobs-cmp-toggle{display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:var(--_k-radius);color:var(--_k-fg-3);cursor:pointer;
584
+ transition:color calc(var(--_k-hover-out) * 1ms),background calc(var(--_k-hover-out) * 1ms)}
585
+ .knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash)}
586
+ .knobs-ui .knobs-cmp-toggle[aria-pressed="true"]{color:var(--_k-accent-fg);background:var(--_k-accent-soft)}
587
+ .knobs-cmp-toggle svg{display:block}
580
588
  .knobs-cmp-fill{flex:1}
581
589
  .knobs-cmp-toggle[hidden]{display:none}
582
590
  /* One surface: on a card (a walkthrough's step, a split's pane, anything .knobs-surface) or marked
@@ -591,7 +599,8 @@ var compareCss = `
591
599
  /* Unzoomed, a finger still scrolls the page up and down; zoomed, the stage takes every gesture. */
592
600
  .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:grab;touch-action:none}
593
601
  .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:grabbing!important}
594
- .knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:ew-resize!important}
602
+ /* Wiping, the grip stands in for the cursor. */
603
+ .knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:none!important}
595
604
  .knobs-cmp-sizer{position:absolute;left:0;right:0;top:0;height:0;visibility:hidden;pointer-events:none}
596
605
  .knobs-cmp-canvas{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
597
606
  .knobs-cmp-media{position:absolute;left:0;top:0;display:block;max-width:none;margin:0;padding:0;border:0;object-fit:fill;transform-origin:0 0;pointer-events:none;will-change:transform}
@@ -599,18 +608,26 @@ var compareCss = `
599
608
  /* Glass: what's behind shows through, blurred, on the knobs tokens. */
600
609
  .knobs-cmp-label,.knobs-cmp-zoom{background:color-mix(in srgb,var(--_k-bg) 52%,transparent);-webkit-backdrop-filter:blur(14px) saturate(1.6);backdrop-filter:blur(14px) saturate(1.6);
601
610
  box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim),0 2px 10px rgba(0,0,0,calc(.18 * var(--_k-shadow-k,1)))}
602
- .knobs-cmp-label{position:absolute;left:0;top:0;padding:1px 7px;border-radius:999px;font-size:var(--_k-text-meta);line-height:16px;color:var(--_k-fg-2);white-space:nowrap;pointer-events:none;will-change:transform;transition:color .15s}
603
- .knobs-cmp-label[data-lit]{color:var(--_k-fg)}
604
- .knobs-cmp-label[data-dim]{color:var(--_k-fg-3)}
605
- /* The wipe: a hairline down the line with a grip at its middle; a wider strip takes the hand and
606
- the keys (a slider). It fades with the mode. */
611
+ /* ✗ Before · ✓ After: Before quieter (fg-3), After in full text. Neutral: no colour of its own. */
612
+ .knobs-cmp-label{position:absolute;left:0;top:0;display:flex;align-items:center;gap:4px;padding:1px 7px 1px 5px;border-radius:999px;font-size:var(--_k-text-meta);line-height:16px;font-weight:var(--_k-weight-label);
613
+ white-space:nowrap;pointer-events:none;will-change:transform}
614
+ .knobs-cmp-label[data-side="before"]{color:var(--_k-fg-3)}
615
+ .knobs-cmp-label[data-side="after"]{color:var(--_k-fg)}
616
+ .knobs-cmp-mark svg{display:block;width:12px;height:12px}
617
+ /* The wipe: a hairline down the line with a raised grip at the pointer's height; a wider strip
618
+ takes the hand and the keys (a slider). It fades with the mode; wiping, line and grip brighten. */
607
619
  .knobs-cmp-handle{cursor:ew-resize;position:absolute;left:-12px;top:0;width:24px;height:0;opacity:0;pointer-events:none;outline:none;will-change:transform;transition:opacity .18s cubic-bezier(.4,0,1,1)}
608
620
  .knobs-cmp-handle[data-on]{opacity:1;pointer-events:auto;transition:opacity .22s cubic-bezier(.2,.8,.2,1)}
609
- .knobs-cmp-handle::before{content:"";position:absolute;top:0;bottom:0;left:11.5px;width:1px;background:var(--_ku-cmp-line);box-shadow:0 0 0 .5px rgba(0,0,0,.28),0 0 6px rgba(0,0,0,.25)}
610
- .knobs-cmp-handle::after{content:"";position:absolute;top:50%;left:9px;width:6px;height:28px;margin-top:-14px;border-radius:3px;background:var(--_ku-cmp-line);
611
- box-shadow:0 1px 3px rgba(0,0,0,.35),0 0 0 .5px rgba(0,0,0,.2);transition:scale .18s var(--_ku-ease),box-shadow .15s}
612
- .knobs-compare[data-dragging] .knobs-cmp-handle::after{scale:1.12}
613
- .knobs-cmp-handle:focus-visible::after,.knobs-compare[data-keyboard] .knobs-cmp-handle:focus::after{box-shadow:0 1px 3px rgba(0,0,0,.35),0 0 0 3px color-mix(in srgb,var(--_k-accent) 40%,transparent)}
621
+ .knobs-cmp-handle::before{content:"";position:absolute;top:0;bottom:0;left:11.5px;width:1px;background:color-mix(in srgb,var(--_ku-cmp-line) 72%,transparent);box-shadow:0 0 0 .5px rgba(0,0,0,.28),0 0 6px rgba(0,0,0,.25)}
622
+ .knobs-compare[data-dragging] .knobs-cmp-handle::before{background:var(--_ku-cmp-line)}
623
+ .knobs-cmp-grip{position:absolute;top:0;left:4px;width:16px;height:28px;border-radius:min(8px,calc(var(--_k-radius) * 999));background:var(--_k-bg-overlay);
624
+ box-shadow:var(--_k-shadow-overlay),inset 0 0 0 1px var(--_k-border),inset 0 1px 0 var(--_k-rim);will-change:transform;transition:box-shadow calc(var(--_k-hover-out) * 1ms)}
625
+ .knobs-cmp-grip::before,.knobs-cmp-grip::after{content:"";position:absolute;top:10px;width:1px;height:8px;background:var(--_k-fg-3);transition:background calc(var(--_k-hover-out) * 1ms)}
626
+ .knobs-cmp-grip::before{left:6px}
627
+ .knobs-cmp-grip::after{left:9px}
628
+ .knobs-compare[data-dragging] .knobs-cmp-grip{box-shadow:var(--_k-shadow-overlay),inset 0 0 0 1px var(--_k-border-strong),inset 0 1px 0 var(--_k-rim)}
629
+ .knobs-compare[data-dragging] .knobs-cmp-grip::before,.knobs-compare[data-dragging] .knobs-cmp-grip::after{background:var(--_k-fg)}
630
+ .knobs-cmp-handle:focus-visible .knobs-cmp-grip,.knobs-compare[data-keyboard] .knobs-cmp-handle:focus .knobs-cmp-grip{box-shadow:var(--_k-shadow-overlay),0 0 0 2px var(--_k-accent)}
614
631
  /* The split's hairline, down the middle side by side. */
615
632
  .knobs-cmp-split{position:absolute;left:0;top:0;width:1px;height:0;margin-left:-.5px;background:color-mix(in srgb,var(--_k-bg) 72%,transparent);pointer-events:none;will-change:transform,opacity}
616
633
  /* Zoomed: how far, and a way back, in the stage's corner; it fades in and out, nothing shifts. */
@@ -619,12 +636,12 @@ var compareCss = `
619
636
  opacity:0;scale:.96;pointer-events:none;transition:opacity .16s cubic-bezier(.4,0,1,1),scale .16s cubic-bezier(.4,0,1,1)}
620
637
  .knobs-cmp-zoom[data-on]{opacity:1;scale:1;pointer-events:auto;transition:opacity .2s cubic-bezier(.2,.8,.2,1),scale .2s cubic-bezier(.2,.8,.2,1)}
621
638
  :where(.knobs-ui) .knobs-cmp-zoom-reset{all:revert;box-sizing:border-box;font:inherit;letter-spacing:inherit;margin:0;border:0;outline:none;box-shadow:none;
622
- height:20px;padding:0 8px;border-radius:999px;background:transparent;color:var(--_k-fg-2);cursor:pointer;transition:color .15s,background .15s}
639
+ display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border-radius:999px;background:transparent;color:var(--_k-fg-2);cursor:pointer;transition:color .15s,background .15s}
623
640
  :where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){color:var(--_k-fg);background:color-mix(in srgb,var(--_k-fg) 12%,transparent)}
624
641
  .knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);font-size:var(--_k-text-meta);pointer-events:none}
625
642
  .knobs-cmp-note:empty{display:none}
626
643
  .knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
627
- .knobs-cmp-timeline-host{flex:none;height:56px;min-height:0}
644
+ .knobs-cmp-timeline-host{flex:none;min-height:0}
628
645
  /* The frame strip: Before over After per column, a difference bar under them, a dot where it peaks. */
629
646
  .knobs-ui.knobs-cmp-strip{--_ku-cmp-diff:var(--k-compare-diff,var(--_k-ok));position:relative;width:100%;height:100%;min-height:40px;overflow:hidden;cursor:col-resize;touch-action:none;
630
647
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg)}
@@ -767,7 +784,7 @@ var init_picker_css = __esm(() => {
767
784
  .knobs-picker-suggested{flex:none;color:var(--_k-fg-3);font-size:var(--_k-text-meta)}
768
785
  .knobs-picker-check{flex:none;display:flex;width:14px;height:14px;color:var(--_k-accent);opacity:0;scale:.6;filter:blur(2px);
769
786
  transition:opacity .11s ease-in,scale .11s ease-in,filter .11s ease-in}
770
- .knobs-picker-check svg{display:block;width:14px;height:14px}
787
+ .knobs-picker-check svg{display:block;width:12px;height:12px}
771
788
  .knobs-picker-option[data-chosen] .knobs-picker-check{opacity:1;scale:1;filter:none;transition:opacity .15s ease-out,scale var(--kp-snappy-ms) var(--kp-snappy),filter .15s ease-out}
772
789
  .knobs-picker-body{position:relative;overflow:visible}
773
790
  .knobs-picker-body[data-moving]{overflow:hidden}
@@ -871,6 +888,15 @@ var readoutCss = `
871
888
  @media(prefers-reduced-motion:reduce){.knobs-rd :where(*){transition:none!important}}
872
889
  `;
873
890
 
891
+ // src/ui/selection-pill-css.ts
892
+ var selectionPillCss = `
893
+ .knobs-pick-pill,.knobs-pick-copy{position:absolute;left:0;top:0;width:0;height:0;pointer-events:none;transition:opacity .1s linear}
894
+ .knobs-pick-pill{background:var(--_k-fill-selected);will-change:transform}
895
+ .knobs-pick-copy{overflow:hidden;will-change:transform}
896
+ .knobs-pick-copy-rows{position:absolute;left:0;top:0;width:0;height:0}
897
+ :is(.knobs-pick-pill,.knobs-pick-copy)[data-off]{opacity:0}
898
+ `;
899
+
874
900
  // src/tokens/color.ts
875
901
  function linearToOklch([r, g, b, a]) {
876
902
  const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
@@ -1311,6 +1337,22 @@ var init_schema = __esm(() => {
1311
1337
  source: css("var(--k-warn)"),
1312
1338
  doc: "Modified lines."
1313
1339
  },
1340
+ {
1341
+ name: "before",
1342
+ tier: "semantic",
1343
+ type: "color",
1344
+ group: "Data",
1345
+ source: css("var(--k-fg-3)"),
1346
+ doc: "Before in a comparison, and only Before: its label and side of the seam. Neutral unless a theme gives it a colour."
1347
+ },
1348
+ {
1349
+ name: "after",
1350
+ tier: "semantic",
1351
+ type: "color",
1352
+ group: "Data",
1353
+ source: css("var(--k-fg)"),
1354
+ doc: "After in a comparison, and only After."
1355
+ },
1314
1356
  {
1315
1357
  name: "series-1",
1316
1358
  tier: "semantic",
@@ -1401,6 +1443,150 @@ var init_schema = __esm(() => {
1401
1443
  source: value("288px"),
1402
1444
  doc: "Panel width."
1403
1445
  },
1446
+ {
1447
+ name: "space-inside",
1448
+ tier: "semantic",
1449
+ type: "length",
1450
+ group: "Space",
1451
+ source: css("var(--k-unit)"),
1452
+ doc: "Parts of one thing: an icon and its label, a meta line and its title."
1453
+ },
1454
+ {
1455
+ name: "space-sibling",
1456
+ tier: "semantic",
1457
+ type: "length",
1458
+ group: "Space",
1459
+ source: css("calc(var(--k-unit) * 2)"),
1460
+ doc: "Siblings in a group: buttons in a row, a list item's lines."
1461
+ },
1462
+ {
1463
+ name: "space-title",
1464
+ tier: "semantic",
1465
+ type: "length",
1466
+ group: "Space",
1467
+ source: css("calc(var(--k-unit) * 3)"),
1468
+ doc: "A heading and what it heads."
1469
+ },
1470
+ {
1471
+ name: "space-group",
1472
+ tier: "semantic",
1473
+ type: "length",
1474
+ group: "Space",
1475
+ source: css("calc(var(--k-unit) * 12)"),
1476
+ doc: "Groups inside a section (prose, video, diff), stacked media, feed items (a hairline in the middle)."
1477
+ },
1478
+ {
1479
+ name: "space-section",
1480
+ tier: "semantic",
1481
+ type: "length",
1482
+ group: "Space",
1483
+ source: css("calc(var(--k-unit) * 24)"),
1484
+ doc: "Sections of a page (the intro, the updates, each PR), a hairline in the middle."
1485
+ },
1486
+ {
1487
+ name: "space-column",
1488
+ tier: "semantic",
1489
+ type: "length",
1490
+ group: "Space",
1491
+ source: css("calc(var(--k-unit) * 6)"),
1492
+ doc: "Columns: a contents rail and the page, media side by side."
1493
+ },
1494
+ {
1495
+ name: "pad-page",
1496
+ tier: "semantic",
1497
+ type: "length",
1498
+ group: "Space",
1499
+ source: css("calc(var(--k-unit) * 6)"),
1500
+ doc: "A page's edge; 16px under 640px wide."
1501
+ },
1502
+ {
1503
+ name: "pad-card",
1504
+ tier: "semantic",
1505
+ type: "length",
1506
+ group: "Space",
1507
+ source: css("calc(var(--k-unit) * 3)"),
1508
+ doc: "Inside a card or popover."
1509
+ },
1510
+ {
1511
+ name: "type-heading",
1512
+ tier: "semantic",
1513
+ type: "length",
1514
+ group: "Type",
1515
+ source: value("20px"),
1516
+ doc: "Section headings (a PR's title, under its kicker), weight 500."
1517
+ },
1518
+ {
1519
+ name: "leading-heading",
1520
+ tier: "semantic",
1521
+ type: "length",
1522
+ group: "Type",
1523
+ source: value("28px"),
1524
+ doc: "Line height for type-heading."
1525
+ },
1526
+ {
1527
+ name: "type-title",
1528
+ tier: "semantic",
1529
+ type: "length",
1530
+ group: "Type",
1531
+ source: value("16px"),
1532
+ doc: "A page's title, weight 500."
1533
+ },
1534
+ {
1535
+ name: "leading-title",
1536
+ tier: "semantic",
1537
+ type: "length",
1538
+ group: "Type",
1539
+ source: value("24px"),
1540
+ doc: "Line height for type-title."
1541
+ },
1542
+ {
1543
+ name: "type-body",
1544
+ tier: "semantic",
1545
+ type: "length",
1546
+ group: "Type",
1547
+ source: value("13px"),
1548
+ doc: "Prose, questions, list rows, step titles; 400, or 500 for emphasis."
1549
+ },
1550
+ {
1551
+ name: "leading-body",
1552
+ tier: "semantic",
1553
+ type: "length",
1554
+ group: "Type",
1555
+ source: value("20px"),
1556
+ doc: "Line height for type-body."
1557
+ },
1558
+ {
1559
+ name: "type-label",
1560
+ tier: "semantic",
1561
+ type: "length",
1562
+ group: "Type",
1563
+ source: value("12px"),
1564
+ doc: "Labels over media, weight 500; mono inside body text."
1565
+ },
1566
+ {
1567
+ name: "leading-label",
1568
+ tier: "semantic",
1569
+ type: "length",
1570
+ group: "Type",
1571
+ source: value("16px"),
1572
+ doc: "Line height for type-label."
1573
+ },
1574
+ {
1575
+ name: "type-small",
1576
+ tier: "semantic",
1577
+ type: "length",
1578
+ group: "Type",
1579
+ source: value("11px"),
1580
+ doc: "Meta, controls, keys, timestamps; mono inside labels."
1581
+ },
1582
+ {
1583
+ name: "leading-small",
1584
+ tier: "semantic",
1585
+ type: "length",
1586
+ group: "Type",
1587
+ source: value("16px"),
1588
+ doc: "Line height for type-small."
1589
+ },
1404
1590
  {
1405
1591
  name: "font-size",
1406
1592
  tier: "semantic",
@@ -2365,16 +2551,18 @@ function injectTokens(doc) {
2365
2551
  style.textContent = rootCss;
2366
2552
  doc.head.append(style);
2367
2553
  }
2368
- var ROOT_CLASS = "knobs-root", extras, spring, rootCss;
2554
+ var ROOT_CLASS = "knobs-root", extras, narrow, spring, rootCss;
2369
2555
  var init_root = __esm(() => {
2370
2556
  init_knobs();
2371
2557
  init_css();
2372
2558
  init_glide();
2373
2559
  init_motion();
2374
2560
  extras = `:where(.${ROOT_CLASS}){--_k-shadow-k:calc(var(--k-shadow-strength,calc(.48 - .32 * var(--_k-light,0))) / .48);--_k-glass-tone:var(--k-glass-tone,calc(.5 + .58 * var(--_k-light,0)));--_k-fill-base:var(--k-fill-base,var(--_k-fill-selected));--_k-fill-hover-mix:var(--k-fill-hover-mix,var(--k-fill-rest-mix,20%));--_k-reveal-ease:var(--k-ease,cubic-bezier(.2,.8,.2,1))}`;
2561
+ narrow = `@media(max-width:639.98px){:where(.${ROOT_CLASS}){--_k-pad-page:var(--k-pad-page,calc(var(--_k-unit) * 4))}}`;
2375
2562
  spring = `:where(:root,:host){--k-fill-ease:${DEFAULT_LINEAR.easing};--k-fill-ms:${DEFAULT_LINEAR.ms}}`;
2376
2563
  rootCss = `${tokensCss(knobs)}
2377
2564
  ${extras}
2565
+ ${narrow}
2378
2566
  ${spring}
2379
2567
  ${DRIVERS}
2380
2568
  ${motionCss}`;
@@ -2585,6 +2773,12 @@ var css2 = `
2585
2773
  --_ku-pane-bg and --_ku-pane-ring so this reaches them. */
2586
2774
  :where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-ui:not(.knobs-surface),.knobs-ui[data-nested]{--_ku-gap:var(--k-ui-gap,0px);--_ku-pane-bg:transparent;--_ku-pane-ring:none;
2587
2775
  --_ku-divider:var(--_k-split-gutter);--_kd-bg:transparent}
2776
+ /* What draws its box without those: a picker's card keeps its fill as the highlight but drops its
2777
+ ring; a timeline's track drops its well for a hairline under the lane; an inline panel sheds its
2778
+ card (--k-chrome 0). A full-window step is the one raised exception (it is the surface). */
2779
+ :is(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-picker-card,.knobs-ui[data-nested] .knobs-picker-card{box-shadow:none}
2780
+ :is(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-tl-track,.knobs-ui[data-nested] .knobs-tl-track{background:transparent;border-radius:0;box-shadow:inset 0 -1px 0 var(--_k-border-subtle)}
2781
+ :where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-panel.knobs-inline:not(.knobs-page){--k-chrome:0}
2588
2782
  /* Only the pieces' own elements take their type and resets: what you put in a pane is yours. */
2589
2783
  .knobs-ui{box-sizing:border-box}
2590
2784
  :where(.knobs-split>*,.knobs-pane-clip,.knobs-pane-inner,.knobs-pane-body,.knobs-pane-head,.knobs-pane-head *,.knobs-pane-rail,.knobs-pane-rail *,.knobs-tree,.knobs-tree *,.knobs-tree-ghost){box-sizing:border-box}
@@ -2719,11 +2913,9 @@ var css2 = `
2719
2913
  .knobs-tree:focus .knobs-tree-row[data-active][aria-selected="true"]::before{background:var(--_ku-select-focus)}
2720
2914
  .knobs-tree-row[data-drop="into"]::before{opacity:1;background:color-mix(in srgb,var(--_k-accent) 30%,transparent)}
2721
2915
  .knobs-tree-row[data-dragged]{opacity:.38}
2722
- /* glide: one highlight under the selected row that glides between rows (the row's own stays off). */
2723
- .knobs-tree-cursor{position:absolute;left:var(--_ku-row-x);right:var(--_ku-row-x);top:0;height:var(--_k-row);border-radius:var(--_ku-row-radius);background:var(--_ku-select);
2724
- opacity:0;pointer-events:none;will-change:transform;transition:opacity .15s ease-out,background .12s}
2725
- .knobs-tree-cursor[data-on]{opacity:1}
2726
- .knobs-tree:focus .knobs-tree-cursor{background:var(--_ku-select-focus)}
2916
+ /* glide: the selection pill on the selected row, in the selection's colour (the row's own stays off). */
2917
+ .knobs-tree>.knobs-pick-pill{background:var(--_ku-select)}
2918
+ .knobs-tree:focus>.knobs-pick-pill{background:var(--_ku-select-focus)}
2727
2919
  .knobs-tree[data-glide] .knobs-tree-row[aria-selected="true"]::before{opacity:0!important}
2728
2920
  .knobs-tree-row>*{position:relative}
2729
2921
  .knobs-tree-caret{flex:none;display:flex;align-items:center;justify-content:center;width:16px;height:100%;color:var(--_k-fg-3);transition:transform var(--_ku-ease-ms) var(--_ku-ease)}
@@ -2755,22 +2947,20 @@ var css2 = `
2755
2947
  box-shadow:0 0 0 1px var(--_k-border-strong),0 6px 18px rgba(0,0,0,calc(.35 * var(--_k-shadow-k,1)));pointer-events:none;white-space:nowrap;font:var(--_k-weight) var(--_k-font-size)/1.6 var(--_k-font)}
2756
2948
  .knobs-tree-empty{padding:10px var(--_k-inset);color:var(--_k-fg-3)}
2757
2949
 
2758
- /* Timeline: a transport (play, the time) beside a track: the ruler's canvas over a lane of
2759
- markers, ranges as soft bands, and the playhead, a hairline in the accent with a cap. */
2950
+ /* Timeline: play beside a track: the ruler's canvas over a lane of markers, ranges as soft bands,
2951
+ and the playhead, a hairline in the accent with a cap. No clock: the time shows under the
2952
+ pointer on the track (the tag), in seconds, while hovering or scrubbing. */
2760
2953
  :where(.knobs-timeline,.knobs-timeline *){box-sizing:border-box}
2761
2954
  .knobs-ui.knobs-timeline{display:flex;align-items:stretch;gap:10px;width:100%;height:100%;min-height:52px;padding:6px var(--_k-inset) 8px 8px;outline:none;user-select:none;-webkit-user-select:none;
2762
2955
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
2763
2956
  :where(.knobs-ui) :is(.knobs-tl-play,.knobs-tl-edit){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;margin:0;padding:0;outline:none;box-shadow:none}
2764
- .knobs-tl-transport{flex:none;display:flex;align-items:center;gap:8px;min-width:96px}
2957
+ .knobs-tl-transport{flex:none;display:flex;align-items:center}
2765
2958
  .knobs-ui .knobs-tl-play{flex:none;display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);min-width:22px;min-height:22px;border-radius:calc(var(--_k-radius) * 999);
2766
2959
  background:var(--_k-fill-selected);color:var(--_k-fg);cursor:pointer;transition:background .15s,transform .15s var(--_ku-ease)}
2767
2960
  .knobs-ui .knobs-tl-play:hover{background:color-mix(in srgb,var(--_k-fg) 16%,transparent)}
2768
2961
  .knobs-ui .knobs-tl-play:active{transform:scale(.94)}
2769
2962
  .knobs-tl-play svg{width:12px;height:12px}
2770
2963
  .knobs-timeline[data-playing] .knobs-tl-play{background:var(--_k-accent);color:var(--_k-bg)}
2771
- .knobs-tl-time{display:flex;flex-direction:column;line-height:1.2;font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}
2772
- .knobs-tl-now{font:var(--_k-weight) var(--_k-value-size)/1.2 var(--_k-value-font);font-variant-numeric:tabular-nums;font-feature-settings:"tnum";color:var(--_k-fg);white-space:nowrap}
2773
- .knobs-tl-sub{font-size:var(--_k-text-meta);color:var(--_k-fg-3);white-space:nowrap}
2774
2964
  .knobs-tl-track{position:relative;flex:1;min-width:0;overflow:hidden;border-radius:var(--_k-radius);background:var(--_k-fill);cursor:col-resize;touch-action:none;
2775
2965
  transition:background calc(var(--_k-hover-out) * 1ms)}
2776
2966
  .knobs-timeline:focus-visible .knobs-tl-track,.knobs-timeline[data-keyboard]:focus .knobs-tl-track{background:color-mix(in srgb,var(--_k-fill-selected) 80%,transparent)}
@@ -2794,7 +2984,8 @@ var css2 = `
2794
2984
  .knobs-tl-pin{position:absolute;top:13px;left:-5px;width:10px;height:10px;pointer-events:auto;cursor:grab}
2795
2985
  .knobs-tl-pin::after{content:"";position:absolute;inset:1.5px;border-radius:min(2px,var(--_k-radius));background:var(--knobs-tl-c);rotate:45deg;transition:background .15s,scale .2s var(--_ku-ease)}
2796
2986
  .knobs-tl-mlabel{position:absolute;top:26px;left:3px;max-width:140px;padding:0 6px;border-radius:max(0px,calc(var(--_k-radius) - 3px));background:var(--_k-fill-selected);color:var(--_k-fg);
2797
- font-size:var(--_k-text-meta);line-height:17px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:auto;cursor:grab;transition:background .15s,color .15s}
2987
+ font-size:var(--_k-text-meta);line-height:17px;white-space:nowrap;pointer-events:auto;cursor:grab;transition:background .15s,color .15s}
2988
+ .knobs-tl-mtext{display:block;overflow:hidden;white-space:nowrap}
2798
2989
  .knobs-tl-marker[data-selected]{--knobs-tl-c:var(--_k-accent)}
2799
2990
  .knobs-tl-marker[data-selected] .knobs-tl-mlabel{background:color-mix(in srgb,var(--_k-accent) 24%,var(--_k-fill-selected))}
2800
2991
  .knobs-tl-marker[data-selected] .knobs-tl-pin::after{scale:1.2}
@@ -2815,7 +3006,22 @@ var css2 = `
2815
3006
  .knobs-timeline[data-scrubbing] .knobs-tl-hover{opacity:0}
2816
3007
  @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
2817
3008
  @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
2818
- `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
3009
+ .knobs-tl-tag{position:absolute;left:0;bottom:2px;font-size:var(--_k-text-meta);line-height:16px;color:var(--_k-fg);font-variant-numeric:tabular-nums;font-feature-settings:"tnum";white-space:nowrap;pointer-events:none;opacity:0}
3010
+ .knobs-tl-tag[data-on]{opacity:1}
3011
+ /* The lane (variant: "lane"): one slim track of ticks under a compare or a video; markers are
3012
+ faint chapter ticks, the current one in text colour; nothing to edit. */
3013
+ .knobs-ui.knobs-timeline[data-variant="lane"]{height:auto;min-height:0;padding:0;gap:var(--_k-gap)}
3014
+ .knobs-timeline[data-variant="lane"] .knobs-tl-track{height:var(--_k-control-h)}
3015
+ .knobs-timeline[data-variant="lane"] .knobs-tl-ruler{height:100%}
3016
+ .knobs-timeline[data-variant="lane"] .knobs-tl-lane{top:0}
3017
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker{top:0}
3018
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker::before{top:0;bottom:50%;left:-.75px;width:1.5px;background:var(--_k-fg-3)}
3019
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker[data-current]::before{background:var(--_k-fg)}
3020
+ .knobs-timeline[data-variant="lane"] :is(.knobs-tl-pin,.knobs-tl-mlabel,.knobs-tl-rlabel){display:none}
3021
+ .knobs-timeline[data-variant="lane"] .knobs-tl-head::before{top:0}
3022
+ .knobs-timeline[data-variant="lane"] .knobs-tl-head::after{content:none}
3023
+ .knobs-timeline[data-variant="lane"] .knobs-tl-tag{top:2px;bottom:auto}
3024
+ `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + selectionPillCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
2819
3025
  s.omega = m.omega;
2820
3026
  s.zeta = m.zeta;
2821
3027
  return s;
@@ -2973,33 +3179,29 @@ var init_diff_css = __esm(() => {
2973
3179
  .knobs-diff-fill{flex:1}
2974
3180
  .knobs-diff-nav{flex:none;display:inline-flex;align-items:center;gap:1px;color:var(--_k-fg-3);font-size:var(--_k-text-meta);font-variant-numeric:tabular-nums}
2975
3181
  .knobs-diff-nav>span{min-width:3.6em;text-align:center}
2976
- :where(.knobs-ui) :is(.knobs-diff-btn,.knobs-diff-seg>button,.knobs-diff-viewed,.knobs-diff-filter>input,.knobs-diff-filter>button){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;border-radius:0;
3182
+ :where(.knobs-ui) :is(.knobs-diff-btn,.knobs-diff-choice,.knobs-diff-viewed,.knobs-diff-filter>input,.knobs-diff-filter>button){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;border-radius:0;
2977
3183
  margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
2978
- .knobs-ui .knobs-diff-btn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:calc(var(--_k-radius) * 999);color:var(--_k-fg-3);cursor:pointer;
3184
+ .knobs-ui .knobs-diff-btn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:calc(var(--_k-radius) * 999);color:var(--_k-fg-3);cursor:pointer;
2979
3185
  transition:background .15s,color .15s,opacity .15s}
2980
3186
  .knobs-ui .knobs-diff-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
2981
3187
  .knobs-ui .knobs-diff-btn[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
2982
3188
  .knobs-ui .knobs-diff-btn:disabled{opacity:.35;cursor:default;background:transparent}
2983
- .knobs-diff-btn svg{width:13px;height:13px}
3189
+ .knobs-diff-btn svg{width:16px;height:16px}
2984
3190
  /* Viewed: a check with its word, as on GitHub; the path dims once it's ticked. */
2985
3191
  .knobs-ui .knobs-diff-viewed{flex:none;display:inline-flex;align-items:center;gap:5px;height:var(--_k-control-h);padding:0 8px 0 6px;border-radius:var(--kd-ctl);color:var(--_k-fg-2);
2986
3192
  font-size:var(--_k-text-meta);font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:background .15s,color .15s}
2987
3193
  .knobs-ui .knobs-diff-viewed:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
2988
3194
  .knobs-ui .knobs-diff-viewed[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
2989
- .knobs-diff-viewed svg{width:13px;height:13px}
3195
+ .knobs-diff-viewed svg{width:16px;height:16px}
2990
3196
  .knobs-diff-viewed[aria-pressed="true"] svg{color:var(--kd-add)}
2991
3197
  .knobs-diff-path{transition:opacity .2s}
2992
3198
  .knobs-diff[data-viewed]>.knobs-diff-head .knobs-diff-path,.knobs-diff-part[data-viewed] .knobs-diff-path{opacity:.6}
2993
- /* Split | Unified: one pill that glides between the two, raised as a panel's segments are (a
2994
- lighter bg in dark, near white in light). */
2995
- .knobs-diff-seg{position:relative;flex:none;display:grid;grid-template-columns:1fr 1fr;height:var(--_k-control-h);padding:2px;border-radius:var(--kd-ctl);background:var(--_k-segmented-track)}
2996
- .knobs-ui .knobs-diff-seg>button{position:relative;z-index:1;padding:0 9px;border-radius:max(0px,calc(var(--kd-ctl) - 2px));color:var(--_k-fg-2);font-size:var(--_k-text-meta);
2997
- font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:color .15s}
2998
- .knobs-ui .knobs-diff-seg>button:is(:hover,:focus-visible),.knobs-ui .knobs-diff-seg>button[aria-checked="true"]{color:var(--_k-fg)}
2999
- .knobs-diff-pill{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:max(0px,calc(var(--kd-ctl) - 2px));background:color-mix(in srgb,var(--_k-accent) 18%,var(--_k-segmented-active));
3000
- box-shadow:0 1px 2px rgba(0,0,0,calc(.16 * var(--k-lift,1)));transition:transform var(--_ku-ease-ms) var(--_ku-ease)}
3001
- .knobs-diff-seg[data-value="unified"]>.knobs-diff-pill{transform:translateX(100%)}
3002
- .knobs-diff[data-frame="flat"] .knobs-diff-pill{box-shadow:none}
3199
+ /* Split | Unified: the selection pill on the one picked, its bright copy the picked one's text; a
3200
+ choice isn't brighter for being picked. Choices are styled by class, as the copy holds them too. */
3201
+ .knobs-diff-seg{position:relative;flex:none;display:grid;grid-template-columns:1fr 1fr;height:var(--_k-control-h);padding:2px;border-radius:var(--kd-ctl);background:var(--_k-fill)}
3202
+ .knobs-ui .knobs-diff-choice{padding:0 9px;border-radius:max(0px,calc(var(--kd-ctl) - 2px));color:var(--_k-fg-2);font-size:var(--_k-text-meta);
3203
+ font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:color calc(var(--_k-hover-out) * 1ms)}
3204
+ .knobs-ui .knobs-diff-choice:is(:hover,:focus-visible){color:var(--_k-fg)}
3003
3205
  /* The diff: one layer per file or layout on show, the old fading under the new. */
3004
3206
  .knobs-diff-body{position:relative;flex:1;min-height:0}
3005
3207
  .knobs-diff-probe{position:absolute;left:0;top:0;width:0;height:var(--kd-line);visibility:hidden;pointer-events:none}
@@ -3094,7 +3296,7 @@ var init_diff_css = __esm(() => {
3094
3296
  .knobs-ui .knobs-diff-filter>input::placeholder{color:var(--_k-fg-3)}
3095
3297
  .knobs-ui .knobs-diff-filter>button{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:999px;color:var(--_k-fg-3);cursor:pointer;transition:background .12s,color .12s,opacity .12s}
3096
3298
  .knobs-ui .knobs-diff-filter>button:hover{background:var(--_ku-wash-hi);color:var(--_k-fg)}
3097
- .knobs-diff-filter>button svg{width:10px;height:10px}
3299
+ .knobs-diff-filter>button svg{width:12px;height:12px}
3098
3300
  .knobs-diff-list{position:relative;flex:1;min-height:0}
3099
3301
  .knobs-diff-files .knobs-diff-counts{font-size:var(--_k-text-meta);align-items:center}
3100
3302
  .knobs-diff-files .knobs-diff-counts{gap:5px}
@@ -3861,6 +4063,283 @@ var init_rgba = __esm(() => {
3861
4063
  cache = new Map;
3862
4064
  });
3863
4065
 
4066
+ // src/ui/icons.ts
4067
+ function icon(name, options = {}) {
4068
+ const size = options.small ? 12 : 16;
4069
+ return `<svg class="knobs-icon" viewBox="0 0 256 256" width="${size}" height="${size}" fill="currentColor" aria-hidden="true" focusable="false"><path d="${PATHS[name]}"/></svg>`;
4070
+ }
4071
+ var PATHS, ICON_NAMES;
4072
+ var init_icons = __esm(() => {
4073
+ PATHS = {
4074
+ arrowLeft: "M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z",
4075
+ arrowUpRight: "M200,64V168a8,8,0,0,1-16,0V83.31L69.66,197.66a8,8,0,0,1-11.32-11.32L172.69,72H88a8,8,0,0,1,0-16H192A8,8,0,0,1,200,64Z",
4076
+ arrowSquareOut: "M224,104a8,8,0,0,1-16,0V59.32l-66.33,66.34a8,8,0,0,1-11.32-11.32L196.68,48H152a8,8,0,0,1,0-16h64a8,8,0,0,1,8,8Zm-40,24a8,8,0,0,0-8,8v72H48V80h72a8,8,0,0,0,0-16H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V136A8,8,0,0,0,184,128Z",
4077
+ arrowCounterClockwise: "M224,128a96,96,0,0,1-94.71,96H128A95.38,95.38,0,0,1,62.1,197.8a8,8,0,0,1,11-11.63A80,80,0,1,0,71.43,71.39a3.07,3.07,0,0,1-.26.25L44.59,96H72a8,8,0,0,1,0,16H24a8,8,0,0,1-8-8V56a8,8,0,0,1,16,0V85.8L60.25,60A96,96,0,0,1,224,128Z",
4078
+ arrowUDownLeft: "M232,112a64.07,64.07,0,0,1-64,64H51.31l34.35,34.34a8,8,0,0,1-11.32,11.32l-48-48a8,8,0,0,1,0-11.32l48-48a8,8,0,0,1,11.32,11.32L51.31,160H168a48,48,0,0,0,0-96H80a8,8,0,0,1,0-16h88A64.07,64.07,0,0,1,232,112Z",
4079
+ arrowsHorizontal: "M136,40V216a8,8,0,0,1-16,0V40a8,8,0,0,1,16,0ZM96,120H35.31l18.35-18.34A8,8,0,0,0,42.34,90.34l-32,32a8,8,0,0,0,0,11.32l32,32a8,8,0,0,0,11.32-11.32L35.31,136H96a8,8,0,0,0,0-16Zm149.66,2.34-32-32a8,8,0,0,0-11.32,11.32L220.69,120H160a8,8,0,0,0,0,16h60.69l-18.35,18.34a8,8,0,0,0,11.32,11.32l32-32A8,8,0,0,0,245.66,122.34Z",
4080
+ caretDown: "M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z",
4081
+ caretLeft: "M165.66,202.34a8,8,0,0,1-11.32,11.32l-80-80a8,8,0,0,1,0-11.32l80-80a8,8,0,0,1,11.32,11.32L91.31,128Z",
4082
+ caretRight: "M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z",
4083
+ caretUp: "M213.66,165.66a8,8,0,0,1-11.32,0L128,91.31,53.66,165.66a8,8,0,0,1-11.32-11.32l80-80a8,8,0,0,1,11.32,0l80,80A8,8,0,0,1,213.66,165.66Z",
4084
+ check: "M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z",
4085
+ checkCircle: "M173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z",
4086
+ circle: "M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Z",
4087
+ copy: "M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z",
4088
+ cornersIn: "M152,96V48a8,8,0,0,1,16,0V88h40a8,8,0,0,1,0,16H160A8,8,0,0,1,152,96ZM96,152H48a8,8,0,0,0,0,16H88v40a8,8,0,0,0,16,0V160A8,8,0,0,0,96,152Zm112,0H160a8,8,0,0,0-8,8v48a8,8,0,0,0,16,0V168h40a8,8,0,0,0,0-16ZM96,40a8,8,0,0,0-8,8V88H48a8,8,0,0,0,0,16H96a8,8,0,0,0,8-8V48A8,8,0,0,0,96,40Z",
4089
+ cornersOut: "M216,48V88a8,8,0,0,1-16,0V56H168a8,8,0,0,1,0-16h40A8,8,0,0,1,216,48ZM88,200H56V168a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H88a8,8,0,0,0,0-16Zm120-40a8,8,0,0,0-8,8v32H168a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V168A8,8,0,0,0,208,160ZM88,40H48a8,8,0,0,0-8,8V88a8,8,0,0,0,16,0V56H88a8,8,0,0,0,0-16Z",
4090
+ folder: "M216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72Zm0,128H40V64H93.33L123.2,86.4A8,8,0,0,0,128,88h88Z",
4091
+ keyboard: "M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192Zm-16-64a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,128Zm0-32a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,96ZM72,160a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16h8A8,8,0,0,1,72,160Zm96,0a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,160Zm40,0a8,8,0,0,1-8,8h-8a8,8,0,0,1,0-16h8A8,8,0,0,1,208,160Z",
4092
+ magnifyingGlass: "M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z",
4093
+ magnifyingGlassMinus: "M152,112a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h64A8,8,0,0,1,152,112Zm77.66,117.66a8,8,0,0,1-11.32,0l-50.06-50.07a88.11,88.11,0,1,1,11.31-11.31l50.07,50.06A8,8,0,0,1,229.66,229.66ZM112,184a72,72,0,1,0-72-72A72.08,72.08,0,0,0,112,184Z",
4094
+ magnifyingGlassPlus: "M152,112a8,8,0,0,1-8,8H120v24a8,8,0,0,1-16,0V120H80a8,8,0,0,1,0-16h24V80a8,8,0,0,1,16,0v24h24A8,8,0,0,1,152,112Zm77.66,117.66a8,8,0,0,1-11.32,0l-50.06-50.07a88.11,88.11,0,1,1,11.31-11.31l50.07,50.06A8,8,0,0,1,229.66,229.66ZM112,184a72,72,0,1,0-72-72A72.08,72.08,0,0,0,112,184Z",
4095
+ microphone: "M128,176a48.05,48.05,0,0,0,48-48V64a48,48,0,0,0-96,0v64A48.05,48.05,0,0,0,128,176ZM96,64a32,32,0,0,1,64,0v64a32,32,0,0,1-64,0Zm40,143.6V240a8,8,0,0,1-16,0V207.6A80.11,80.11,0,0,1,48,128a8,8,0,0,1,16,0,64,64,0,0,0,128,0,8,8,0,0,1,16,0A80.11,80.11,0,0,1,136,207.6Z",
4096
+ paperPlaneRight: "M231.87,114l-168-95.89A16,16,0,0,0,40.92,37.34L71.55,128,40.92,218.67A16,16,0,0,0,56,240a16.15,16.15,0,0,0,7.93-2.1l167.92-96.05a16,16,0,0,0,.05-27.89ZM56,224a.56.56,0,0,0,0-.12L85.74,136H144a8,8,0,0,0,0-16H85.74L56.06,32.16A.46.46,0,0,0,56,32l168,95.83Z",
4097
+ pencilSimple: "M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z",
4098
+ repeat: "M24,128A72.08,72.08,0,0,1,96,56H204.69L194.34,45.66a8,8,0,0,1,11.32-11.32l24,24a8,8,0,0,1,0,11.32l-24,24a8,8,0,0,1-11.32-11.32L204.69,72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H51.31l10.35-10.34a8,8,0,0,0-11.32-11.32l-24,24a8,8,0,0,0,0,11.32l24,24a8,8,0,0,0,11.32-11.32L51.31,200H160a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Z",
4099
+ sidebar: "M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40ZM40,56H80V200H40ZM216,200H96V56H216V200Z",
4100
+ x: "M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"
4101
+ };
4102
+ ICON_NAMES = Object.keys(PATHS);
4103
+ });
4104
+
4105
+ // src/ui/selection-pill.ts
4106
+ function pillSpring(distance, base = SPRINGS.default) {
4107
+ return distance > 400 ? { duration: base.duration * 1.4, bounce: base.bounce } : base;
4108
+ }
4109
+ function pillBoxOf(row, container) {
4110
+ return {
4111
+ x: row.left - container.left - container.clientLeft + container.scrollLeft,
4112
+ y: row.top - container.top - container.clientTop + container.scrollTop,
4113
+ w: row.width,
4114
+ h: row.height
4115
+ };
4116
+ }
4117
+ function pillRadius(from, to, h, k) {
4118
+ const t = Math.abs(to.h - from.h) > 0.5 ? (h - from.h) / (to.h - from.h) : Number.isFinite(k) ? k : 1;
4119
+ return from.r + (to.r - from.r) * Math.min(1, Math.max(0, t));
4120
+ }
4121
+ function pillProgress(from, to, at) {
4122
+ const d = Math.hypot(to.x - from.x, to.y - from.y);
4123
+ if (d < 0.5)
4124
+ return 1;
4125
+ const p = ((at.x - from.x) * (to.x - from.x) + (at.y - from.y) * (to.y - from.y)) / (d * d);
4126
+ return Math.min(1, Math.max(0, p));
4127
+ }
4128
+ function planPill(state, next, flags = {}) {
4129
+ if (!next)
4130
+ return { kind: "hide" };
4131
+ if (!state.shown || !state.at || flags.first || flags.reduced || flags.snap)
4132
+ return { kind: "land", box: next };
4133
+ const t = state.target;
4134
+ if (t && t.x === next.x && t.y === next.y && t.w === next.w && t.h === next.h)
4135
+ return { kind: "stay" };
4136
+ const d = Math.hypot(next.x - state.at.x, next.y - state.at.y);
4137
+ return { kind: "travel", box: next, spring: pillSpring(d) };
4138
+ }
4139
+ function selectionPill(container, options) {
4140
+ const doc = container.ownerDocument;
4141
+ const win = doc.defaultView;
4142
+ injectUiStyles(doc);
4143
+ if (win.getComputedStyle(container).position === "static")
4144
+ container.style.position = "relative";
4145
+ const pill = doc.createElement("i");
4146
+ pill.className = "knobs-pick-pill";
4147
+ pill.setAttribute("aria-hidden", "true");
4148
+ pill.dataset.off = "";
4149
+ const copyOn = options.copy !== false;
4150
+ const copy = doc.createElement("div");
4151
+ copy.className = "knobs-pick-copy";
4152
+ copy.setAttribute("aria-hidden", "true");
4153
+ copy.inert = true;
4154
+ copy.dataset.off = "";
4155
+ const copyRows = doc.createElement("div");
4156
+ copyRows.className = "knobs-pick-copy-rows";
4157
+ copy.append(copyRows);
4158
+ container.prepend(pill);
4159
+ if (copyOn)
4160
+ container.append(copy);
4161
+ const { omega, zeta } = springMotion(SPRINGS.default);
4162
+ const springs = [0, 0, 0, 0].map(() => new Spring(0, omega, zeta, 0.01));
4163
+ const [sx, sy, sw, sh] = springs;
4164
+ let shown = false;
4165
+ let first = true;
4166
+ let target = null;
4167
+ let from = { x: 0, y: 0, w: 0, h: 0 };
4168
+ let fromR = 0;
4169
+ let toR = 0;
4170
+ let rowBoxes = [];
4171
+ const clones = new Map;
4172
+ let disposed = false;
4173
+ const radiusOf = (row) => options.radius?.(row) ?? (Number.parseFloat(win.getComputedStyle(row).borderTopLeftRadius) || 0);
4174
+ const rowsNow = () => options.rows ? [...options.rows()] : [...container.children].filter((n) => n !== pill && n !== copy);
4175
+ const measure = (row) => {
4176
+ const c = container.getBoundingClientRect();
4177
+ const r = row.getBoundingClientRect();
4178
+ return pillBoxOf(r, {
4179
+ left: c.left,
4180
+ top: c.top,
4181
+ scrollLeft: container.scrollLeft,
4182
+ scrollTop: container.scrollTop,
4183
+ clientLeft: container.clientLeft,
4184
+ clientTop: container.clientTop
4185
+ });
4186
+ };
4187
+ const at = () => ({ x: sx.x, y: sy.x, w: sw.x, h: sh.x });
4188
+ const syncCopy = (span) => {
4189
+ if (!copyOn)
4190
+ return;
4191
+ const keep = new Set;
4192
+ for (const { row, box } of rowBoxes) {
4193
+ if (!boxesMeet(box, span))
4194
+ continue;
4195
+ keep.add(row);
4196
+ let n = clones.get(row);
4197
+ if (!n) {
4198
+ n = row.cloneNode(true);
4199
+ n.removeAttribute("id");
4200
+ for (const d of n.querySelectorAll("[id]"))
4201
+ d.removeAttribute("id");
4202
+ clones.set(row, n);
4203
+ copyRows.append(n);
4204
+ }
4205
+ const s = n.style;
4206
+ s.position = "absolute";
4207
+ s.inset = "auto";
4208
+ s.left = `${box.x}px`;
4209
+ s.top = `${box.y}px`;
4210
+ s.width = `${box.w}px`;
4211
+ s.height = `${box.h}px`;
4212
+ s.margin = "0";
4213
+ s.transform = "none";
4214
+ s.clipPath = "";
4215
+ s.opacity = "";
4216
+ s.boxSizing = "border-box";
4217
+ s.color = options.color ?? "var(--_k-fg)";
4218
+ }
4219
+ for (const [row, n] of clones)
4220
+ if (!keep.has(row)) {
4221
+ n.remove();
4222
+ clones.delete(row);
4223
+ }
4224
+ };
4225
+ const paint = () => {
4226
+ const b = at();
4227
+ const x0 = grid(b.x);
4228
+ const y0 = grid(b.y);
4229
+ const w = grid(b.x + b.w) - x0;
4230
+ const h = grid(b.y + b.h) - y0;
4231
+ const r = target ? pillRadius({ h: from.h, r: fromR }, { h: target.h, r: toR }, b.h, pillProgress(from, target, b)) : toR;
4232
+ const radius = `${Math.max(0, Math.min(r, h / 2, w / 2)).toFixed(2)}px`;
4233
+ for (const n of copyOn ? [pill, copy] : [pill]) {
4234
+ n.style.transform = `translate3d(${x0}px,${y0}px,0)`;
4235
+ n.style.width = `${w}px`;
4236
+ n.style.height = `${h}px`;
4237
+ n.style.borderRadius = radius;
4238
+ }
4239
+ if (copyOn)
4240
+ copyRows.style.transform = `translate3d(${-x0}px,${-y0}px,0)`;
4241
+ };
4242
+ const loop = frameLoop(win, (dt) => {
4243
+ for (const s of springs)
4244
+ s.step(dt);
4245
+ paint();
4246
+ return springs.some((s) => !s.resting);
4247
+ });
4248
+ const show = (on) => {
4249
+ shown = on;
4250
+ pill.toggleAttribute("data-off", !on);
4251
+ copy.toggleAttribute("data-off", !on);
4252
+ container.toggleAttribute("data-pick-empty", !on);
4253
+ };
4254
+ const update = (o) => {
4255
+ if (disposed)
4256
+ return;
4257
+ const row = options.selected();
4258
+ const next = row?.isConnected ? measure(row) : null;
4259
+ const plan = planPill({ shown, at: shown ? at() : null, target }, next, {
4260
+ first,
4261
+ reduced: reducedMotion(win),
4262
+ ...o?.snap ? { snap: true } : {}
4263
+ });
4264
+ first = false;
4265
+ for (const n of clones.values())
4266
+ n.remove();
4267
+ clones.clear();
4268
+ rowBoxes = copyOn ? rowsNow().filter((r) => r.isConnected).map((r) => ({ row: r, box: measure(r) })) : [];
4269
+ if (plan.kind === "hide") {
4270
+ if (shown)
4271
+ show(false);
4272
+ return;
4273
+ }
4274
+ if (plan.kind === "stay") {
4275
+ syncCopy(boxUnion(at(), target));
4276
+ return;
4277
+ }
4278
+ const r = radiusOf(row);
4279
+ if (plan.kind === "land") {
4280
+ sx.snap(plan.box.x);
4281
+ sy.snap(plan.box.y);
4282
+ sw.snap(plan.box.w);
4283
+ sh.snap(plan.box.h);
4284
+ from = plan.box;
4285
+ fromR = r;
4286
+ toR = r;
4287
+ target = plan.box;
4288
+ syncCopy(plan.box);
4289
+ paint();
4290
+ loop.stop();
4291
+ if (!shown)
4292
+ show(true);
4293
+ return;
4294
+ }
4295
+ const m = springMotion(plan.spring);
4296
+ for (const s of springs) {
4297
+ s.omega = m.omega;
4298
+ s.zeta = m.zeta;
4299
+ }
4300
+ const now = at();
4301
+ fromR = target ? pillRadius({ h: from.h, r: fromR }, { h: target.h, r: toR }, now.h, pillProgress(from, target, now)) : r;
4302
+ from = now;
4303
+ toR = r;
4304
+ target = plan.box;
4305
+ sx.set(plan.box.x);
4306
+ sy.set(plan.box.y);
4307
+ sw.set(plan.box.w);
4308
+ sh.set(plan.box.h);
4309
+ syncCopy(boxUnion(now, plan.box));
4310
+ loop.wake();
4311
+ };
4312
+ const ro = "ResizeObserver" in win ? new win.ResizeObserver(() => update({ snap: true })) : null;
4313
+ ro?.observe(container);
4314
+ update();
4315
+ return {
4316
+ element: pill,
4317
+ update,
4318
+ dispose() {
4319
+ disposed = true;
4320
+ loop.stop();
4321
+ ro?.disconnect();
4322
+ pill.remove();
4323
+ copy.remove();
4324
+ container.removeAttribute("data-pick-empty");
4325
+ }
4326
+ };
4327
+ }
4328
+ var boxesMeet = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h, boxUnion = (a, b) => {
4329
+ const x = Math.min(a.x, b.x);
4330
+ const y = Math.min(a.y, b.y);
4331
+ return {
4332
+ x,
4333
+ y,
4334
+ w: Math.max(a.x + a.w, b.x + b.w) - x,
4335
+ h: Math.max(a.y + a.h, b.y + b.h) - y
4336
+ };
4337
+ }, grid = (v) => Math.round(v * 64) / 64;
4338
+ var init_selection_pill = __esm(() => {
4339
+ init_motion();
4340
+ init_look();
4341
+ });
4342
+
3864
4343
  // src/panel/hints.ts
3865
4344
  function extractWords(text) {
3866
4345
  return text.toLowerCase().match(/[a-z]+/g) ?? [];
@@ -4689,34 +5168,27 @@ function tree(container, options) {
4689
5168
  const empty = doc.createElement("div");
4690
5169
  empty.className = "knobs-tree-empty";
4691
5170
  empty.hidden = true;
4692
- const cursor = doc.createElement("div");
4693
- cursor.className = "knobs-tree-cursor";
4694
- root.append(space, cursor, line, empty);
4695
- const cursorY = new Spring(0, MOVE.omega, MOVE.zeta, 0.1);
4696
- let cursorOn = false;
4697
- const drawCursor = () => {
4698
- cursor.style.transform = `translate3d(0,${Math.round(cursorY.x * 2) / 2}px,0)`;
5171
+ root.append(space, line, empty);
5172
+ let pill = null;
5173
+ let picked = null;
5174
+ let travelUntil = 0;
5175
+ const pickedRow = () => {
5176
+ const id = sel.ids.length === 1 ? sel.ids[0] : null;
5177
+ const v = id === null ? undefined : views.get(id);
5178
+ return v && !v.leaving && rowIndex.has(v.id) ? v : null;
4699
5179
  };
4700
5180
  const placeCursor = (animate) => {
4701
- if (!options.glide)
5181
+ if (!pill)
4702
5182
  return;
4703
5183
  const id = sel.ids.length === 1 ? sel.ids[0] : null;
4704
- const i = id === null ? undefined : rowIndex.get(id);
4705
- root.toggleAttribute("data-glide", i !== undefined);
4706
- if (i === undefined) {
4707
- cursor.removeAttribute("data-on");
4708
- cursorOn = false;
4709
- return;
4710
- }
4711
- if (cursorOn && animate && !reducedMotion(win))
4712
- cursorY.set(i * h);
4713
- else
4714
- cursorY.snap(i * h);
4715
- cursorOn = true;
4716
- cursor.setAttribute("data-on", "");
4717
- drawCursor();
4718
- if (!cursorY.resting)
4719
- loop.wake();
5184
+ root.toggleAttribute("data-glide", id !== null && rowIndex.has(id));
5185
+ const fresh = id !== picked;
5186
+ picked = id;
5187
+ if (animate && fresh) {
5188
+ travelUntil = performance.now() + 450;
5189
+ pill.update();
5190
+ } else if (performance.now() >= travelUntil)
5191
+ pill.update({ snap: true });
4720
5192
  };
4721
5193
  container.append(root);
4722
5194
  const scrollbar = overlayScrollbar(root, space);
@@ -4745,6 +5217,12 @@ function tree(container, options) {
4745
5217
  };
4746
5218
  const views = new Map;
4747
5219
  const pool = [];
5220
+ if (options.glide)
5221
+ pill = selectionPill(root, {
5222
+ rows: () => [...views.values()].filter((v) => !v.leaving).map((v) => v.el),
5223
+ selected: () => pickedRow()?.el ?? null,
5224
+ radius: (row) => Number.parseFloat(win.getComputedStyle(row, "::before").borderTopLeftRadius) || 0
5225
+ });
4748
5226
  const makeView = (id) => {
4749
5227
  const v = pool.pop() ?? createView();
4750
5228
  v.id = id;
@@ -4925,12 +5403,6 @@ function tree(container, options) {
4925
5403
  function frame(dt) {
4926
5404
  let moving = false;
4927
5405
  let bottom = rows.length * h;
4928
- if (options.glide && !cursorY.resting) {
4929
- cursorY.step(dt);
4930
- drawCursor();
4931
- if (!cursorY.resting)
4932
- moving = true;
4933
- }
4934
5406
  for (const v of views.values()) {
4935
5407
  v.y.step(dt);
4936
5408
  if (!v.y.resting)
@@ -4950,6 +5422,8 @@ function tree(container, options) {
4950
5422
  }
4951
5423
  setSpace(moving ? bottom : rows.length * h);
4952
5424
  trim();
5425
+ if (pill && moving && performance.now() >= travelUntil)
5426
+ pill.update({ snap: true });
4953
5427
  return moving;
4954
5428
  }
4955
5429
  const trim = () => {
@@ -5578,6 +6052,7 @@ function tree(container, options) {
5578
6052
  autoScroll.stop();
5579
6053
  ro.disconnect();
5580
6054
  scrollbar.dispose();
6055
+ pill?.dispose();
5581
6056
  dragging?.ghost.remove();
5582
6057
  root.remove();
5583
6058
  }
@@ -5596,6 +6071,7 @@ var init_tree = __esm(() => {
5596
6071
  init_motion();
5597
6072
  init_page_hints();
5598
6073
  init_look();
6074
+ init_selection_pill();
5599
6075
  init_tree_model();
5600
6076
  MOVE = springMotion(SPRINGS.default);
5601
6077
  });
@@ -5732,7 +6208,7 @@ function codeDiff(container, options) {
5732
6208
  };
5733
6209
  const nav = make("span", "knobs-diff-nav");
5734
6210
  const prev = button("Previous file", PREV);
5735
- const next = button("Next file", ICONS.caret);
6211
+ const next = button("Next file", NEXT);
5736
6212
  const place = make("span", "");
5737
6213
  nav.append(prev, place, next);
5738
6214
  const wrapButton = button("Wrap lines", WRAP);
@@ -5753,9 +6229,8 @@ function codeDiff(container, options) {
5753
6229
  const seg = make("div", "knobs-diff-seg");
5754
6230
  seg.setAttribute("role", "radiogroup");
5755
6231
  seg.setAttribute("aria-label", "Layout");
5756
- const pill = make("i", "knobs-diff-pill");
5757
6232
  const choice = (value, label) => {
5758
- const b = make("button", "");
6233
+ const b = make("button", "knobs-diff-choice");
5759
6234
  b.type = "button";
5760
6235
  b.setAttribute("role", "radio");
5761
6236
  b.dataset.value = value;
@@ -5764,7 +6239,7 @@ function codeDiff(container, options) {
5764
6239
  };
5765
6240
  const splitButton = choice("split", "Split");
5766
6241
  const unifiedButton = choice("unified", "Unified");
5767
- seg.append(pill, splitButton, unifiedButton);
6242
+ seg.append(splitButton, unifiedButton);
5768
6243
  if (options.lead)
5769
6244
  head.append(options.lead);
5770
6245
  head.append(path, stat, make("span", "knobs-diff-fill"), nav, wrapButton, seg, viewedButton);
@@ -5775,6 +6250,11 @@ function codeDiff(container, options) {
5775
6250
  body.append(note, probe);
5776
6251
  root.append(head, body);
5777
6252
  container.append(root);
6253
+ const segPill = selectionPill(seg, {
6254
+ rows: () => [splitButton, unifiedButton],
6255
+ selected: () => layout === "split" ? splitButton : unifiedButton
6256
+ });
6257
+ let pilled = layout;
5778
6258
  const fileNow = () => files.find((f) => f.name === current) ?? null;
5779
6259
  const paintHead = () => {
5780
6260
  const file = fileNow();
@@ -5807,6 +6287,10 @@ function codeDiff(container, options) {
5807
6287
  next.disabled = at >= files.length - 1;
5808
6288
  wrapButton.setAttribute("aria-pressed", String(wrap));
5809
6289
  seg.dataset.value = layout;
6290
+ if (pilled !== layout) {
6291
+ pilled = layout;
6292
+ segPill.update();
6293
+ }
5810
6294
  splitButton.setAttribute("aria-checked", String(layout === "split"));
5811
6295
  unifiedButton.setAttribute("aria-checked", String(layout === "unified"));
5812
6296
  splitButton.tabIndex = layout === "split" ? 0 : -1;
@@ -6641,6 +7125,7 @@ function codeDiff(container, options) {
6641
7125
  dispose() {
6642
7126
  disposed = true;
6643
7127
  loop.stop();
7128
+ segPill.dispose();
6644
7129
  sizes.disconnect();
6645
7130
  attaching.disconnect();
6646
7131
  resized.disconnect();
@@ -6996,7 +7481,7 @@ function diffReview(container, options) {
6996
7481
  }
6997
7482
  };
6998
7483
  }
6999
- var lib, load = () => lib ??= import("@pierre/diffs"), SYNTAX, IN, OUT, FADE4, HEIGHT, DRAWER, BLUR = 3, WRAP, PREV, UNVIEWED, VIEWED, SEARCH, CLEAR2, SIDEBAR, SEPARATOR = 32;
7484
+ var lib, load = () => lib ??= import("@pierre/diffs"), SYNTAX, IN, OUT, FADE4, HEIGHT, DRAWER, BLUR = 3, WRAP, PREV, NEXT, UNVIEWED, VIEWED, SEARCH, CLEAR2, SIDEBAR, SEPARATOR = 32;
7000
7485
  var init_diff = __esm(() => {
7001
7486
  init_glide();
7002
7487
  init_motion();
@@ -7004,6 +7489,8 @@ var init_diff = __esm(() => {
7004
7489
  init_diff_model();
7005
7490
  init_look();
7006
7491
  init_rgba();
7492
+ init_icons();
7493
+ init_selection_pill();
7007
7494
  init_tree();
7008
7495
  SYNTAX = { dark: "pierre-dark", light: "pierre-light" };
7009
7496
  IN = springMotion(SPRINGS.default);
@@ -7011,13 +7498,14 @@ var init_diff = __esm(() => {
7011
7498
  FADE4 = springMotion(FADE);
7012
7499
  HEIGHT = springMotion(SPRINGS.calm);
7013
7500
  DRAWER = springMotion(SPRINGS.calm);
7014
- WRAP = svg('<path d="M2.5 4h11"/><path d="M2.5 8h9a2 2 0 0 1 0 4H8.5"/><path d="M10 10.5 8.5 12l1.5 1.5"/><path d="M2.5 12h3"/>');
7015
- PREV = svg('<path d="M10 3.5 5.5 8l4.5 4.5"/>');
7016
- UNVIEWED = svg('<circle cx="8" cy="8" r="5.5"/>');
7017
- VIEWED = svg('<circle cx="8" cy="8" r="5.5"/><path d="m5.6 8.1 1.7 1.7 3.2-3.5"/>');
7018
- SEARCH = svg('<circle cx="7" cy="7" r="4.5"/><path d="m10.5 10.5 3 3"/>');
7019
- CLEAR2 = svg('<path d="m5 5 6 6M11 5l-6 6"/>');
7020
- SIDEBAR = svg('<rect x="2" y="3" width="12" height="10" rx="2"/><path d="M6.5 3v10"/>');
7501
+ WRAP = icon("arrowUDownLeft");
7502
+ PREV = icon("caretLeft");
7503
+ NEXT = icon("caretRight");
7504
+ UNVIEWED = icon("circle");
7505
+ VIEWED = icon("checkCircle");
7506
+ SEARCH = icon("magnifyingGlass", { small: true });
7507
+ CLEAR2 = icon("x", { small: true });
7508
+ SIDEBAR = icon("sidebar");
7021
7509
  });
7022
7510
 
7023
7511
  // src/ui/layer.ts
@@ -8000,10 +8488,25 @@ function advance(t, dt, o) {
8000
8488
  }
8001
8489
  return { t: hi, ended: true, wrapped: false };
8002
8490
  }
8003
- var SECOND_STEPS, FRAME_STEPS, toFrame = (t, fps) => Math.round(t * fps), frameTime = (t, fps) => Math.round(t * fps) / fps, round = (t) => Math.round(t * 1e6) / 1e6;
8491
+ function moveHead(head, t, motion, catching, minGap = 0) {
8492
+ if (motion === "jump" && Math.abs(t - head.x) > minGap)
8493
+ return { x: head.x, target: t, catching: true };
8494
+ if (catching)
8495
+ return { x: head.x + (t - head.target), target: t, catching: true };
8496
+ return { x: t, target: t, catching: false };
8497
+ }
8498
+ function currentMarker(markers, t, fps) {
8499
+ let best = null;
8500
+ for (const m of markers)
8501
+ if (m.time <= t + 0.5 / fps && (!best || m.time >= best.time))
8502
+ best = m;
8503
+ return best;
8504
+ }
8505
+ var SECOND_STEPS, FRAME_STEPS, toFrame = (t, fps) => Math.round(t * fps), frameTime = (t, fps) => Math.round(t * fps) / fps, round = (t) => Math.round(t * 1e6) / 1e6, CATCH_UP;
8004
8506
  var init_timeline_model = __esm(() => {
8005
8507
  SECOND_STEPS = [0.1, 0.2, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600];
8006
8508
  FRAME_STEPS = [1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 9000, 18000];
8509
+ CATCH_UP = { duration: 0.15, bounce: 0 };
8007
8510
  });
8008
8511
 
8009
8512
  // src/ui/timeline.ts
@@ -8016,6 +8519,7 @@ function timeline(container, options) {
8016
8519
  let speed = options.speed ?? 1;
8017
8520
  let repeat = options.repeat ?? true;
8018
8521
  const arrows = options.arrows ?? "frames";
8522
+ const lane = options.variant === "lane";
8019
8523
  let time = Math.min(duration, Math.max(0, options.time ?? 0));
8020
8524
  let playing = false;
8021
8525
  let markers = [...options.markers ?? []];
@@ -8049,27 +8553,27 @@ function timeline(container, options) {
8049
8553
  root.setAttribute("aria-label", options.label ?? "Timeline");
8050
8554
  root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight , . Home End [ ] I O M");
8051
8555
  applyLook2(root, options);
8556
+ if (lane)
8557
+ root.dataset.variant = "lane";
8052
8558
  const transport = el("div", "knobs-tl-transport");
8053
8559
  const playBtn = el("button", "knobs-tl-play");
8054
8560
  playBtn.type = "button";
8055
8561
  playBtn.tabIndex = -1;
8056
- const readout = el("div", "knobs-tl-time");
8057
- const now = el("span", "knobs-tl-now");
8058
- const sub = el("span", "knobs-tl-sub");
8059
- readout.append(now, sub);
8060
- transport.append(playBtn, readout);
8562
+ transport.append(playBtn);
8061
8563
  const track = el("div", "knobs-tl-track");
8062
8564
  const canvas = el("canvas", "knobs-tl-ruler");
8063
8565
  canvas.setAttribute("aria-hidden", "true");
8064
- const lane = el("div", "knobs-tl-lane");
8566
+ const markerLane = el("div", "knobs-tl-lane");
8065
8567
  const hover = el("div", "knobs-tl-hover");
8568
+ const tag = el("span", "knobs-tl-tag");
8569
+ tag.setAttribute("aria-hidden", "true");
8066
8570
  const head = el("div", "knobs-tl-head");
8067
8571
  head.id = `${id}-head`;
8068
8572
  head.setAttribute("role", "slider");
8069
8573
  head.setAttribute("aria-label", "Playhead");
8070
8574
  head.setAttribute("aria-valuemin", "0");
8071
8575
  head.setAttribute("aria-valuemax", String(duration));
8072
- track.append(canvas, lane, hover, head);
8576
+ track.append(canvas, markerLane, hover, head, tag);
8073
8577
  root.append(transport, track);
8074
8578
  container.append(root);
8075
8579
  let width = 1;
@@ -8116,13 +8620,14 @@ function timeline(container, options) {
8116
8620
  let drawn = "";
8117
8621
  const drawRuler = () => {
8118
8622
  const v = view();
8119
- const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${units}:${fps}`;
8623
+ const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${height}:${units}:${fps}`;
8120
8624
  if (key === drawn)
8121
8625
  return;
8122
8626
  drawn = key;
8123
8627
  const dpr = win.devicePixelRatio || 1;
8124
8628
  const w = Math.max(1, Math.round(width * dpr));
8125
- const h = Math.round(RULER * dpr);
8629
+ const rh = lane ? height : RULER;
8630
+ const h = Math.round(rh * dpr);
8126
8631
  if (canvas.width !== w || canvas.height !== h) {
8127
8632
  canvas.width = w;
8128
8633
  canvas.height = h;
@@ -8131,8 +8636,19 @@ function timeline(container, options) {
8131
8636
  if (!ctx)
8132
8637
  return;
8133
8638
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
8134
- ctx.clearRect(0, 0, width, RULER);
8639
+ ctx.clearRect(0, 0, width, rh);
8135
8640
  const ticks = rulerTicks(v, width, { fps, units });
8641
+ if (lane) {
8642
+ ctx.fillStyle = colors.tick;
8643
+ for (const t of ticks.minor)
8644
+ if (t >= -0.000000001 && t <= duration + 0.000000001)
8645
+ ctx.fillRect(xOf(t) - 0.5, rh - 4, 1, 4);
8646
+ ctx.fillStyle = colors.major;
8647
+ for (const m of ticks.major)
8648
+ if (m.t >= -0.000000001 && m.t <= duration + 0.000000001)
8649
+ ctx.fillRect(xOf(m.t) - 0.5, rh - 7, 1, 7);
8650
+ return;
8651
+ }
8136
8652
  const px = (t) => Math.round(xOf(t)) + 0.5;
8137
8653
  ctx.fillStyle = colors.line;
8138
8654
  ctx.fillRect(0, RULER - 1, width, 1);
@@ -8172,18 +8688,21 @@ function timeline(container, options) {
8172
8688
  n.dataset.id = m.id;
8173
8689
  const pin = el("span", "knobs-tl-pin");
8174
8690
  const label = el("span", "knobs-tl-mlabel");
8691
+ label.append(el("span", "knobs-tl-mtext knobs-scroll-fade-x"));
8175
8692
  n.append(pin, label);
8176
- lane.append(n);
8693
+ markerLane.append(n);
8177
8694
  markerEls.set(m.id, n);
8178
8695
  if (built)
8179
8696
  enter(n);
8180
8697
  }
8181
8698
  const label = n.querySelector(".knobs-tl-mlabel");
8182
- if (label.textContent !== (m.label ?? ""))
8183
- label.textContent = m.label ?? "";
8699
+ const text = label.firstElementChild;
8700
+ if (text.textContent !== (m.label ?? ""))
8701
+ text.textContent = m.label ?? "";
8184
8702
  label.hidden = !m.label;
8185
8703
  n.style.setProperty("--knobs-tl-color", m.color ?? "");
8186
8704
  n.toggleAttribute("data-selected", selected?.kind === "marker" && selected.id === m.id);
8705
+ n.toggleAttribute("data-current", m.id === currentId);
8187
8706
  n.title = `${m.label ? `${m.label} · ` : ""}${clock(m.time)}`;
8188
8707
  }
8189
8708
  placeMarkers();
@@ -8211,7 +8730,7 @@ function timeline(container, options) {
8211
8730
  b.dataset.edge = "end";
8212
8731
  const label = el("span", "knobs-tl-rlabel");
8213
8732
  n.append(label, a, b);
8214
- track.insertBefore(n, lane);
8733
+ track.insertBefore(n, markerLane);
8215
8734
  rangeEls.set(r.id, n);
8216
8735
  if (built)
8217
8736
  enter(n);
@@ -8260,16 +8779,17 @@ function timeline(container, options) {
8260
8779
  };
8261
8780
  const clock = (t) => units === "frames" ? `f${toFrame(t, fps)}` : `${timeLabel(t, "seconds", fps, 0.01)}`;
8262
8781
  let shownHead = time;
8782
+ let currentId = null;
8263
8783
  const renderHead = () => {
8264
8784
  head.style.transform = `translateX(${xOf(shownHead).toFixed(2)}px)`;
8265
- const main = units === "frames" ? `f${toFrame(time, fps)}` : `${time.toFixed(2)}s`;
8266
- const minor = units === "frames" ? `${time.toFixed(2)}s` : `f${toFrame(time, fps)}`;
8267
- if (now.textContent !== main)
8268
- now.textContent = main;
8269
- if (sub.textContent !== minor)
8270
- sub.textContent = minor;
8271
8785
  head.setAttribute("aria-valuenow", time.toFixed(3));
8272
8786
  head.setAttribute("aria-valuetext", `${time.toFixed(2)} seconds, frame ${toFrame(time, fps)}`);
8787
+ const cur = currentMarker(markers, time, fps)?.id ?? null;
8788
+ if (cur !== currentId) {
8789
+ markerEls.get(currentId ?? "")?.removeAttribute("data-current");
8790
+ currentId = cur;
8791
+ markerEls.get(cur ?? "")?.setAttribute("data-current", "");
8792
+ }
8273
8793
  };
8274
8794
  let viewSent = "";
8275
8795
  const render = () => {
@@ -8295,18 +8815,27 @@ function timeline(container, options) {
8295
8815
  };
8296
8816
  const caret = new Spring(time, GLIDE.omega, GLIDE.zeta, 0.00001);
8297
8817
  let gliding = false;
8298
- const setTime = (t, source, shown = t, glide = false) => {
8299
- const next = Math.min(duration, Math.max(0, t));
8300
- if (glide && !reducedMotion(win)) {
8818
+ const headTo = (shown, motion) => {
8819
+ const from = gliding ? { x: caret.x, target: caret.target } : { x: shownHead, target: shownHead };
8820
+ const jump = motion === "jump" && !reducedMotion(win);
8821
+ const next = moveHead(from, shown, jump ? "jump" : "follow", gliding, 2 / pxPerSec());
8822
+ if (next.catching) {
8301
8823
  if (!gliding)
8302
- caret.snap(shownHead);
8303
- caret.set(next);
8824
+ caret.snap(from.x);
8825
+ caret.x = next.x;
8826
+ caret.target = next.target;
8304
8827
  gliding = true;
8828
+ shownHead = caret.x;
8305
8829
  loop.wake();
8306
8830
  } else {
8307
8831
  gliding = false;
8308
- shownHead = shown;
8832
+ caret.snap(next.x);
8833
+ shownHead = next.x;
8309
8834
  }
8835
+ };
8836
+ const setTime = (t, source, shown = t, glide = false) => {
8837
+ const next = Math.min(duration, Math.max(0, t));
8838
+ headTo(Math.min(duration, Math.max(0, shown)), glide ? "jump" : "follow");
8310
8839
  const moved = next !== time;
8311
8840
  time = next;
8312
8841
  renderHead();
@@ -8347,8 +8876,7 @@ function timeline(container, options) {
8347
8876
  options.onCross?.(m);
8348
8877
  }
8349
8878
  time = step.t;
8350
- shownHead = time;
8351
- gliding = false;
8879
+ headTo(time, step.wrapped ? "jump" : "follow");
8352
8880
  emitSeek(time, "play");
8353
8881
  keepInView();
8354
8882
  if (step.ended) {
@@ -8361,7 +8889,7 @@ function timeline(container, options) {
8361
8889
  shownHead = caret.step(dt);
8362
8890
  if (caret.resting) {
8363
8891
  gliding = false;
8364
- shownHead = time;
8892
+ shownHead = caret.target;
8365
8893
  }
8366
8894
  }
8367
8895
  vStart.step(dt);
@@ -8374,9 +8902,9 @@ function timeline(container, options) {
8374
8902
  return;
8375
8903
  const r = loopRange();
8376
8904
  if (r && (time < r.start || time >= r.end - 0.5 / fps))
8377
- setTime(r.start, "code");
8905
+ setTime(r.start, "code", r.start, true);
8378
8906
  else if (!r && time >= duration - 0.5 / fps)
8379
- setTime(0, "code");
8907
+ setTime(0, "code", 0, true);
8380
8908
  playing = true;
8381
8909
  syncPlay();
8382
8910
  emitPlay(true);
@@ -8505,24 +9033,35 @@ function timeline(container, options) {
8505
9033
  };
8506
9034
  const localX = (e) => e.clientX - track.getBoundingClientRect().left;
8507
9035
  let drag = null;
8508
- const scrubTo = (x) => {
9036
+ const scrubTo = (x, press = false) => {
8509
9037
  const shownX = softClamp(x, xOf(0), xOf(duration), 10);
8510
9038
  const s = scrubTime(tOf(shownX), {
8511
9039
  duration,
8512
9040
  fps,
8513
9041
  pxPerSec: pxPerSec(),
8514
- stops: snapMarkers ? markers.map((m) => m.time) : [],
9042
+ stops: snapMarkers && !lane ? markers.map((m) => m.time) : [],
8515
9043
  frames: snapFrames
8516
9044
  });
8517
- setTime(s.t, "scrub", s.shown);
9045
+ setTime(s.t, "scrub", s.shown, press);
8518
9046
  root.toggleAttribute("data-stuck", s.stop !== null);
8519
9047
  };
9048
+ const showTag = (x) => {
9049
+ const at = Math.min(duration, Math.max(0, tOf(x)));
9050
+ const text = `${at.toFixed(2)} s`;
9051
+ if (tag.textContent !== text)
9052
+ tag.textContent = text;
9053
+ const cx = Math.min(width, Math.max(0, x));
9054
+ const flip = cx > width - tag.offsetWidth - 12;
9055
+ tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
9056
+ tag.dataset.on = "";
9057
+ };
9058
+ const hideTag = () => delete tag.dataset.on;
8520
9059
  track.addEventListener("pointerdown", (e) => {
8521
9060
  if (e.button !== 0 || editing)
8522
9061
  return;
8523
9062
  root.focus({ preventScroll: true });
8524
9063
  const t = e.target;
8525
- const markerEl = t.closest(".knobs-tl-marker");
9064
+ const markerEl = lane ? null : t.closest(".knobs-tl-marker");
8526
9065
  const edgeEl = t.closest(".knobs-tl-edge");
8527
9066
  const rangeEl = t.closest(".knobs-tl-range");
8528
9067
  track.setPointerCapture(e.pointerId);
@@ -8567,13 +9106,16 @@ function timeline(container, options) {
8567
9106
  root.dataset.scrubbing = "";
8568
9107
  for (const l of listeners)
8569
9108
  l.onScrub?.(true);
8570
- scrubTo(localX(e));
9109
+ scrubTo(localX(e), true);
9110
+ showTag(localX(e));
8571
9111
  }
8572
9112
  e.preventDefault();
8573
9113
  });
8574
9114
  track.addEventListener("pointermove", (e) => {
8575
9115
  const x = localX(e);
8576
9116
  hover.style.transform = `translateX(${x.toFixed(1)}px)`;
9117
+ if (!drag || drag.kind === "scrub")
9118
+ showTag(x);
8577
9119
  if (!drag)
8578
9120
  return;
8579
9121
  if (drag.kind === "scrub")
@@ -8619,8 +9161,10 @@ function timeline(container, options) {
8619
9161
  delete root.dataset.dragging;
8620
9162
  root.removeAttribute("data-stuck");
8621
9163
  if (d.kind === "scrub") {
8622
- shownHead = time;
9164
+ headTo(time, "follow");
8623
9165
  renderHead();
9166
+ if (!track.matches(":hover"))
9167
+ hideTag();
8624
9168
  for (const l of listeners)
8625
9169
  l.onScrub?.(false);
8626
9170
  if (d.wasPlaying)
@@ -8633,7 +9177,7 @@ function timeline(container, options) {
8633
9177
  } else {
8634
9178
  const m = markers.find((x) => x.id === d.id);
8635
9179
  if (m)
8636
- setTime(m.time, "marker");
9180
+ setTime(m.time, "marker", m.time, true);
8637
9181
  }
8638
9182
  } else if (d.kind === "edge" || d.moved)
8639
9183
  emitRanges();
@@ -8642,10 +9186,14 @@ function timeline(container, options) {
8642
9186
  track.addEventListener("pointercancel", endDrag);
8643
9187
  track.addEventListener("lostpointercapture", endDrag);
8644
9188
  track.addEventListener("pointerleave", () => {
8645
- if (!drag)
8646
- hover.style.transform = "translateX(-100px)";
9189
+ if (drag)
9190
+ return;
9191
+ hover.style.transform = "translateX(-100px)";
9192
+ hideTag();
8647
9193
  });
8648
9194
  track.addEventListener("dblclick", (e) => {
9195
+ if (lane)
9196
+ return;
8649
9197
  const markerEl = e.target.closest(".knobs-tl-marker");
8650
9198
  if (markerEl)
8651
9199
  return rename(markerEl.dataset.id);
@@ -8656,6 +9204,8 @@ function timeline(container, options) {
8656
9204
  rename(m.id);
8657
9205
  });
8658
9206
  track.addEventListener("wheel", (e) => {
9207
+ if (lane)
9208
+ return;
8659
9209
  e.preventDefault();
8660
9210
  const scale = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? height : 1;
8661
9211
  if (e.ctrlKey || e.metaKey) {
@@ -8719,7 +9269,7 @@ function timeline(container, options) {
8719
9269
  if (!m)
8720
9270
  return;
8721
9271
  select({ kind: "marker", id: m.id });
8722
- return setTime(m.time, "marker");
9272
+ return setTime(m.time, "marker", m.time, true);
8723
9273
  }
8724
9274
  case "i":
8725
9275
  case "I":
@@ -8731,11 +9281,13 @@ function timeline(container, options) {
8731
9281
  return setEdge("end", time);
8732
9282
  case "m":
8733
9283
  case "M":
9284
+ if (lane)
9285
+ return;
8734
9286
  claim();
8735
9287
  addMarker(time);
8736
9288
  return;
8737
9289
  case "Enter":
8738
- if (selected?.kind === "marker") {
9290
+ if (selected?.kind === "marker" && !lane) {
8739
9291
  claim();
8740
9292
  rename(selected.id);
8741
9293
  }
@@ -8753,19 +9305,25 @@ function timeline(container, options) {
8753
9305
  return;
8754
9306
  case "=":
8755
9307
  case "+":
9308
+ if (lane)
9309
+ return;
8756
9310
  claim();
8757
9311
  return zoom(1.5);
8758
9312
  case "-":
9313
+ if (lane)
9314
+ return;
8759
9315
  claim();
8760
9316
  return zoom(1 / 1.5);
8761
9317
  case "0":
9318
+ if (lane)
9319
+ return;
8762
9320
  claim();
8763
9321
  return fit();
8764
9322
  }
8765
9323
  });
8766
9324
  const unregisterHints = options.hints === false ? undefined : hints.register(root, () => [
8767
9325
  { el: playBtn, label: playing ? "Pause" : "Play", activate: toggle },
8768
- ...markers.flatMap((m) => {
9326
+ ...(lane ? [] : markers).flatMap((m) => {
8769
9327
  const n = markerEls.get(m.id);
8770
9328
  if (!n || n.dataset.leaving !== undefined)
8771
9329
  return [];
@@ -8777,7 +9335,7 @@ function timeline(container, options) {
8777
9335
  ...label && !label.hidden ? { labelEl: label } : {},
8778
9336
  activate: () => {
8779
9337
  select({ kind: "marker", id: m.id });
8780
- setTime(m.time, "marker");
9338
+ setTime(m.time, "marker", m.time, true);
8781
9339
  root.focus({ preventScroll: true });
8782
9340
  }
8783
9341
  }
@@ -8789,7 +9347,7 @@ function timeline(container, options) {
8789
9347
  activate: () => root.focus({ preventScroll: true })
8790
9348
  }
8791
9349
  ], { listen: false, rank: 1 });
8792
- const menu = contextMenu(track, (e) => {
9350
+ const menu = lane ? null : contextMenu(track, (e) => {
8793
9351
  const t = e?.target ?? null;
8794
9352
  const markerEl = t?.closest(".knobs-tl-marker");
8795
9353
  const rangeEl = t?.closest(".knobs-tl-range");
@@ -8804,7 +9362,7 @@ function timeline(container, options) {
8804
9362
  run: () => {
8805
9363
  const m = markers.find((x) => x.id === mid);
8806
9364
  if (m)
8807
- setTime(m.time, "marker");
9365
+ setTime(m.time, "marker", m.time, true);
8808
9366
  }
8809
9367
  },
8810
9368
  { label: "Rename…", shortcut: "Enter", run: () => rename(mid) },
@@ -8968,7 +9526,7 @@ function timeline(container, options) {
8968
9526
  listeners.clear();
8969
9527
  loop.stop();
8970
9528
  ro.disconnect();
8971
- menu.dispose();
9529
+ menu?.dispose();
8972
9530
  root.remove();
8973
9531
  }
8974
9532
  };
@@ -8985,10 +9543,21 @@ var init_timeline = __esm(() => {
8985
9543
  init_timeline_model();
8986
9544
  INSTANT2 = springMotion(SPRINGS.instant);
8987
9545
  CALM = springMotion(SPRINGS.calm);
8988
- GLIDE = springMotion(SPRINGS.snappy);
9546
+ GLIDE = springMotion(CATCH_UP);
8989
9547
  });
8990
9548
 
8991
9549
  // src/ui/compare-model.ts
9550
+ function compareModes(modes, mode) {
9551
+ const list = [...new Set((modes ?? ["side", "wipe"]).filter((m) => ALL_MODES.includes(m)))];
9552
+ const offered = list.length ? list : ["side", "wipe"];
9553
+ return { modes: offered, mode: mode && offered.includes(mode) ? mode : offered[0] };
9554
+ }
9555
+ function modeForKey(key, modes) {
9556
+ const k = key.toLowerCase();
9557
+ if (/^[1-9]$/.test(k))
9558
+ return modes[Number(k) - 1] ?? null;
9559
+ return modes.find((m) => MODE_KEYS[m] === k) ?? null;
9560
+ }
8992
9561
  function mediaKind(src, kind) {
8993
9562
  if (kind)
8994
9563
  return kind;
@@ -9067,6 +9636,18 @@ function wipeGrab(x, rect, wipe, slop = 12) {
9067
9636
  const onLine = Math.abs(x - line) <= slop;
9068
9637
  return { onLine, offset: onLine ? x - line : 0 };
9069
9638
  }
9639
+ function seamLabels(x, left, right, wb, wa, gap = 8) {
9640
+ const room = (space, w) => Math.min(1, Math.max(0, (space - w - 2 * gap) / gap));
9641
+ return {
9642
+ before: { x: x - gap - wb, opacity: room(x - left, wb) },
9643
+ after: { x: x + gap, opacity: room(right - x, wa) }
9644
+ };
9645
+ }
9646
+ function gripTop(y, rect, grip, margin = 8) {
9647
+ const lo = rect.y + margin;
9648
+ const hi = rect.y + rect.h - margin - grip;
9649
+ return hi < lo ? rect.y + (rect.h - grip) / 2 : Math.min(hi, Math.max(lo, y - grip / 2));
9650
+ }
9070
9651
  function stripColumns(view, width, o) {
9071
9652
  const { fps } = o;
9072
9653
  const total = Math.max(1, Math.round(o.duration * fps));
@@ -9117,7 +9698,7 @@ function stripPeaks(diffs, o = {}) {
9117
9698
  });
9118
9699
  return out;
9119
9700
  }
9120
- var VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), lastFrame = (duration, fps) => Math.max(0, Math.ceil(duration * fps - 0.0001) - 1), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, splitHeight = (w, media) => media.w > 0 && media.h > 0 ? w / 2 * (media.h / media.w) : w / 2 * (9 / 16), ZOOM_FIT, ZOOM_MAX = 16, panBy = (r, dx, dy, base) => clampZoom({
9701
+ var ALL_MODES, MODE_KEYS, modeKey = (mode) => MODE_KEYS[mode].toUpperCase(), VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), lastFrame = (duration, fps) => Math.max(0, Math.ceil(duration * fps - 0.0001) - 1), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, splitHeight = (w, media) => media.w > 0 && media.h > 0 ? w / 2 * (media.h / media.w) : w / 2 * (9 / 16), ZOOM_FIT, ZOOM_MAX = 16, panBy = (r, dx, dy, base) => clampZoom({
9121
9702
  u: r.u - dx / Math.max(1, base.w) * r.w,
9122
9703
  v: r.v - dy / Math.max(1, base.h) * r.h,
9123
9704
  w: r.w
@@ -9136,6 +9717,8 @@ var VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), last
9136
9717
  h: a.h + (b.h - a.h) * k
9137
9718
  }), wipeX = (rect, wipe) => rect.x + Math.min(1, Math.max(0, wipe)) * rect.w, wipeAt = (x, rect, offset = 0) => rect.w <= 0 ? 0.5 : Math.min(1, Math.max(0, (x - offset - rect.x) / rect.w)), wipeKey = (wipe, dir, big = false) => Math.min(1, Math.max(0, Math.round((wipe + dir * (big ? 0.1 : 0.01)) * 1000) / 1000));
9138
9719
  var init_compare_model = __esm(() => {
9720
+ ALL_MODES = ["side", "wipe", "difference"];
9721
+ MODE_KEYS = { side: "s", wipe: "w", difference: "d" };
9139
9722
  VIDEO = /\.(mp4|m4v|mov|webm|ogv|mkv)$/i;
9140
9723
  SEEK_IDLE = { seeking: false, pending: null };
9141
9724
  ZOOM_FIT = { u: 0, v: 0, w: 1, h: 1 };
@@ -9935,8 +10518,8 @@ async function createCompareGpu(canvas, onLost) {
9935
10518
  r(0, f.before);
9936
10519
  r(4, f.after);
9937
10520
  data.set([f.width, f.height, f.dpr, 0], 8);
9938
- data.set([f.wipeX, f.soft, 0, f.phase ? 1 : 0], 12);
9939
- data.set([f.from, f.to, f.mix, f.flick ? 1 : 0], 16);
10521
+ data.set([f.wipeX, f.soft, 0, 0], 12);
10522
+ data.set([f.from, f.to, f.mix, 0], 16);
9940
10523
  data.set([f.threshold, f.gain, ready[0], ready[1]], 20);
9941
10524
  data.set([f.accent[0], f.accent[1], f.accent[2], 1], 24);
9942
10525
  const l = f.loupe;
@@ -9978,8 +10561,8 @@ var MODE_INDEX, WGSL = `
9978
10561
  struct U {
9979
10562
  ra: vec4f, rb: vec4f,
9980
10563
  view: vec4f, // w, h, dpr, _
9981
- wipe: vec4f, // x, soft, _, phase
9982
- modes: vec4f, // from, to, mix, flick
10564
+ wipe: vec4f, // x, soft, _, _
10565
+ modes: vec4f, // from, to, mix, _
9983
10566
  diff: vec4f, // threshold, gain, readyA, readyB
9984
10567
  accent: vec4f,
9985
10568
  loupe: vec4f, // x, y, r, zoom (0: none)
@@ -10030,21 +10613,14 @@ fn takeB(p: vec2f, r: vec4f, c0: vec4f, near: bool) -> vec4f {
10030
10613
 
10031
10614
  fn pm(c: vec4f) -> vec4f { return vec4f(c.rgb * c.a, c.a); }
10032
10615
 
10033
- fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
10034
- let flick = u.modes.w > 0.5;
10616
+ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
10035
10617
  switch mode {
10036
- case 0u: {
10037
- let bb = select(b, vec4f(a2.rgb, b.a), flick);
10038
- return vec4f(a.rgb * a.a + bb.rgb * bb.a * (1.0 - a.a), a.a + bb.a * (1.0 - a.a));
10039
- }
10040
10618
  case 1u: {
10041
- if (flick) { return pm(a); }
10042
10619
  let s = max(u.wipe.y, 0.5);
10043
10620
  let m = smoothstep(u.wipe.x - s, u.wipe.x + s, p.x);
10044
10621
  return pm(vec4f(mix(a.rgb, b.rgb, m), mix(a.a, b.a, m)));
10045
10622
  }
10046
10623
  case 2u: {
10047
- if (flick) { return pm(a); }
10048
10624
  let d = abs(a.rgb - b.rgb);
10049
10625
  let h = smoothstep(u.diff.x, u.diff.x + 0.06, max(d.r, max(d.g, d.b)) * u.diff.y) * min(a.a, b.a);
10050
10626
  let c = mix(vec3f(luma(b.rgb) * 0.35), u.accent.rgb, h);
@@ -10052,8 +10628,8 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
10052
10628
  }
10053
10629
  default: {}
10054
10630
  }
10055
- // Flicker: After, or Before while X is held or on the flicker's other beat.
10056
- return pm(select(b, a, flick || u.wipe.w > 0.5));
10631
+ // Side by side: each side in its own place, Before over After where they meet.
10632
+ return vec4f(a.rgb * a.a + b.rgb * b.a * (1.0 - a.a), a.a + b.a * (1.0 - a.a));
10057
10633
  }
10058
10634
 
10059
10635
  @fragment fn fs(@builtin(position) q: vec4f) -> @location(0) vec4f {
@@ -10071,16 +10647,14 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
10071
10647
  }
10072
10648
  let a = takeA(p, u.ra, u.ca, near);
10073
10649
  let b = takeB(p, u.rb, u.cb, near);
10074
- // Before in After's place (X held, side by side): After's rect, as if Before were zoomed there.
10075
- let a2 = takeA(p, u.rb, u.cb, near);
10076
- var o = mix(composite(u32(u.modes.x), p, a, b, a2), composite(u32(u.modes.y), p, a, b, a2), u.modes.z);
10650
+ var o = mix(composite(u32(u.modes.x), p, a, b), composite(u32(u.modes.y), p, a, b), u.modes.z);
10077
10651
  if (near) { o = o + vec4f(0.05, 0.05, 0.06, 1.0) * (1.0 - o.a); }
10078
10652
  o = mix(o, vec4f(0.92), ring * 0.9);
10079
10653
  return o;
10080
10654
  }
10081
10655
  `, TEXTURE_BINDING = 4, COPY_DST = 2, RENDER_ATTACHMENT = 16, shared = null, lostListeners;
10082
10656
  var init_compare_gpu = __esm(() => {
10083
- MODE_INDEX = { side: 0, wipe: 1, difference: 2, flicker: 3 };
10657
+ MODE_INDEX = { side: 0, wipe: 1, difference: 2 };
10084
10658
  lostListeners = new Set;
10085
10659
  });
10086
10660
 
@@ -10466,7 +11040,9 @@ function compare(container, options) {
10466
11040
  ...options.theme ? { theme: options.theme } : {},
10467
11041
  ...options.appearance ? { appearance: options.appearance } : {}
10468
11042
  };
10469
- let mode = options.mode ?? "side";
11043
+ const offered = compareModes(options.modes, options.mode);
11044
+ const modeList = offered.modes;
11045
+ let mode = offered.mode;
10470
11046
  let wipe = Math.min(1, Math.max(0, options.wipe ?? 0.5));
10471
11047
  let loupeOn = options.loupe ?? false;
10472
11048
  let threshold = options.threshold ?? 4 / 255;
@@ -10484,7 +11060,7 @@ function compare(container, options) {
10484
11060
  root.setAttribute("role", "group");
10485
11061
  root.setAttribute("aria-roledescription", "compare");
10486
11062
  root.setAttribute("aria-label", options.label ?? "Compare");
10487
- root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight , . 1 2 3 4 S W D X Z L 0 = -");
11063
+ root.setAttribute("aria-keyshortcuts", `Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight , . ${modeList.map((m, i) => `${i + 1} ${modeKey(m)}`).join(" ")} Z L 0 = -`);
10488
11064
  applyLook2(root, options);
10489
11065
  root.dataset.mode = mode;
10490
11066
  if (auto)
@@ -10493,35 +11069,37 @@ function compare(container, options) {
10493
11069
  const modes = el("div", "knobs-cmp-modes");
10494
11070
  modes.setAttribute("role", "radiogroup");
10495
11071
  modes.setAttribute("aria-label", "Mode");
10496
- const modeBtns = MODES2.map((m, i) => {
11072
+ modes.hidden = modeList.length < 2;
11073
+ const modeBtns = modeList.map((m, i) => {
10497
11074
  const b = el("button", "knobs-cmp-mode");
10498
11075
  b.type = "button";
10499
11076
  b.tabIndex = -1;
10500
- b.textContent = m.label;
10501
- b.title = `${m.label} · ${i + 1}${m.key ? ` or ${m.key}` : " (hold X to flick)"}`;
11077
+ b.textContent = MODE_LABEL[m].short;
11078
+ b.title = `${MODE_LABEL[m].long} · ${i + 1} or ${modeKey(m)}`;
10502
11079
  b.setAttribute("role", "radio");
10503
- b.setAttribute("aria-keyshortcuts", m.key ? `${i + 1} ${m.key}` : `${i + 1}`);
11080
+ b.setAttribute("aria-keyshortcuts", `${i + 1} ${modeKey(m)}`);
10504
11081
  b.addEventListener("click", () => {
10505
- setMode(m.mode);
11082
+ setMode(m);
10506
11083
  root.focus({ preventScroll: true });
10507
11084
  });
10508
11085
  modes.append(b);
10509
11086
  return b;
10510
11087
  });
10511
- const loupeBtn = el("button", "knobs-cmp-toggle");
10512
- loupeBtn.type = "button";
10513
- loupeBtn.tabIndex = -1;
10514
- loupeBtn.textContent = "Loupe";
10515
- loupeBtn.title = "Loupe · hold Z";
11088
+ const toggleBtn = (name, label, title) => {
11089
+ const b = el("button", "knobs-cmp-toggle");
11090
+ b.type = "button";
11091
+ b.tabIndex = -1;
11092
+ b.innerHTML = icon(name);
11093
+ b.setAttribute("aria-label", label);
11094
+ b.title = title;
11095
+ return b;
11096
+ };
11097
+ const loupeBtn = toggleBtn("magnifyingGlass", "Loupe", "Loupe · hold Z");
10516
11098
  loupeBtn.addEventListener("click", () => {
10517
11099
  setLoupe(!loupeOn);
10518
11100
  root.focus({ preventScroll: true });
10519
11101
  });
10520
- const loopBtn = el("button", "knobs-cmp-toggle");
10521
- loopBtn.type = "button";
10522
- loopBtn.tabIndex = -1;
10523
- loopBtn.textContent = "Loop";
10524
- loopBtn.title = "Loop · L (drag its ends on the timeline)";
11102
+ const loopBtn = toggleBtn("repeat", "Loop", "Loop · L (drag its ends on the timeline)");
10525
11103
  loopBtn.setAttribute("aria-keyshortcuts", "L");
10526
11104
  loopBtn.addEventListener("click", () => {
10527
11105
  api.setLoop(!loopNow());
@@ -10538,7 +11116,8 @@ function compare(container, options) {
10538
11116
  const zoomReset = el("button", "knobs-cmp-zoom-reset");
10539
11117
  zoomReset.type = "button";
10540
11118
  zoomReset.tabIndex = -1;
10541
- zoomReset.textContent = "Reset";
11119
+ zoomReset.innerHTML = icon("arrowCounterClockwise", { small: true });
11120
+ zoomReset.setAttribute("aria-label", "Reset zoom");
10542
11121
  zoomReset.title = "Fit · 0 or double-click";
10543
11122
  zoomReset.addEventListener("click", () => {
10544
11123
  api.setZoom(null);
@@ -10548,11 +11127,25 @@ function compare(container, options) {
10548
11127
  const labels = [el("div", "knobs-cmp-label"), el("div", "knobs-cmp-label")];
10549
11128
  labels[0].dataset.side = "before";
10550
11129
  labels[1].dataset.side = "after";
11130
+ const setLabel = (index, text) => {
11131
+ const l = labels[index];
11132
+ if (!l.firstElementChild) {
11133
+ const mark = el("span", "knobs-cmp-mark");
11134
+ mark.innerHTML = icon(index === 0 ? "x" : "check", { small: true });
11135
+ l.append(mark, el("span", "knobs-cmp-name"));
11136
+ }
11137
+ const name = l.lastElementChild;
11138
+ if (name.textContent !== text)
11139
+ name.textContent = text;
11140
+ };
10551
11141
  const handle = el("div", "knobs-cmp-handle");
10552
11142
  handle.setAttribute("role", "slider");
10553
11143
  handle.setAttribute("aria-label", "Wipe");
10554
11144
  handle.setAttribute("aria-valuemin", "0");
10555
11145
  handle.setAttribute("aria-valuemax", "100");
11146
+ const grip = el("span", "knobs-cmp-grip");
11147
+ grip.setAttribute("aria-hidden", "true");
11148
+ handle.append(grip);
10556
11149
  const note = el("div", "knobs-cmp-note");
10557
11150
  stage.append(canvas, split, labels[0], labels[1], handle, zoomPill, note);
10558
11151
  root.append(bar, stage);
@@ -10569,6 +11162,7 @@ function compare(container, options) {
10569
11162
  root.append(host);
10570
11163
  tl = timeline(host, {
10571
11164
  ...look,
11165
+ variant: "lane",
10572
11166
  duration: 1,
10573
11167
  fps: fps(),
10574
11168
  arrows: { step: 0.5, shift: 2 },
@@ -10587,9 +11181,9 @@ function compare(container, options) {
10587
11181
  const fade = new Spring(1, FADE6.omega, FADE6.zeta, 0.001);
10588
11182
  let fromMode = mode;
10589
11183
  let toMode = mode;
10590
- let phase = false;
10591
- let flickerTimer = 0;
10592
- let flickHeld = false;
11184
+ const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
11185
+ let wipeCatching = false;
11186
+ let gripY = null;
10593
11187
  let loupeHeld = false;
10594
11188
  let pointer = null;
10595
11189
  let drag = null;
@@ -10907,25 +11501,16 @@ function compare(container, options) {
10907
11501
  gpu = g;
10908
11502
  setRenderer("gpu");
10909
11503
  };
10910
- const syncFlicker = () => {
10911
- win.clearInterval(flickerTimer);
10912
- flickerTimer = 0;
10913
- phase = false;
10914
- if (mode === "flicker")
10915
- flickerTimer = win.setInterval(() => {
10916
- phase = !phase;
10917
- requestDraw();
10918
- }, FLICKER_MS);
10919
- };
10920
11504
  const syncModeUi = () => {
10921
11505
  root.dataset.mode = mode;
10922
- MODES2.forEach((m, i) => {
10923
- modeBtns[i].setAttribute("aria-checked", String(m.mode === mode));
11506
+ modeList.forEach((m, i) => {
11507
+ modeBtns[i].setAttribute("aria-checked", String(m === mode));
10924
11508
  });
10925
11509
  handle.tabIndex = mode === "wipe" ? 0 : -1;
11510
+ modePill?.update();
10926
11511
  };
10927
11512
  const setMode = (next, emit = true) => {
10928
- if (next === mode)
11513
+ if (next === mode || !modeList.includes(next))
10929
11514
  return;
10930
11515
  mode = next;
10931
11516
  const reduced = reducedMotion(win);
@@ -10941,19 +11526,29 @@ function compare(container, options) {
10941
11526
  else
10942
11527
  lay.set(next === "side" ? 0 : 1);
10943
11528
  syncModeUi();
10944
- syncFlicker();
10945
11529
  if (emit)
10946
11530
  options.onMode?.(next);
10947
11531
  requestDraw();
10948
11532
  };
10949
- const setWipe = (next, emit = true) => {
11533
+ const setWipe = (next, emit = true, jump = false) => {
10950
11534
  const w = Math.min(1, Math.max(0, next));
10951
- if (w === wipe)
11535
+ if (w === wipe && !jump)
10952
11536
  return;
10953
11537
  wipe = w;
11538
+ const minGap = 2 / Math.max(1, wipeRect(layoutNow().L.overlay).w);
11539
+ const head = moveHead({ x: wipeLine.x, target: wipeLine.target }, w, jump && !reducedMotion(win) ? "jump" : "follow", wipeCatching, minGap);
11540
+ wipeCatching = head.catching;
11541
+ if (head.catching) {
11542
+ wipeLine.x = head.x;
11543
+ wipeLine.target = head.target;
11544
+ } else
11545
+ wipeLine.snap(head.x);
10954
11546
  if (emit)
10955
11547
  options.onWipe?.(w);
10956
- drawNow();
11548
+ if (head.catching)
11549
+ requestDraw();
11550
+ else
11551
+ drawNow();
10957
11552
  };
10958
11553
  const setLoupe = (on) => {
10959
11554
  loupeOn = on;
@@ -11106,28 +11701,44 @@ function compare(container, options) {
11106
11701
  viewSent = key;
11107
11702
  options.onView({ zoom: clampZoom(z), loop: l });
11108
11703
  };
11704
+ const labelsIn = (m, r, wr, wx) => {
11705
+ if (m === "wipe") {
11706
+ const at = seamLabels(wx, wr.x, wr.x + wr.w, labelW[0], labelW[1], INSET);
11707
+ return [at.before, at.after];
11708
+ }
11709
+ const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * lay.x;
11710
+ return [
11711
+ { x: r.before.x + INSET, opacity: 1 },
11712
+ { x: ax, opacity: 1 }
11713
+ ];
11714
+ };
11715
+ const placeGrip = (wr) => {
11716
+ const gy = gripTop(gripY ?? wr.y + wr.h / 2, wr, GRIP.h) - wr.y;
11717
+ grip.style.transform = `translate3d(0,${gy.toFixed(2)}px,0)`;
11718
+ };
11109
11719
  const draw = () => {
11110
11720
  dirty = false;
11111
11721
  const r = layoutNow();
11112
11722
  const wr = wipeRect(r.L.overlay);
11113
- const wx = wipeX(wr, wipe);
11723
+ const wx = wipeX(wr, wipeLine.x);
11114
11724
  const k = lay.x;
11115
- const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * k;
11116
- labels[0].style.transform = `translate(${(r.before.x + INSET).toFixed(2)}px,${(r.before.y + INSET).toFixed(2)}px)`;
11117
- labels[1].style.transform = `translate(${ax.toFixed(2)}px,${(r.after.y + INSET).toFixed(2)}px)`;
11118
- const showsBefore = flickHeld || mode === "flicker" && phase;
11119
- const flickering = flickHeld || mode === "flicker";
11120
- labels[0].toggleAttribute("data-lit", flickering && showsBefore);
11121
- labels[1].toggleAttribute("data-lit", flickering && !showsBefore);
11122
- labels[0].toggleAttribute("data-dim", flickering && !showsBefore || mode === "wipe" && wipe < 0.02);
11123
- labels[1].toggleAttribute("data-dim", flickering && showsBefore || mode === "wipe" && wipe > 0.98);
11725
+ const a = labelsIn(fromMode, r, wr, wx);
11726
+ const b = labelsIn(toMode, r, wr, wx);
11727
+ const f = Math.min(1, Math.max(0, fade.x));
11728
+ const ys = [r.before.y + INSET, r.after.y + INSET];
11729
+ for (const i of [0, 1]) {
11730
+ const x = a[i].x + (b[i].x - a[i].x) * f;
11731
+ labels[i].style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
11732
+ labels[i].style.opacity = (a[i].opacity + (b[i].opacity - a[i].opacity) * f).toFixed(3);
11733
+ }
11124
11734
  const top = Math.min(r.L.side.before.y, r.L.side.after.y);
11125
11735
  split.style.transform = `translate(${r.L.side.split.toFixed(2)}px,${top.toFixed(2)}px)`;
11126
11736
  split.style.height = `${Math.max(0, r.L.side.after.h).toFixed(2)}px`;
11127
11737
  split.style.opacity = String(Math.max(0, 1 - k * 2));
11128
- handle.toggleAttribute("data-on", mode === "wipe" && !flickHeld);
11738
+ handle.toggleAttribute("data-on", mode === "wipe");
11129
11739
  handle.style.transform = `translate(${wx.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
11130
11740
  handle.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
11741
+ placeGrip(wr);
11131
11742
  handle.setAttribute("aria-valuenow", String(Math.round(wipe * 100)));
11132
11743
  handle.setAttribute("aria-valuetext", `${Math.round(wipe * 100)}% Before`);
11133
11744
  const pct = `${Math.round(100 / zw.x)}%`;
@@ -11150,7 +11761,7 @@ function compare(container, options) {
11150
11761
  const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, cutAt));
11151
11762
  st.clipPath = t + l + b + right > 0.01 ? `inset(${t.toFixed(2)}px ${right.toFixed(2)}px ${b.toFixed(2)}px ${l.toFixed(2)}px)` : "";
11152
11763
  };
11153
- const cut = mode === "wipe" && !flickHeld ? wx : Number.POSITIVE_INFINITY;
11764
+ const cut = mode === "wipe" ? wx : Number.POSITIVE_INFINITY;
11154
11765
  for (const s of sides) {
11155
11766
  const rect = s.index === 0 ? r.zBefore : r.zAfter;
11156
11767
  const clip = s.index === 0 ? r.before : r.after;
@@ -11159,10 +11770,8 @@ function compare(container, options) {
11159
11770
  place(s.preview, rect, clip, at);
11160
11771
  if (s.index !== 0)
11161
11772
  continue;
11162
- for (const n of [s.el, s.preview]) {
11163
- n.style.mixBlendMode = mode === "difference" && !flickHeld ? "difference" : "";
11164
- n.style.opacity = mode === "flicker" && !showsBefore ? "0" : "";
11165
- }
11773
+ for (const n of [s.el, s.preview])
11774
+ n.style.mixBlendMode = mode === "difference" ? "difference" : "";
11166
11775
  }
11167
11776
  return;
11168
11777
  }
@@ -11183,8 +11792,6 @@ function compare(container, options) {
11183
11792
  from: MODE_INDEX[fromMode],
11184
11793
  to: MODE_INDEX[toMode],
11185
11794
  mix: Math.min(1, Math.max(0, fade.x)),
11186
- flick: flickHeld,
11187
- phase,
11188
11795
  threshold,
11189
11796
  gain: 1,
11190
11797
  accent,
@@ -11200,6 +11807,11 @@ function compare(container, options) {
11200
11807
  const loop = frameLoop(win, (dt) => {
11201
11808
  lay.step(dt);
11202
11809
  fade.step(dt);
11810
+ if (wipeCatching) {
11811
+ wipeLine.step(dt);
11812
+ if (wipeLine.resting)
11813
+ wipeCatching = false;
11814
+ }
11203
11815
  zu.step(dt);
11204
11816
  zv.step(dt);
11205
11817
  zw.step(dt);
@@ -11233,7 +11845,7 @@ function compare(container, options) {
11233
11845
  pushFrame(s);
11234
11846
  }
11235
11847
  }
11236
- const moving = !lay.resting || !fade.resting || !zoomResting();
11848
+ const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching;
11237
11849
  if (moving)
11238
11850
  dirty = true;
11239
11851
  if (auto && !lay.resting)
@@ -11376,16 +11988,19 @@ function compare(container, options) {
11376
11988
  e.preventDefault();
11377
11989
  return startPinch();
11378
11990
  }
11379
- if (mode === "wipe" && !flickHeld) {
11991
+ if (mode === "wipe") {
11380
11992
  const wr = wipeRect(layoutNow().L.overlay);
11381
11993
  const inside = p.x >= wr.x - SLOP && p.x <= wr.x + wr.w + SLOP && p.y >= wr.y && p.y <= wr.y + wr.h;
11382
- const grab = wipeGrab(p.x, wr, wipe, SLOP);
11994
+ const grab = wipeGrab(p.x, wr, wipeLine.x, SLOP);
11383
11995
  if (inside && (grab.onLine || !zoomed())) {
11384
11996
  drag = { kind: "wipe", offset: grab.offset };
11385
11997
  root.dataset.dragging = "";
11998
+ gripY = p.y;
11386
11999
  e.preventDefault();
11387
12000
  if (!grab.onLine)
11388
- setWipe(wipeAt(p.x, wr));
12001
+ setWipe(wipeAt(p.x, wr), true, true);
12002
+ else
12003
+ drawNow();
11389
12004
  return;
11390
12005
  }
11391
12006
  }
@@ -11411,6 +12026,11 @@ function compare(container, options) {
11411
12026
  pinch = { d, x: mid.x, y: mid.y };
11412
12027
  return handZoom(zoomAbout(panned, k, q.pu, q.pv));
11413
12028
  }
12029
+ if (mode === "wipe" && drag?.kind !== "pan") {
12030
+ gripY = p.y;
12031
+ if (!drag)
12032
+ placeGrip(wipeRect(layoutNow().L.overlay));
12033
+ }
11414
12034
  if (drag?.kind === "wipe")
11415
12035
  setWipe(wipeAt(p.x, wipeRect(layoutNow().L.overlay), drag.offset));
11416
12036
  else if (drag?.kind === "pan") {
@@ -11483,12 +12103,6 @@ function compare(container, options) {
11483
12103
  const z = { u: zu.target, v: zv.target, w: zw.target, h: zw.target };
11484
12104
  glideZoom(zoomAbout(z, factor, 0.5, 0.5));
11485
12105
  };
11486
- const holdFlick = (on) => {
11487
- if (flickHeld === on)
11488
- return;
11489
- flickHeld = on;
11490
- drawNow();
11491
- };
11492
12106
  const holdLoupe = (on) => {
11493
12107
  if (loupeHeld === on)
11494
12108
  return;
@@ -11534,25 +12148,17 @@ function compare(container, options) {
11534
12148
  case "s":
11535
12149
  case "w":
11536
12150
  case "d":
11537
- if (e.altKey || e.shiftKey || key === "s" && claimsHintKey(doc, key))
11538
- return;
11539
- claim();
11540
- return setMode(KEY_MODE[key]);
11541
12151
  case "1":
11542
12152
  case "2":
11543
- case "3":
11544
- case "4":
11545
- if (e.altKey || e.shiftKey)
12153
+ case "3": {
12154
+ if (e.altKey || e.shiftKey || key === "s" && claimsHintKey(doc, key))
11546
12155
  return;
11547
- claim();
11548
- return setMode(MODES2[Number(key) - 1].mode);
11549
- case "x":
11550
- if (e.altKey)
12156
+ const m = modeForKey(key, modeList);
12157
+ if (!m)
11551
12158
  return;
11552
12159
  claim();
11553
- if (!e.repeat)
11554
- holdFlick(true);
11555
- return;
12160
+ return setMode(m);
12161
+ }
11556
12162
  case "z":
11557
12163
  if (e.altKey)
11558
12164
  return;
@@ -11593,7 +12199,7 @@ function compare(container, options) {
11593
12199
  old.preview.replaceWith(s.preview);
11594
12200
  sides[index] = s;
11595
12201
  note.textContent = "";
11596
- labels[index].textContent = labelOf(index);
12202
+ setLabel(index, labelOf(index));
11597
12203
  if (near)
11598
12204
  loadSide(s);
11599
12205
  readColors();
@@ -11604,12 +12210,15 @@ function compare(container, options) {
11604
12210
  stage.insertBefore(sides[1].preview, labels[0]);
11605
12211
  stage.insertBefore(sides[0].el, labels[0]);
11606
12212
  stage.insertBefore(sides[0].preview, labels[0]);
11607
- labels[0].textContent = labelOf(0);
11608
- labels[1].textContent = labelOf(1);
12213
+ setLabel(0, labelOf(0));
12214
+ setLabel(1, labelOf(1));
11609
12215
  loupeBtn.setAttribute("aria-pressed", String(loupeOn));
11610
12216
  loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
12217
+ const modePill = modeList.length > 1 ? selectionPill(modes, {
12218
+ rows: () => modeBtns,
12219
+ selected: () => modeBtns[modeList.indexOf(mode)] ?? null
12220
+ }) : null;
11611
12221
  syncModeUi();
11612
- syncFlicker();
11613
12222
  const unlisten = tl?.listen({
11614
12223
  onSeek: (t, source) => {
11615
12224
  if (source === "play") {
@@ -11667,24 +12276,20 @@ function compare(container, options) {
11667
12276
  });
11668
12277
  root.addEventListener("keydown", onKey);
11669
12278
  root.addEventListener("keyup", (e) => {
11670
- const key = e.key.toLowerCase();
11671
- if (key === "x")
11672
- holdFlick(false);
11673
- else if (key === "z")
12279
+ if (e.key.toLowerCase() === "z")
11674
12280
  holdLoupe(false);
11675
12281
  });
11676
12282
  root.addEventListener("focusout", (e) => {
11677
12283
  if (root.contains(e.relatedTarget))
11678
12284
  return;
11679
- holdFlick(false);
11680
12285
  holdLoupe(false);
11681
12286
  });
11682
12287
  const unregisterHints = options.hints === false ? undefined : hints.register(root, () => [
11683
- ...MODES2.map((m, i) => ({
12288
+ ...modeList.length > 1 ? modeList.map((m, i) => ({
11684
12289
  el: modeBtns[i],
11685
- label: m.label,
11686
- activate: () => setMode(m.mode)
11687
- })),
12290
+ label: MODE_LABEL[m].long,
12291
+ activate: () => setMode(m)
12292
+ })) : [],
11688
12293
  { el: loopBtn, label: "Loop", activate: () => api.setLoop(!loopNow()) },
11689
12294
  ...loupeBtn.hidden ? [] : [{ el: loupeBtn, label: "Loupe", activate: () => setLoupe(!loupeOn) }],
11690
12295
  ...zoomed() ? [{ el: zoomReset, label: "Reset zoom", activate: () => api.setZoom(null) }] : [],
@@ -11709,6 +12314,7 @@ function compare(container, options) {
11709
12314
  get mode() {
11710
12315
  return mode;
11711
12316
  },
12317
+ modes: modeList,
11712
12318
  get wipe() {
11713
12319
  return wipe;
11714
12320
  },
@@ -11745,11 +12351,11 @@ function compare(container, options) {
11745
12351
  if (before)
11746
12352
  replace(0, before);
11747
12353
  else if (next.before)
11748
- labels[0].textContent = next.before.label ?? "Before";
12354
+ setLabel(0, next.before.label ?? "Before");
11749
12355
  if (after)
11750
12356
  replace(1, after);
11751
12357
  else if (next.after)
11752
- labels[1].textContent = next.after.label ?? "After";
12358
+ setLabel(1, next.after.label ?? "After");
11753
12359
  if (before || after)
11754
12360
  strip?.update({ ...before ? { before } : {}, ...after ? { after } : {} });
11755
12361
  if (next.before || next.after)
@@ -11764,7 +12370,7 @@ function compare(container, options) {
11764
12370
  tl?.update({ fps: next.fps });
11765
12371
  requestDraw();
11766
12372
  },
11767
- seek: (t) => tl ? tl.seek(t) : ownSeek(t),
12373
+ seek: (t, o) => tl ? tl.seek(t, o?.glide ? { glide: true } : undefined) : ownSeek(t),
11768
12374
  seekBy(d) {
11769
12375
  const total = duration || tl?.duration || 0;
11770
12376
  const t = Math.min(total, Math.max(0, now() + d));
@@ -11805,7 +12411,7 @@ function compare(container, options) {
11805
12411
  dispose() {
11806
12412
  disposed = true;
11807
12413
  loop.stop();
11808
- win.clearInterval(flickerTimer);
12414
+ modePill?.dispose();
11809
12415
  win.clearTimeout(releaseTimer);
11810
12416
  ro.disconnect();
11811
12417
  nearIo?.disconnect();
@@ -11835,7 +12441,7 @@ function compare(container, options) {
11835
12441
  wake();
11836
12442
  return api;
11837
12443
  }
11838
- var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, FLICKER_MS = 400, SLOP = 12, INSET = 8, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODES2, KEY_MODE;
12444
+ var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
11839
12445
  var init_compare = __esm(() => {
11840
12446
  init_motion();
11841
12447
  init_page_hints();
@@ -11843,21 +12449,24 @@ var init_compare = __esm(() => {
11843
12449
  init_compare_gpu();
11844
12450
  init_compare_model();
11845
12451
  init_compare_strip();
12452
+ init_icons();
11846
12453
  init_look();
11847
12454
  init_rgba();
12455
+ init_selection_pill();
11848
12456
  init_timeline();
12457
+ init_timeline_model();
11849
12458
  LAYOUT2 = springMotion({ duration: 0.35, bounce: 0 });
11850
12459
  FADE6 = springMotion(SPRINGS.snappy);
11851
12460
  FADE_REDUCED = springMotion(FADE);
11852
12461
  ZOOM = springMotion({ duration: 0.35, bounce: 0 });
12462
+ WIPE_CATCH = springMotion(CATCH_UP);
12463
+ GRIP = { w: 16, h: 28 };
11853
12464
  LOUPE = { r: 88, zoom: 4 };
11854
- MODES2 = [
11855
- { mode: "side", label: "Side by side", key: "S" },
11856
- { mode: "wipe", label: "Wipe", key: "W" },
11857
- { mode: "difference", label: "Difference", key: "D" },
11858
- { mode: "flicker", label: "Flicker", key: "" }
11859
- ];
11860
- KEY_MODE = { s: "side", w: "wipe", d: "difference" };
12465
+ MODE_LABEL = {
12466
+ side: { short: "Side", long: "Side by side" },
12467
+ wipe: { short: "Wipe", long: "Wipe" },
12468
+ difference: { short: "Difference", long: "Difference" }
12469
+ };
11861
12470
  });
11862
12471
 
11863
12472
  // src/ui/react.tsx
@@ -14757,6 +15366,7 @@ function timelineChanges(now, changed) {
14757
15366
  // src/ui/picker.ts
14758
15367
  init_motion();
14759
15368
  init_look();
15369
+ init_icons();
14760
15370
 
14761
15371
  // src/ui/picker-model.ts
14762
15372
  function digitPick(ids, key) {
@@ -14811,7 +15421,7 @@ var CLOSE3 = springMotion(CLOSE);
14811
15421
  var CARD = springMotion(SPRINGS.default);
14812
15422
  var FADE5 = springMotion(FADE);
14813
15423
  var DRIFT = 6;
14814
- var CHECK = svg('<path d="m3.5 8.4 3 3 6-6.6"/>');
15424
+ var CHECK = icon("check", { small: true });
14815
15425
  function optionPicker(container, options) {
14816
15426
  const doc = container.ownerDocument;
14817
15427
  const win = doc.defaultView;
@@ -16288,7 +16898,9 @@ function confirm(options) {
16288
16898
  }
16289
16899
 
16290
16900
  // src/ui/walkthrough.ts
16901
+ init_icons();
16291
16902
  init_look();
16903
+ init_selection_pill();
16292
16904
 
16293
16905
  // src/ui/toast.ts
16294
16906
  init_look();
@@ -16354,399 +16966,11 @@ function toast(message, options = {}) {
16354
16966
  };
16355
16967
  }
16356
16968
 
16357
- // src/ui/walkthrough-media.ts
16358
- init_compare_model();
16359
- init_glide();
16360
- init_look();
16361
-
16362
- // src/ui/walkthrough-css.ts
16969
+ // src/ui/walkthrough-blocks.ts
16363
16970
  init_motion();
16364
- var snappy3 = springCss(SPRINGS.snappy);
16365
- var calm = springCss(SPRINGS.calm);
16366
- var walkthroughCss = `
16367
- :where(.knobs-walkthrough,.knobs-walkthrough *,.knobs-wt-slot,.knobs-wt-slot *){box-sizing:border-box}
16368
- .knobs-ui.knobs-walkthrough{--kw-snappy:${snappy3.easing};--kw-snappy-ms:${snappy3.duration}ms;--kw-calm:${calm.easing};--kw-calm-ms:${calm.duration}ms;
16369
- --kw-text:var(--k-walkthrough-text,14px);--kw-measure:var(--k-walkthrough-measure,68ch);--kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));
16370
- --kw-card:var(--_ku-pane-bg);--kw-card-ring:var(--_ku-pane-ring);--kw-pad:16px;--kw-ink:var(--k-walkthrough-ink,#ff375f);
16371
- container:knobs-wt / inline-size;display:block;width:100%;color:var(--_k-fg);font:var(--_k-weight) var(--kw-text)/1.5 var(--_k-font);text-align:left;
16372
- letter-spacing:normal;-webkit-font-smoothing:antialiased}
16373
- .knobs-walkthrough [hidden]{display:none!important}
16374
- .knobs-walkthrough :is(h1,h2,h3,p){margin:0;font:inherit;letter-spacing:inherit}
16375
- .knobs-walkthrough a{color:inherit;text-decoration:underline;text-decoration-color:color-mix(in srgb,currentColor 35%,transparent);text-underline-offset:2px;transition:text-decoration-color .15s}
16376
- .knobs-walkthrough a:hover{text-decoration-color:currentColor}
16377
- .knobs-walkthrough code,.knobs-wt-slot code{font:calc(1em - 1px) var(--_k-mono);padding:1px 5px;border-radius:5px;background:var(--_k-fill)}
16378
- :where(.knobs-ui) :is(.knobs-wt-btn,.knobs-wt-pick,.knobs-wt-toc-step,button.knobs-wt-stitle,.knobs-wt-chapter){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;
16379
- background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
16380
-
16381
- /* ——— layout: the rail beside the column; one column where it's narrow ——— */
16382
- .knobs-wt-layout{display:grid;grid-template-columns:200px minmax(0,1fr);gap:40px;align-items:start}
16383
- .knobs-wt-main{display:flex;flex-direction:column;gap:56px;min-width:0;max-width:1120px;padding-bottom:40vh}
16384
- @container knobs-wt (max-width:880px){.knobs-wt-layout{grid-template-columns:minmax(0,1fr);gap:0}.knobs-wt-main{gap:44px}}
16385
-
16386
- /* ——— the page's head ——— */
16387
- .knobs-wt-head{display:flex;flex-direction:column;gap:10px}
16388
- .knobs-wt-title{font-size:calc(var(--kw-text) + 10px)!important;font-weight:var(--_k-weight-title)!important;line-height:1.2;letter-spacing:-.01em!important}
16389
- .knobs-wt-summary{max-width:var(--kw-measure);color:var(--_k-fg-2)}
16390
- .knobs-wt-meta{display:flex;flex-wrap:wrap;gap:6px 16px;color:var(--_k-fg-3);font-size:calc(var(--kw-text) - 2px)}
16391
- .knobs-wt-meta>span{display:inline-flex;gap:6px;white-space:nowrap}
16392
- .knobs-wt-meta b{font-weight:var(--_k-weight);color:var(--_k-fg-2);font-family:var(--_k-mono);font-size:calc(1em - .5px)}
16393
-
16394
- /* ——— a section: a PR or topic ——— */
16395
- .knobs-wt-section{display:flex;flex-direction:column;gap:14px;min-width:0}
16396
- .knobs-wt-shead{display:flex;flex-direction:column;gap:6px;scroll-margin-top:16px}
16397
- .knobs-wt-kicker{display:flex;align-items:center;gap:10px;font-size:calc(var(--kw-text) - 2px);color:var(--_k-fg-3)}
16398
- .knobs-wt-kicker a{font-family:var(--_k-mono);color:var(--_k-fg-2);text-decoration:none}
16399
- .knobs-wt-kicker a:hover{color:var(--_k-fg)}
16400
- .knobs-wt-stitle{font-size:calc(var(--kw-text) + 4px)!important;font-weight:var(--_k-weight-title)!important;line-height:1.3}
16401
- .knobs-wt-ssum{max-width:var(--kw-measure);color:var(--_k-fg-2)}
16402
- .knobs-wt-chip{display:inline-flex;align-items:center;gap:6px;height:20px;padding:0 8px;border-radius:999px;font-size:calc(var(--kw-text) - 3px);font-weight:var(--_k-weight-label);white-space:nowrap;
16403
- --kw-tone:var(--_k-fg-2);color:var(--kw-tone);background:color-mix(in srgb,var(--kw-tone) 14%,transparent)}
16404
- .knobs-wt-chip::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
16405
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="good"]{--kw-tone:var(--kw-good)}
16406
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="warn"]{--kw-tone:var(--_k-warn)}
16407
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="bad"]{--kw-tone:var(--_k-danger)}
16408
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="info"]{--kw-tone:var(--kw-info)}
16409
- :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="dim"]{--kw-tone:var(--_k-fg-2)}
16410
-
16411
- /* ——— a step: one card ——— */
16412
- .knobs-wt-step{position:relative;display:flex;flex-direction:column;gap:12px;padding:var(--kw-pad);border-radius:var(--_k-radius-panel);background:var(--kw-card);box-shadow:var(--kw-card-ring);
16413
- scroll-margin-top:16px;outline:none;container:knobs-wt-step / inline-size}
16414
- .knobs-walkthrough[data-frame="flat"] .knobs-wt-step{box-shadow:inset 0 0 0 1px var(--_k-border)}
16415
- .knobs-wt-step-head{display:flex;align-items:baseline;gap:10px;min-width:0}
16416
- .knobs-wt-num{flex:none;font:var(--_k-weight) calc(var(--kw-text) - 2px)/1 var(--_k-mono);color:var(--_k-fg-3);font-variant-numeric:tabular-nums}
16417
- .knobs-wt-stepTitle{flex:1;min-width:0;font-weight:var(--_k-weight-title)!important}
16418
- .knobs-wt-caption{max-width:var(--kw-measure);color:var(--_k-fg-2);margin-top:-6px!important}
16419
- .knobs-wt-question{font-weight:var(--_k-weight-label)!important}
16420
- /* The step Kit is on: its number lights, and its choices show their keys. */
16421
- .knobs-wt-step[data-current] .knobs-wt-num{color:var(--_k-fg)}
16422
- .knobs-wt-media{display:grid;grid-template-columns:repeat(var(--kw-n,1),minmax(0,1fr));gap:12px;min-width:0}
16423
- @container knobs-wt-step (max-width:640px){.knobs-wt-media{grid-template-columns:minmax(0,1fr)}}
16424
-
16425
- /* ——— the feed: the agent's updates and answers, newest first, compact ——— */
16426
- .knobs-wt-feed{margin-top:-24px}
16427
- .knobs-wt-feed-list{list-style:none;margin:0;padding:0;max-height:260px;overflow:auto;scrollbar-width:none;display:flex;flex-direction:column}
16428
- .knobs-wt-feed-list::-webkit-scrollbar{display:none}
16429
- .knobs-wt-fi{display:grid;grid-template-columns:64px 56px minmax(0,1fr) auto;gap:10px;align-items:baseline;padding:6px 0;font-size:calc(var(--kw-text) - 1px)}
16430
- .knobs-wt-fi+.knobs-wt-fi{box-shadow:inset 0 1px 0 var(--_k-border)}
16431
- .knobs-wt-fi-when{color:var(--_k-fg-3);font-size:calc(var(--kw-text) - 2.5px);font-variant-numeric:tabular-nums;white-space:nowrap}
16432
- .knobs-wt-fi-kind{color:var(--_k-fg-2);font-size:calc(var(--kw-text) - 2.5px);font-weight:var(--_k-weight-label)}
16433
- .knobs-wt-fi[data-kind="qa"] .knobs-wt-fi-kind{color:var(--kw-info)}
16434
- .knobs-wt-fi-body{max-width:var(--kw-measure);color:var(--_k-fg)}
16435
- .knobs-wt-fi-q{color:var(--_k-fg-2)}
16436
- .knobs-wt-fi-q::before{content:"You asked: ";color:var(--_k-fg-3)}
16437
- :where(.knobs-ui) :is(.knobs-wt-fi-step,.knobs-wt-steplink){all:revert;box-sizing:border-box;font:var(--_k-weight) calc(var(--k-walkthrough-text,14px) - 2.5px)/1 var(--_k-mono);color:var(--_k-fg-2);background:transparent;border:0;
16438
- padding:3px 6px;margin:0;border-radius:6px;cursor:pointer;outline:none;transition:background .15s,color .15s}
16439
- .knobs-ui :is(.knobs-wt-fi-step,.knobs-wt-steplink):is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
16440
-
16441
- /* ——— a step's sent notes ——— */
16442
- .knobs-wt-notes{display:flex;flex-direction:column}
16443
- .knobs-wt-notes:empty{display:none}
16444
- .knobs-wt-n{display:flex;align-items:flex-start;gap:10px;padding:6px 0;overflow:hidden;
16445
- transition:height var(--kw-snappy-ms) var(--kw-snappy),opacity var(--kw-snappy-ms) var(--kw-snappy),filter var(--kw-snappy-ms) var(--kw-snappy)}
16446
- .knobs-wt-n[data-entering]{opacity:0;filter:blur(3px)}
16447
- .knobs-wt-n[data-leaving]{opacity:0;filter:blur(3px);padding-block:0}
16448
- .knobs-wt-n+.knobs-wt-n{box-shadow:inset 0 1px 0 var(--_k-border)}
16449
- .knobs-wt-n-meta{flex:none;min-width:44px;padding-top:2px;color:var(--_k-fg-3);font:var(--_k-weight) calc(var(--kw-text) - 2.5px)/1.4 var(--_k-mono);font-variant-numeric:tabular-nums}
16450
- .knobs-wt-n-meta[data-seek]{cursor:pointer}
16451
- .knobs-wt-n-meta[data-seek]:hover{color:var(--_k-fg)}
16452
- .knobs-wt-n-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px;font-size:calc(var(--kw-text) - 1px)}
16453
- .knobs-wt-n-voice{height:30px;max-width:320px}
16454
- .knobs-wt-n-ink{display:block;width:120px;max-height:80px;border-radius:6px;background:var(--_k-fill)}
16455
- .knobs-wt-n-ink path{fill:none;stroke:var(--kw-ink);stroke-width:2px;vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round}
16456
-
16457
- /* ——— the note bar, docked at the bottom of the column ——— */
16458
- .knobs-wt-dock{position:sticky;bottom:12px;z-index:40;margin-top:-24px}
16459
- .knobs-wt-notebar{display:flex;flex-direction:column;gap:2px;padding:6px 6px 6px 14px;border-radius:calc(var(--_k-radius-panel) + 2px);background:var(--_k-bg);
16460
- box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay)}
16461
- .knobs-wt-on{padding-top:3px;color:var(--_k-fg-3);font-size:calc(var(--kw-text) - 2.5px);white-space:nowrap;overflow:hidden;
16462
- -webkit-mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent)}
16463
- .knobs-wt-row{display:flex;align-items:flex-end;gap:4px}
16464
- :where(.knobs-ui) :is(.knobs-wt-input,.knobs-wt-tool,.knobs-wt-x,.knobs-wt-back){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;
16465
- border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
16466
- .knobs-ui .knobs-wt-input{flex:1;min-width:0;height:32px;max-height:140px;padding:6px 0;resize:none;color:var(--_k-fg);caret-color:var(--_k-accent);line-height:20px;font-size:var(--kw-text)}
16467
- .knobs-ui .knobs-wt-input::placeholder{color:var(--_k-fg-3)}
16468
- .knobs-wt-attach{display:flex;gap:4px;align-items:center;align-self:center}
16469
- .knobs-wt-attach:empty{display:none}
16470
- .knobs-wt-chip-a{display:inline-flex;align-items:center;gap:2px;height:24px;padding:0 2px 0 9px;border-radius:999px;background:var(--_k-fill-selected);font-size:calc(var(--kw-text) - 2.5px);white-space:nowrap}
16471
- .knobs-ui .knobs-wt-x{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;color:var(--_k-fg-3);cursor:pointer;transition:background .15s,color .15s}
16472
- .knobs-ui .knobs-wt-x:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
16473
- .knobs-wt-x svg{width:10px;height:10px}
16474
- .knobs-ui .knobs-wt-tool{flex:none;display:inline-flex;align-items:center;justify-content:center;gap:5px;min-width:32px;height:32px;padding:0 8px;border-radius:calc(var(--_k-radius) + 2px);
16475
- color:var(--_k-fg-2);cursor:pointer;transition:background .15s,color .15s}
16476
- .knobs-ui .knobs-wt-tool:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
16477
- .knobs-ui .knobs-wt-tool[aria-pressed="true"]{background:var(--_k-fill-selected);color:var(--_k-fg)}
16478
- .knobs-wt-tool svg{width:15px;height:15px}
16479
- .knobs-wt-rec{font-size:calc(var(--kw-text) - 2.5px);font-variant-numeric:tabular-nums}
16480
- .knobs-wt-rec:empty{display:none}
16481
- .knobs-ui .knobs-wt-tool[data-send]{color:var(--_k-fg-3)}
16482
- .knobs-ui .knobs-wt-tool[data-send][data-ready]{background:var(--_k-accent);color:var(--_k-bg)}
16483
- /* Back: in place over the bar for a few seconds after a note moves the page on. */
16484
- .knobs-ui .knobs-wt-back{position:absolute;left:8px;bottom:calc(100% + 8px);height:28px;padding:0 12px;border-radius:999px;background:var(--_k-bg);color:var(--_k-fg);
16485
- box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay);font-size:calc(var(--kw-text) - 1.5px);font-weight:var(--_k-weight-label);cursor:pointer;
16486
- opacity:0;scale:.96;filter:blur(3px);pointer-events:none;transition:opacity .11s cubic-bezier(.4,0,1,1),scale .11s cubic-bezier(.4,0,1,1),filter .11s cubic-bezier(.4,0,1,1)}
16487
- .knobs-ui .knobs-wt-back[data-on]{opacity:1;scale:1;filter:none;pointer-events:auto;transition:opacity .15s cubic-bezier(.2,.8,.2,1),scale .15s cubic-bezier(.2,.8,.2,1),filter .15s cubic-bezier(.2,.8,.2,1)}
16488
- .knobs-ui .knobs-wt-back:hover{background:var(--_k-fill-selected)}
16489
-
16490
- /* ——— ink over a step's media ——— */
16491
- .knobs-wt-media,.knobs-wt-options{position:relative}
16492
- .knobs-wt-ink{position:absolute;inset:0;z-index:5;width:100%;height:100%;overflow:visible;pointer-events:none;touch-action:none}
16493
- .knobs-wt-ink[data-on]{pointer-events:auto;cursor:crosshair}
16494
- .knobs-wt-ink path{fill:none;stroke:var(--kw-ink);stroke-width:3px;vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round}
16495
-
16496
- /* ——— full-window: the step over the page, its media filling it; its place held in the page ——— */
16497
- .knobs-wt-scrim{position:fixed;inset:0;z-index:30;background:color-mix(in srgb,var(--_k-bg-page) 82%,transparent);opacity:0;pointer-events:none}
16498
- .knobs-wt-scrim[data-on]{pointer-events:auto}
16499
- .knobs-wt-hold-card{flex:none}
16500
- .knobs-wt-step[data-full]{position:fixed;z-index:35;margin:0;overflow:hidden;overscroll-behavior:contain;padding-bottom:84px;box-shadow:var(--kw-card-ring),var(--_k-shadow-overlay);background:var(--_k-bg)}
16501
- .knobs-wt-step[data-full]>:not(.knobs-wt-media){flex:none}
16502
- .knobs-wt-step[data-full] .knobs-wt-notes{display:none}
16503
- .knobs-wt-step[data-full]>.knobs-wt-media{flex:1;min-height:0;grid-auto-rows:minmax(0,1fr)}
16504
- .knobs-wt-step[data-full] .knobs-wt-slot{display:flex;flex-direction:column;min-height:0}
16505
- .knobs-wt-step[data-full] .knobs-wt-body{flex:1;min-height:0}
16506
- .knobs-wt-step[data-full] .knobs-wt-fit{height:100%;transition:none}
16507
- .knobs-wt-step[data-full] .knobs-wt-frame{height:100%;aspect-ratio:auto}
16508
-
16509
- /* ——— a taste test: the options side by side, Kit's pick lit and the rest stepping back ——— */
16510
- .knobs-wt-options{display:grid;grid-template-columns:repeat(var(--kw-n,2),minmax(0,1fr));gap:10px}
16511
- @container knobs-wt-step (max-width:640px){.knobs-wt-options{grid-template-columns:minmax(0,1fr)}}
16512
- .knobs-wt-option{position:relative;display:flex;flex-direction:column;gap:8px;min-width:0;padding:8px;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_ku-wash);
16513
- transition:background var(--kw-snappy-ms) var(--kw-snappy)}
16514
- .knobs-wt-option[data-picked]{background:color-mix(in srgb,var(--_k-accent) 12%,var(--_ku-wash))}
16515
- .knobs-wt-option>:not(.knobs-wt-pick){transition:opacity var(--kw-snappy-ms) var(--kw-snappy)}
16516
- .knobs-wt-options[data-has-pick] .knobs-wt-option:not([data-picked])>:not(.knobs-wt-pick){opacity:.55}
16517
- .knobs-ui .knobs-wt-pick{display:flex;align-items:center;gap:8px;width:100%;min-height:28px;padding:0 4px;border-radius:calc(var(--_k-radius) - 2px);cursor:pointer;color:var(--_k-fg-2);text-align:left}
16518
- .knobs-ui .knobs-wt-pick:is(:hover,:focus-visible){color:var(--_k-fg)}
16519
- .knobs-wt-letter{flex:none;font:var(--_k-weight-title) calc(var(--kw-text) - 1px)/1 var(--_k-font);color:var(--_k-fg)}
16520
- .knobs-wt-olabel{flex:1;min-width:0;font-weight:var(--_k-weight-label);color:var(--_k-fg)}
16521
- .knobs-wt-suggested{flex:none;font-size:calc(var(--kw-text) - 3px);color:var(--_k-fg-3)}
16522
- .knobs-wt-check{flex:none;display:flex;width:18px;height:18px;align-items:center;justify-content:center;border-radius:50%;background:var(--_k-fill);color:transparent;
16523
- transition:background var(--kw-snappy-ms) var(--kw-snappy),color var(--kw-snappy-ms) var(--kw-snappy)}
16524
- .knobs-wt-check svg{width:12px;height:12px}
16525
- .knobs-wt-option[data-picked] .knobs-wt-check{background:var(--_k-accent);color:var(--_k-bg)}
16526
- .knobs-wt-key{display:inline-flex;align-items:center;justify-content:center;min-width:15px;height:15px;padding:0 3px;border-radius:4px;background:var(--_k-fill);color:var(--_k-fg-3);
16527
- font:var(--_k-weight-label) calc(var(--kw-text) - 4px)/1 var(--_k-font);font-variant-numeric:tabular-nums;opacity:.5;transition:opacity .15s}
16528
- .knobs-wt-step[data-current] .knobs-wt-key{opacity:1}
16529
- .knobs-wt-ocap{color:var(--_k-fg-2);font-size:calc(var(--kw-text) - 1px);padding:0 4px}
16530
-
16531
- /* ——— the rail: progress, contents, export ——— */
16532
- .knobs-wt-rail{position:sticky;top:16px;display:flex;flex-direction:column;gap:14px;max-height:calc(100vh - 32px);min-height:0;font-size:calc(var(--kw-text) - 1.5px)}
16533
- .knobs-wt-progress{display:flex;flex-direction:column;gap:7px}
16534
- .knobs-wt-count{display:flex;align-items:baseline;justify-content:space-between;color:var(--_k-fg-2);font-variant-numeric:tabular-nums}
16535
- .knobs-wt-count b{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
16536
- .knobs-wt-bar{position:relative;height:4px;border-radius:2px;overflow:hidden;background:var(--_k-fill)}
16537
- .knobs-wt-fill{position:absolute;inset:0;border-radius:inherit;background:var(--_k-accent);transform-origin:0 50%;transform:scaleX(var(--kw-done,0))}
16538
- .knobs-walkthrough[data-settled] .knobs-wt-fill{transition:transform var(--kw-calm-ms) var(--kw-calm)}
16539
- /* The contents: no box, one text spine for sections and steps (the steps' dots hang in the
16540
- gutter left of it), and one pill behind the current step that springs between rows. */
16541
- .knobs-wt-toc{position:relative;flex:1 1 auto;min-height:0;overflow:auto;scrollbar-width:none;margin:0 -8px;padding:0 0 8px}
16542
- .knobs-wt-toc::-webkit-scrollbar{display:none}
16543
- .knobs-wt-toc-section{display:block;padding:12px 8px 4px 26px;color:var(--_k-fg-3);font-size:calc(var(--kw-text) - 3px);font-weight:var(--_k-weight-label);white-space:nowrap;overflow:hidden;
16544
- -webkit-mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent)}
16545
- .knobs-wt-toc-section:first-of-type{padding-top:2px}
16546
- .knobs-wt-toc-mark{position:absolute;left:0;right:0;top:0;height:0;border-radius:calc(var(--_k-radius) - 1px);background:var(--_ku-wash-hi);pointer-events:none;opacity:0;will-change:transform}
16547
- .knobs-wt-toc-mark[data-on]{opacity:1}
16548
- .knobs-walkthrough[data-settled] .knobs-wt-toc-mark{transition:opacity .15s}
16549
- .knobs-ui .knobs-wt-toc-step{position:relative;display:flex;align-items:center;width:100%;text-align:left;height:26px;padding:0 8px 0 26px;color:var(--_k-fg-2);cursor:pointer;white-space:nowrap}
16550
- .knobs-ui .knobs-wt-toc-step:is(:hover,:focus-visible){color:var(--_k-fg)}
16551
- .knobs-wt-toc-step[aria-current="step"]{color:var(--_k-fg)}
16552
- .knobs-wt-toc-label{flex:1;min-width:0;overflow:hidden;-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent)}
16553
- /* A step's state: an open ring, a filled dot once answered, nothing to answer a faint ring. */
16554
- .knobs-wt-toc-glyph{--kw-tone:var(--_k-fg-3);position:absolute;left:11px;top:50%;width:7px;height:7px;margin-top:-3.5px;border-radius:50%;box-shadow:inset 0 0 0 1.5px var(--kw-tone);
16555
- transition:background var(--kw-snappy-ms) var(--kw-snappy),box-shadow var(--kw-snappy-ms) var(--kw-snappy)}
16556
- .knobs-wt-toc-glyph[data-tone="accent"]{--kw-tone:var(--_k-accent)}
16557
- .knobs-wt-toc-glyph[data-done]{background:var(--kw-tone)}
16558
- .knobs-wt-toc-glyph[data-tone="none"]{box-shadow:inset 0 0 0 1px var(--_k-border)}
16559
- .knobs-wt-actions{display:flex;flex-wrap:wrap;gap:4px}
16560
- .knobs-ui .knobs-wt-btn{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 9px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:pointer;white-space:nowrap;
16561
- transition:background .15s,color .15s}
16562
- .knobs-ui .knobs-wt-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
16563
- .knobs-ui .knobs-wt-btn[data-primary]{background:var(--_k-fill-selected);color:var(--_k-fg)}
16564
- .knobs-ui .knobs-wt-btn[data-primary]:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_k-fg) 16%,transparent)}
16565
- .knobs-wt-keys{color:var(--_k-fg-3);font-size:calc(var(--kw-text) - 3px);line-height:1.6}
16566
- .knobs-wt-keys kbd{font:inherit;color:var(--_k-fg-2)}
16567
- /* Narrow: the rail becomes a bar along the top, with the progress and the export only. */
16568
- @container knobs-wt (max-width:880px){
16569
- .knobs-wt-rail{z-index:4;flex-direction:row;align-items:center;gap:12px;max-height:none;margin:0 calc(var(--kw-pad) * -1) 20px;padding:8px var(--kw-pad);top:0;background:var(--_k-bg-page)}
16570
- .knobs-wt-progress{flex:1}.knobs-wt-toc,.knobs-wt-keys{display:none}
16571
- .knobs-wt-step,.knobs-wt-shead{scroll-margin-top:60px}
16572
- }
16573
-
16574
- /* ——— media slots ——— */
16575
- .knobs-ui.knobs-wt-slot{position:relative;min-width:0;container:knobs-wt-slot / inline-size;color:var(--_k-fg);font:var(--_k-weight) var(--k-walkthrough-text,14px)/1.5 var(--_k-font);
16576
- --kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info))}
16577
- .knobs-wt-slot-label{display:block;margin:0 0 6px 2px;font:var(--_k-weight-label) calc(var(--k-walkthrough-text,14px) - 3px)/1.3 var(--_k-font);color:var(--_k-fg-2);letter-spacing:.04em;text-transform:uppercase}
16578
- /* A compare is exactly as tall as its pictures at this width, so there's no letterbox: the two
16579
- sides edge to edge (side) or one (overlaid), plus its mode bar, the timeline and their two gaps
16580
- (92 px), and the frame strip with its gap (158 px). */
16581
- .knobs-wt-fit{width:100%;height:calc(100cqw / 2 / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
16582
- .knobs-wt-fit[data-overlay]{height:calc(100cqw / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
16583
- .knobs-wt-fit[data-strip]{--kw-chrome:158px}
16584
- .knobs-wt-fit[data-settled]{transition:height var(--k-wt-calm-ms,${calm.duration}ms) ${calm.easing}}
16585
- .knobs-wt-frame{position:relative;width:100%;overflow:hidden;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill);aspect-ratio:var(--kw-aspect,16/9)}
16586
- .knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
16587
- .knobs-wt-frame[data-natural]{aspect-ratio:auto}
16588
- .knobs-wt-frame[data-natural]>img{position:static;height:auto}
16589
- .knobs-wt-code{margin:0;padding:12px 14px;max-height:420px;overflow:auto;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill);color:var(--_k-fg);
16590
- font:calc(var(--k-walkthrough-text,14px) - 2px)/1.55 var(--_k-mono);white-space:pre;tab-size:2;scrollbar-width:thin}
16591
- .knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
16592
- .knobs-wt-prompt{color:var(--_k-fg-3);user-select:none;-webkit-user-select:none}
16593
- .knobs-wt-cmd{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
16594
- .knobs-wt-out{color:var(--_k-fg-2)}
16595
- .knobs-wt-metric{display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill)}
16596
- .knobs-wt-mlabel{color:var(--_k-fg-2);font-size:calc(var(--k-walkthrough-text,14px) - 2px)}
16597
- .knobs-wt-mvalue{display:flex;align-items:baseline;flex-wrap:wrap;gap:8px;font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}
16598
- .knobs-wt-mfrom{color:var(--_k-fg-3);font-size:calc(var(--k-walkthrough-text,14px) + 2px)}
16599
- .knobs-wt-mto{font-size:calc(var(--k-walkthrough-text,14px) + 12px);font-weight:var(--_k-weight-title);line-height:1}
16600
- .knobs-wt-mof{color:var(--_k-fg-3)}
16601
- .knobs-wt-mdelta{--kw-tone:var(--_k-fg-2);padding:1px 7px;border-radius:999px;font-size:calc(var(--k-walkthrough-text,14px) - 3px);font-weight:var(--_k-weight-label);color:var(--kw-tone);background:color-mix(in srgb,var(--kw-tone) 14%,transparent)}
16602
- .knobs-wt-mdelta[data-tone="good"]{--kw-tone:var(--kw-good)}
16603
- .knobs-wt-mdelta[data-tone="bad"]{--kw-tone:var(--_k-danger)}
16604
- .knobs-wt-mtrack{position:relative;height:4px;border-radius:2px;background:color-mix(in srgb,var(--_k-fg) 8%,transparent)}
16605
- .knobs-wt-mtrack>span{position:absolute;top:0;bottom:0;left:0;border-radius:inherit}
16606
- .knobs-wt-mtrack>.knobs-wt-mafter{background:var(--_k-accent)}
16607
- .knobs-wt-mtrack>.knobs-wt-mbefore{background:color-mix(in srgb,var(--_k-fg) 30%,transparent)}
16608
- .knobs-wt-callout{--kw-tone:var(--kw-info);display:flex;gap:10px;padding:10px 14px;border-radius:calc(var(--_k-radius-panel) - 4px);background:color-mix(in srgb,var(--kw-tone) 11%,transparent);color:var(--_k-fg)}
16609
- .knobs-wt-callout::before{content:"";flex:none;width:6px;height:6px;margin-top:.62em;border-radius:50%;background:var(--kw-tone)}
16610
- .knobs-wt-callout[data-tone="good"]{--kw-tone:var(--kw-good)}
16611
- .knobs-wt-callout[data-tone="warn"]{--kw-tone:var(--_k-warn)}
16612
- .knobs-wt-callout[data-tone="bad"]{--kw-tone:var(--_k-danger)}
16613
- .knobs-wt-text{max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
16614
- .knobs-wt-hold{border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill)}
16615
- .knobs-wt-failed{display:flex;align-items:center;justify-content:center;min-height:80px;color:var(--_k-fg-3);font-size:calc(var(--k-walkthrough-text,14px) - 2px)}
16616
-
16617
- /* ——— data blocks: history, stack, table, chapters. On the step's card, so no card of their own:
16618
- hairlines and fills only. Tones read the status tokens; hover fades are the panel's own. ——— */
16619
- :where(.knobs-wt-history,.knobs-wt-stack,.knobs-wt-table,.knobs-wt-chapters){--kw-t:var(--k-walkthrough-text,14px);--kw-line:calc(var(--kw-t) + 6px);--kw-rail:var(--_k-border-strong)}
16620
- :where(.knobs-wt-slot) [data-tone="good"]{--kw-tone:var(--kw-good)}
16621
- :where(.knobs-wt-slot) [data-tone="warn"]{--kw-tone:var(--_k-warn)}
16622
- :where(.knobs-wt-slot) [data-tone="bad"]{--kw-tone:var(--_k-danger)}
16623
- :where(.knobs-wt-slot) [data-tone="info"]{--kw-tone:var(--kw-info)}
16624
- :where(.knobs-wt-slot) [data-tone="dim"]{--kw-tone:var(--_k-fg-3)}
16625
- .knobs-wt-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
16626
- .knobs-ui .knobs-wt-tag{display:inline-flex;align-items:center;height:20px;padding:0 6px;border-radius:5px;background:var(--_k-fill);color:var(--_k-fg-2);font:var(--_k-weight) calc(var(--kw-t) - 3px)/1 var(--_k-mono);
16627
- text-decoration:none;white-space:nowrap;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
16628
- .knobs-ui a.knobs-wt-tag:is(:hover,:focus-visible){background:var(--_k-fill-selected);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
16629
-
16630
- /* A history: dates down the left, a spine of tone dots joined by a hairline (broken around each
16631
- dot), the event beside it. Inferred events take a ring for a dot and a dashed tag. */
16632
- .knobs-wt-history{--kw-hy:calc(5px + var(--kw-line) / 2);display:flex;flex-direction:column;gap:18px;font-size:calc(var(--kw-t) - 1px);line-height:var(--kw-line);color:var(--_k-fg)}
16633
- .knobs-wt-hgroup{display:flex;flex-direction:column;gap:6px;min-width:0}
16634
- .knobs-wt-hhead{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px}
16635
- .knobs-ui .knobs-wt-hheading{margin:0;font:var(--_k-weight-label) calc(var(--kw-t) - 3px)/1.3 var(--_k-font);letter-spacing:.04em;text-transform:uppercase;color:var(--_k-fg-2)}
16636
- .knobs-wt-hsum{color:var(--_k-fg-3);font-size:calc(var(--kw-t) - 2px);font-variant-numeric:tabular-nums}
16637
- .knobs-wt-hlist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
16638
- .knobs-wt-hev{display:grid;grid-template-columns:var(--kw-hw,11ch) 14px minmax(0,1fr);grid-template-areas:"when spine main";column-gap:10px;align-items:baseline;padding:5px 0}
16639
- .knobs-wt-hwhen{grid-area:when;display:flex;flex-direction:column;min-width:0;font:var(--_k-weight) calc(var(--kw-t) - 2px)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-2)}
16640
- .knobs-wt-hage{color:var(--_k-fg-3);font-size:calc(var(--kw-t) - 3px);line-height:1.35}
16641
- .knobs-wt-hspine{grid-area:spine;position:relative;align-self:stretch;margin-block:-5px}
16642
- .knobs-wt-hspine::before,.knobs-wt-hspine::after{content:"";position:absolute;left:6.5px;width:1px;background:var(--kw-rail)}
16643
- .knobs-wt-hspine::before{top:0;height:calc(var(--kw-hy) - 6px)}
16644
- .knobs-wt-hspine::after{top:calc(var(--kw-hy) + 6px);bottom:0}
16645
- .knobs-wt-hev:first-child .knobs-wt-hspine::before,.knobs-wt-hev:last-child .knobs-wt-hspine::after{content:none}
16646
- .knobs-wt-hdot{position:absolute;left:3.5px;top:calc(var(--kw-hy) - 3.5px);width:7px;height:7px;border-radius:50%;background:var(--kw-tone,var(--_k-fg-3))}
16647
- .knobs-wt-hev[data-type="release"] .knobs-wt-hdot{border-radius:1.5px;rotate:45deg;scale:.9}
16648
- .knobs-wt-hev[data-type="audit"] .knobs-wt-hdot{border-radius:1.5px}
16649
- .knobs-wt-hev[data-provenance="inferred"] .knobs-wt-hdot{background:transparent;box-shadow:inset 0 0 0 1.5px var(--kw-tone,var(--_k-fg-3))}
16650
- .knobs-wt-hmain{grid-area:main;display:flex;flex-direction:column;gap:2px;min-width:0}
16651
- .knobs-wt-htitle{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 8px;font-weight:var(--_k-weight-label);color:var(--_k-fg)}
16652
- .knobs-wt-prov{padding:0 6px;border-radius:5px;background:var(--_k-fill);color:var(--_k-fg-3);font:var(--_k-weight) calc(var(--kw-t) - 3.5px)/18px var(--_k-mono);white-space:nowrap}
16653
- .knobs-wt-hev[data-provenance="inferred"] .knobs-wt-prov{background:transparent;color:var(--_k-warn);outline:1px dashed color-mix(in oklch,var(--_k-warn) 50%,transparent);outline-offset:-1px}
16654
- .knobs-ui .knobs-wt-hdetail{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
16655
- @container knobs-wt-slot (max-width:520px){
16656
- .knobs-wt-hev{grid-template-columns:14px minmax(0,1fr);grid-template-areas:"spine when" "spine main"}
16657
- .knobs-wt-hwhen{flex-direction:row;flex-wrap:wrap;align-items:baseline;column-gap:8px}
16658
- }
16659
-
16660
- /* A stack: the trunk at the top, each PR hanging off the branch it merges into, rails drawn as a
16661
- git graph (solid, so where two meet they don't darken). A PR with a step goes there on a click. */
16662
- .knobs-wt-stack{--kw-spt:7px;--kw-spb:7px;--kw-sy:calc(var(--kw-spt) + var(--kw-line) / 2);display:flex;flex-direction:column;font-size:calc(var(--kw-t) - 1px);line-height:var(--kw-line);color:var(--_k-fg)}
16663
- .knobs-wt-srow{display:grid;grid-template-columns:auto minmax(0,1fr) auto;column-gap:10px;align-items:start;margin:0 -8px;padding:var(--kw-spt) 8px var(--kw-spb);
16664
- border-radius:max(0px,calc(var(--_k-radius-panel) - 8px));transition:background calc(var(--_k-hover-out) * 1ms)}
16665
- .knobs-wt-srow[data-trunk]{--kw-spb:0px;grid-template-columns:auto minmax(0,1fr)}
16666
- .knobs-wt-srow[data-go]{cursor:pointer}
16667
- .knobs-wt-srow[data-go]:hover{background:var(--_ku-wash);transition-duration:calc(var(--_k-hover-in) * 1ms)}
16668
- .knobs-wt-lanes{display:flex;align-self:stretch;margin-block:calc(var(--kw-spt) * -1) calc(var(--kw-spb) * -1)}
16669
- .knobs-wt-lane{position:relative;flex:none;width:16px}
16670
- .knobs-wt-lane:is([data-rail="pass"],[data-rail="tee"],[data-down])::before{content:"";position:absolute;left:7.5px;top:0;bottom:0;width:1px;background:var(--kw-rail)}
16671
- .knobs-wt-lane[data-down]::before{top:calc(var(--kw-sy) + 3.5px)}
16672
- .knobs-wt-lane:is([data-rail="tee"],[data-rail="end"])::after{content:"";position:absolute;left:7.5px;top:0;width:13px;height:calc(var(--kw-sy) + .5px);border:solid var(--kw-rail);border-width:0 0 1px 1px;border-bottom-left-radius:6px}
16673
- .knobs-wt-lane[data-rail="node"]::after{content:"";position:absolute;left:4.5px;top:calc(var(--kw-sy) - 3.5px);width:7px;height:7px;border-radius:50%;background:var(--_k-fg-3)}
16674
- .knobs-wt-srow[data-trunk] .knobs-wt-lane[data-rail="node"]::after{background:var(--_k-fg-2)}
16675
- .knobs-wt-trunk{font:var(--_k-weight-label) calc(var(--kw-t) - 2px)/var(--kw-line) var(--_k-mono);color:var(--_k-fg-2)}
16676
- .knobs-wt-smain{display:flex;flex-direction:column;gap:1px;min-width:0}
16677
- .knobs-wt-sline{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 8px}
16678
- .knobs-wt-sord{font:var(--_k-weight) calc(var(--kw-t) - 2px)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-3);white-space:nowrap}
16679
- .knobs-ui .knobs-wt-spr{font:var(--_k-weight-label) calc(var(--kw-t) - 1.5px)/var(--kw-line) var(--_k-mono);color:var(--_k-fg-2);text-decoration:none;white-space:nowrap}
16680
- .knobs-ui a.knobs-wt-spr:is(:hover,:focus-visible){color:var(--_k-fg);text-decoration:underline}
16681
- .knobs-ui .knobs-wt-stitle{font-weight:var(--_k-weight-label);line-height:var(--kw-line);color:var(--_k-fg);text-align:left}
16682
- .knobs-ui button.knobs-wt-stitle{cursor:pointer;text-decoration:underline;text-decoration-color:transparent;text-underline-offset:3px;transition:text-decoration-color calc(var(--_k-hover-out) * 1ms)}
16683
- .knobs-ui :is(.knobs-wt-srow[data-go]:hover button.knobs-wt-stitle,button.knobs-wt-stitle:focus-visible){text-decoration-color:color-mix(in oklch,currentColor 45%,transparent);transition-duration:calc(var(--_k-hover-in) * 1ms)}
16684
- .knobs-ui .knobs-wt-ssummary{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
16685
- .knobs-wt-smeta{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 12px;font:var(--_k-weight) calc(var(--kw-t) - 2px)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-3)}
16686
- .knobs-wt-delta{display:inline-flex;gap:6px}
16687
- .knobs-wt-add{color:var(--_k-diff-add)}
16688
- .knobs-wt-del{color:var(--_k-diff-del)}
16689
- .knobs-wt-sstatus{margin-top:calc((var(--kw-line) - 20px) / 2)}
16690
- .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr) auto}
16691
- .knobs-wt-stack[data-flat] .knobs-wt-srow+.knobs-wt-srow{box-shadow:inset 0 1px 0 var(--_k-border)}
16692
- @container knobs-wt-slot (max-width:520px){
16693
- .knobs-wt-srow:not([data-trunk]){grid-template-columns:auto minmax(0,1fr)}
16694
- .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr)}
16695
- .knobs-wt-sstatus{grid-column:-2;justify-self:start;margin-top:4px}
16696
- }
16697
-
16698
- /* A table: a real one, its head stuck while it scrolls, both ways; edges fade where there's more
16699
- (the top one under the head), and thin bars show on hover. */
16700
- @property --kw-fade-l{syntax:"<number>";inherits:false;initial-value:1}
16701
- @property --kw-fade-r{syntax:"<number>";inherits:false;initial-value:1}
16702
- @keyframes kw-fade-l{from{--kw-fade-l:1}to{--kw-fade-l:0}}
16703
- @keyframes kw-fade-r{from{--kw-fade-r:0}to{--kw-fade-r:1}}
16704
- .knobs-wt-table{max-height:520px;overflow:auto;overscroll-behavior-x:contain;outline:none;scrollbar-width:thin;scrollbar-color:transparent transparent;
16705
- transition:scrollbar-color calc(var(--_k-hover-out) * 1ms)}
16706
- .knobs-wt-table:is(:hover,:focus-visible){scrollbar-color:color-mix(in oklch,var(--_k-fg) 28%,transparent) transparent;transition-duration:calc(var(--_k-hover-in) * 1ms)}
16707
- .knobs-wt-table:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus);border-radius:calc(var(--_k-radius-panel) - 4px)}
16708
- @supports (animation-timeline:scroll()){
16709
- .knobs-wt-table{animation:knobs-fade-a linear both,knobs-fade-b linear both,kw-fade-l linear both,kw-fade-r linear both;animation-duration:auto;
16710
- animation-timeline:scroll(self block),scroll(self block),scroll(self inline),scroll(self inline);animation-range:0 24px,calc(100% - 24px) 100%,0 8px,calc(100% - 8px) 100%;
16711
- --kw-table-mask-y:linear-gradient(to bottom,#000 var(--kw-thead,0px),rgb(0 0 0/var(--knobs-fade-a)) var(--kw-thead,0px),#000 calc(var(--kw-thead,0px) + 20px),#000 calc(100% - 20px),rgb(0 0 0/var(--knobs-fade-b)));
16712
- --kw-table-mask-x:linear-gradient(to right,rgb(0 0 0/var(--kw-fade-l)),#000 24px,#000 calc(100% - 24px),rgb(0 0 0/var(--kw-fade-r)));
16713
- -webkit-mask-image:var(--kw-table-mask-y),var(--kw-table-mask-x);mask-image:var(--kw-table-mask-y),var(--kw-table-mask-x);-webkit-mask-composite:source-in;mask-composite:intersect}
16714
- }
16715
- .knobs-wt-table table{width:100%;margin:0;border-collapse:separate;border-spacing:0;font-size:calc(var(--kw-t) - 1.5px);line-height:1.45;font-variant-numeric:tabular-nums;color:var(--_k-fg-2)}
16716
- .knobs-wt-table caption{caption-side:bottom;padding-top:8px;text-align:left;color:var(--_k-fg-3);font-size:calc(var(--kw-t) - 2px)}
16717
- .knobs-wt-table th{position:sticky;top:0;padding:0 12px 7px;text-align:left;vertical-align:bottom;font-weight:var(--_k-weight-label);font-size:calc(var(--kw-t) - 2.5px);color:var(--_k-fg-3);white-space:nowrap;
16718
- background:var(--_k-pane-bg);box-shadow:inset 0 -1px 0 var(--_k-border)}
16719
- .knobs-wt-table td{padding:8px 12px;vertical-align:baseline;box-shadow:inset 0 -1px 0 var(--_k-border)}
16720
- .knobs-wt-table tr:last-child td{box-shadow:none}
16721
- .knobs-wt-table :is(th,td):first-child{padding-left:0}
16722
- .knobs-wt-table :is(th,td):last-child{padding-right:0}
16723
- .knobs-wt-table td:first-child{color:var(--_k-fg)}
16724
- .knobs-wt-table [data-align="right"]{text-align:right}
16725
- .knobs-wt-table [data-align="center"]{text-align:center}
16726
- .knobs-wt-table td[data-mono]{font-family:var(--_k-mono);font-size:calc(var(--kw-t) - 2.5px);color:var(--_k-fg);white-space:nowrap}
16727
- .knobs-wt-table td[data-long]{min-width:24ch}
16728
- .knobs-wt-table td>.knobs-wt-steplink{margin-left:4px}
16729
- .knobs-ui .knobs-wt-table a.knobs-wt-chip{text-decoration:none}
16730
- .knobs-wt-ctone{color:var(--kw-tone)}
16731
-
16732
- /* Chapters: timestamp pills under a video; one highlight springs to the one playing. */
16733
- .knobs-wt-chapters{position:relative;display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}
16734
- .knobs-wt-chapter-mark{position:absolute;left:0;top:0;width:0;border-radius:calc(var(--_k-radius) + 2px);background:var(--_k-fill-selected);opacity:0;pointer-events:none;will-change:transform;
16735
- transition:opacity calc(var(--_k-motion-snappy-ms) * 1ms)}
16736
- .knobs-wt-chapter-mark[data-on]{opacity:1}
16737
- .knobs-ui .knobs-wt-chapter{position:relative;display:inline-flex;align-items:baseline;gap:6px;height:26px;padding:0 10px 0 8px;border-radius:calc(var(--_k-radius) + 2px);color:var(--_k-fg-2);
16738
- font-size:calc(var(--kw-t) - 2px);line-height:26px;white-space:nowrap;cursor:pointer;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
16739
- .knobs-ui .knobs-wt-chapter:hover{background:var(--_ku-wash);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
16740
- .knobs-ui .knobs-wt-chapter[aria-current]{background:transparent;color:var(--_k-fg)}
16741
- .knobs-ui .knobs-wt-chapter:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus)}
16742
- .knobs-wt-chapter-time{font:var(--_k-weight) calc(var(--kw-t) - 3px)/1 var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-3)}
16743
- .knobs-wt-chapter[aria-current] .knobs-wt-chapter-time{color:var(--_k-fg-2)}
16744
- @media(prefers-reduced-motion:reduce){:is(.knobs-walkthrough,.knobs-wt-slot) :where(*,::before,::after){transition-duration:.15s!important;transition-property:opacity,background,color!important}}
16745
- `;
16746
-
16747
- // src/ui/walkthrough-blocks.ts
16748
- init_motion();
16749
- init_look();
16971
+ init_icons();
16972
+ init_look();
16973
+ init_selection_pill();
16750
16974
 
16751
16975
  // src/ui/walkthrough-model.ts
16752
16976
  var DEFAULT_VERDICTS = ["ship", "change", "unsure"];
@@ -16953,7 +17177,15 @@ function fillTable(m) {
16953
17177
  out[k] = fillCell(v);
16954
17178
  return out;
16955
17179
  });
16956
- return { kind: "table", columns, rows, ...m.caption ? { caption: m.caption } : {} };
17180
+ const primary = columns.find((c) => c.key === m.primary)?.key ?? columns[0]?.key ?? "";
17181
+ return {
17182
+ kind: "table",
17183
+ columns,
17184
+ rows,
17185
+ ...m.caption ? { caption: m.caption } : {},
17186
+ stack: m.stack ?? "auto",
17187
+ primary
17188
+ };
16957
17189
  }
16958
17190
  function fillChapters(chapters) {
16959
17191
  const id = uniqueIds();
@@ -17026,6 +17258,8 @@ function normalizeWalkthrough(doc) {
17026
17258
  ...step.caption ? { caption: step.caption } : {},
17027
17259
  ...step.question ? { question: step.question } : {},
17028
17260
  media: list.map((m) => fillMedia(m, doc.base)),
17261
+ columns: Math.max(1, Math.min(3, Math.round(step.columns ?? 1))),
17262
+ ...step.more?.trim() && list.length > 1 ? { more: step.more.trim() } : {},
17029
17263
  options,
17030
17264
  ask
17031
17265
  };
@@ -17037,6 +17271,7 @@ function normalizeWalkthrough(doc) {
17037
17271
  id,
17038
17272
  title: s.title,
17039
17273
  ...link ? { link } : {},
17274
+ ...typeof s.kicker === "string" && s.kicker.trim() ? { kicker: s.kicker.trim() } : {},
17040
17275
  ...s.status ? { status: s.status } : {},
17041
17276
  ...s.summary ? { summary: s.summary } : {},
17042
17277
  steps: own
@@ -17047,14 +17282,25 @@ function normalizeWalkthrough(doc) {
17047
17282
  value
17048
17283
  }));
17049
17284
  const stepRef = (ref) => findStep(steps, ref)?.key ?? null;
17285
+ const extra = (x) => ({
17286
+ ...x.status?.trim() ? { status: x.status.trim() } : {},
17287
+ trail: (x.trail ?? []).filter((t) => typeof t === "string" && t.trim())
17288
+ });
17050
17289
  const feed = sortFeed([
17051
- ...(doc.updates ?? []).map((u) => ({ kind: "update", at: parseAt(u.at), text: u.text, step: stepRef(u.step) })),
17290
+ ...(doc.updates ?? []).map((u) => ({
17291
+ kind: "update",
17292
+ at: parseAt(u.at),
17293
+ text: u.text,
17294
+ step: stepRef(u.step),
17295
+ ...extra(u)
17296
+ })),
17052
17297
  ...(doc.qa ?? []).map((x) => ({
17053
17298
  kind: "qa",
17054
17299
  at: parseAt(x.at),
17055
17300
  text: x.a,
17056
17301
  q: x.q,
17057
- step: stepRef(x.step)
17302
+ step: stepRef(x.step),
17303
+ ...extra(x)
17058
17304
  }))
17059
17305
  ]);
17060
17306
  return {
@@ -17067,10 +17313,32 @@ function normalizeWalkthrough(doc) {
17067
17313
  id: slug(v),
17068
17314
  label: label(v)
17069
17315
  })),
17070
- sections,
17316
+ sections: withKickers(sections, doc.sections, doc.kickers),
17071
17317
  steps
17072
17318
  };
17073
17319
  }
17320
+ var isPullRequest = (href) => /\/(?:pull|merge_requests)\/\d+(?:[/?#]|$)/.test(href);
17321
+ function withKickers(sections, source, kickers = "pr") {
17322
+ if (kickers === false)
17323
+ return [...sections];
17324
+ const noun = typeof kickers === "object" ? kickers.noun.trim() : "PR";
17325
+ const counted = typeof kickers === "object" ? sections : sections.filter((s) => s.link && isPullRequest(s.link.href));
17326
+ return sections.map((s, i) => {
17327
+ const own = source[i]?.kicker;
17328
+ if (own !== undefined || s.kicker)
17329
+ return s;
17330
+ const at = counted.indexOf(s);
17331
+ return at < 0 ? s : { ...s, kicker: `${noun} ${at + 1} of ${counted.length}`.trim() };
17332
+ });
17333
+ }
17334
+ function tableStacks(stack, room, natural) {
17335
+ if (stack !== "auto")
17336
+ return stack === "always";
17337
+ return room > 0 && natural > room + 1;
17338
+ }
17339
+ function noteBarReserve(bar, inset, gap) {
17340
+ return Math.max(0, Math.ceil(bar)) + inset + gap;
17341
+ }
17074
17342
  function parseAt(at) {
17075
17343
  if (!at)
17076
17344
  return null;
@@ -17355,6 +17623,9 @@ function feedbackMarkdown(fb, verdicts) {
17355
17623
  `;
17356
17624
  }
17357
17625
  var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
17626
+ function plainMarkdown(text) {
17627
+ return text.replace(/\[([^\]\n]+)\]\([^)\s]+\)/g, "$1").replace(/`([^`\n]+)`/g, "$1").replace(/\*\*([^*\n]+)\*\*/g, "$1").replace(/(^|[^*\w])\*([^*\n]+)\*(?!\w)/g, "$1$2").replace(/\s+/g, " ").trim();
17628
+ }
17358
17629
  function inlineMarkdown(text) {
17359
17630
  const codes = [];
17360
17631
  let s = text.replace(/`([^`\n]+)`/g, (_, c) => {
@@ -17375,6 +17646,10 @@ function inlineMarkdown(text) {
17375
17646
  }
17376
17647
 
17377
17648
  // src/ui/walkthrough-blocks.ts
17649
+ var GLYPH = {
17650
+ go: icon("caretRight", { small: true }),
17651
+ caret: icon("caretRight", { small: true })
17652
+ };
17378
17653
  var el = (doc, tag, cls) => {
17379
17654
  const n = doc.createElement(tag);
17380
17655
  if (cls)
@@ -17404,9 +17679,10 @@ function stepButton(doc, ref, steps) {
17404
17679
  const s = ref && steps?.find(ref);
17405
17680
  if (!s || !steps)
17406
17681
  return null;
17407
- const b = text(doc, "button", "knobs-wt-steplink", s.number);
17682
+ const b = el(doc, "button", "knobs-wt-steplink");
17683
+ b.innerHTML = GLYPH.go;
17408
17684
  b.type = "button";
17409
- b.title = `Go to ${s.number} ${s.title}`;
17685
+ b.title = `Go to “${s.title}”`;
17410
17686
  b.setAttribute("aria-label", b.title);
17411
17687
  b.addEventListener("click", (e) => {
17412
17688
  e.stopPropagation();
@@ -17531,7 +17807,7 @@ function mountStack(host, m, steps) {
17531
17807
  if (target && steps) {
17532
17808
  const title = text(doc, "button", "knobs-wt-stitle", it.title);
17533
17809
  title.type = "button";
17534
- title.title = `Go to ${target.number} ${target.title}`;
17810
+ title.title = `Go to “${target.title}”`;
17535
17811
  title.addEventListener("click", (e) => {
17536
17812
  e.stopPropagation();
17537
17813
  steps.go(target.key);
@@ -17586,175 +17862,775 @@ function delta(doc, d) {
17586
17862
  s.setAttribute("aria-label", `${d.add} added, ${d.del} removed`);
17587
17863
  return s;
17588
17864
  }
17589
- function mountTable(host, m, steps) {
17590
- const doc = host.ownerDocument;
17591
- const win = doc.defaultView;
17592
- const scroller = el(doc, "div", "knobs-wt-table");
17593
- const table = el(doc, "table");
17594
- if (m.caption)
17595
- table.append(text(doc, "caption", "", m.caption));
17596
- const head = el(doc, "thead");
17597
- const hr = el(doc, "tr");
17598
- for (const c of m.columns) {
17599
- const th = text(doc, "th", "", c.label);
17600
- th.scope = "col";
17601
- th.dataset.align = c.align;
17602
- hr.append(th);
17603
- }
17604
- head.append(hr);
17605
- const body = el(doc, "tbody");
17606
- for (const r of m.rows) {
17607
- const tr = el(doc, "tr");
17608
- for (const c of m.columns) {
17609
- const td = el(doc, "td");
17610
- td.dataset.align = c.align;
17611
- if (c.mono)
17612
- td.dataset.mono = "";
17613
- const cell = r[c.key];
17614
- if (cell) {
17615
- if (cell.text.length > 40 && !c.mono && !cell.code)
17616
- td.dataset.long = "";
17617
- let content;
17618
- if (cell.badge) {
17619
- content = link(doc, "knobs-wt-chip", cell.text, cell.href);
17620
- content.dataset.tone = cell.badge;
17621
- } else {
17622
- const inner = cell.code ? text(doc, "code", "", cell.text) : null;
17623
- if (cell.href) {
17624
- content = link(doc, "knobs-wt-clink", inner ? "" : cell.text, cell.href);
17625
- if (inner)
17626
- content.append(inner);
17627
- } else
17628
- content = inner ?? text(doc, "span", "", cell.text);
17629
- if (cell.tone) {
17630
- content.classList.add("knobs-wt-ctone");
17631
- content.dataset.tone = cell.tone;
17632
- }
17633
- }
17634
- td.append(content);
17635
- const go = stepButton(doc, cell.step, steps);
17636
- if (go)
17637
- td.append(go);
17638
- }
17639
- tr.append(td);
17640
- }
17641
- body.append(tr);
17642
- }
17643
- table.append(head, body);
17644
- scroller.append(table);
17645
- host.append(scroller);
17646
- const measure = () => {
17647
- scroller.style.setProperty("--kw-thead", `${head.offsetHeight}px`);
17648
- const scrolls = scroller.scrollWidth > scroller.clientWidth + 1 || scroller.scrollHeight > scroller.clientHeight + 1;
17649
- if (scrolls) {
17650
- scroller.tabIndex = 0;
17651
- scroller.setAttribute("role", "region");
17652
- scroller.setAttribute("aria-label", m.caption ?? "Table");
17653
- } else {
17654
- scroller.removeAttribute("tabindex");
17655
- scroller.removeAttribute("role");
17656
- scroller.removeAttribute("aria-label");
17657
- }
17658
- };
17659
- const ro = win && "ResizeObserver" in win ? new win.ResizeObserver(measure) : null;
17660
- ro?.observe(scroller);
17661
- measure();
17662
- return {
17663
- dispose: () => {
17664
- ro?.disconnect();
17665
- scroller.remove();
17666
- },
17667
- context: () => ({ kind: "table" })
17668
- };
17865
+ function cellContent(doc, into, cell, steps) {
17866
+ let content;
17867
+ if (cell.badge) {
17868
+ content = link(doc, "knobs-wt-chip", cell.text, cell.href);
17869
+ content.dataset.tone = cell.badge;
17870
+ } else {
17871
+ const inner = cell.code ? text(doc, "code", "", cell.text) : null;
17872
+ if (cell.href) {
17873
+ content = link(doc, "knobs-wt-clink", inner ? "" : cell.text, cell.href);
17874
+ if (inner)
17875
+ content.append(inner);
17876
+ } else
17877
+ content = inner ?? text(doc, "span", "", cell.text);
17878
+ if (cell.tone) {
17879
+ content.classList.add("knobs-wt-ctone");
17880
+ content.dataset.tone = cell.tone;
17881
+ }
17882
+ }
17883
+ into.append(content);
17884
+ const go = stepButton(doc, cell.step, steps);
17885
+ if (go)
17886
+ into.append(go);
17887
+ }
17888
+ var cellAttrs = (td, c) => {
17889
+ td.dataset.align = c.align;
17890
+ if (c.mono)
17891
+ td.dataset.mono = "";
17892
+ };
17893
+ function mountTable(host, m, steps) {
17894
+ const doc = host.ownerDocument;
17895
+ const win = doc.defaultView;
17896
+ const box = el(doc, "div", "knobs-wt-tablebox");
17897
+ const scroller = el(doc, "div", "knobs-wt-table");
17898
+ const table = el(doc, "table");
17899
+ if (m.caption)
17900
+ table.append(text(doc, "caption", "", m.caption));
17901
+ const head = el(doc, "thead");
17902
+ const hr = el(doc, "tr");
17903
+ for (const c of m.columns) {
17904
+ const th = text(doc, "th", "", c.label);
17905
+ th.scope = "col";
17906
+ th.dataset.align = c.align;
17907
+ hr.append(th);
17908
+ }
17909
+ head.append(hr);
17910
+ const body = el(doc, "tbody");
17911
+ for (const r of m.rows) {
17912
+ const tr = el(doc, "tr");
17913
+ for (const c of m.columns) {
17914
+ const td = el(doc, "td");
17915
+ cellAttrs(td, c);
17916
+ const cell = r[c.key];
17917
+ if (cell) {
17918
+ if (cell.text.length > 40 && !c.mono && !cell.code)
17919
+ td.dataset.long = "";
17920
+ cellContent(doc, td, cell, steps);
17921
+ }
17922
+ tr.append(td);
17923
+ }
17924
+ body.append(tr);
17925
+ }
17926
+ table.append(head, body);
17927
+ scroller.append(table);
17928
+ const stack = el(doc, "div", "knobs-wt-tstack");
17929
+ stack.setAttribute("role", "list");
17930
+ if (m.caption)
17931
+ stack.setAttribute("aria-label", m.caption);
17932
+ const primary = m.columns.find((c) => c.key === m.primary) ?? m.columns[0];
17933
+ for (const r of m.rows) {
17934
+ const item = el(doc, "div", "knobs-wt-trow");
17935
+ item.setAttribute("role", "listitem");
17936
+ const title = el(doc, "div", "knobs-wt-ttitle");
17937
+ if (primary) {
17938
+ cellAttrs(title, primary);
17939
+ const cell = r[primary.key];
17940
+ if (cell)
17941
+ cellContent(doc, title, cell, steps);
17942
+ }
17943
+ const line = el(doc, "div", "knobs-wt-tline");
17944
+ line.append(title);
17945
+ const badges = el(doc, "div", "knobs-wt-tbadges");
17946
+ for (const c of m.columns) {
17947
+ const cell = r[c.key];
17948
+ if (c !== primary && cell?.badge)
17949
+ cellContent(doc, badges, cell, steps);
17950
+ }
17951
+ if (badges.childNodes.length)
17952
+ line.append(badges);
17953
+ item.append(line);
17954
+ const terms = el(doc, "dl", "knobs-wt-tterms");
17955
+ for (const c of m.columns) {
17956
+ if (c === primary)
17957
+ continue;
17958
+ const cell = r[c.key];
17959
+ if (!cell || cell.text === "" || cell.badge)
17960
+ continue;
17961
+ const dd = el(doc, "dd");
17962
+ cellAttrs(dd, c);
17963
+ dd.removeAttribute("data-align");
17964
+ cellContent(doc, dd, cell, steps);
17965
+ terms.append(text(doc, "dt", "", c.label), dd);
17966
+ }
17967
+ if (terms.childNodes.length)
17968
+ item.append(terms);
17969
+ stack.append(item);
17970
+ }
17971
+ if (m.caption)
17972
+ stack.append(text(doc, "p", "knobs-wt-tcaption", m.caption));
17973
+ box.append(scroller, stack);
17974
+ host.append(box);
17975
+ let natural = 0;
17976
+ const measureNatural = () => {
17977
+ if (!box.isConnected)
17978
+ return 0;
17979
+ const was = box.dataset.stacked;
17980
+ delete box.dataset.stacked;
17981
+ box.dataset.measure = "";
17982
+ const w = table.getBoundingClientRect().width;
17983
+ delete box.dataset.measure;
17984
+ if (was !== undefined)
17985
+ box.dataset.stacked = was;
17986
+ return w;
17987
+ };
17988
+ const measure = () => {
17989
+ if (!natural)
17990
+ natural = measureNatural();
17991
+ const stacked = tableStacks(m.stack, box.clientWidth, natural);
17992
+ box.toggleAttribute("data-stacked", stacked);
17993
+ scroller.style.setProperty("--kw-thead", `${head.offsetHeight}px`);
17994
+ const scrolls = !stacked && (scroller.scrollWidth > scroller.clientWidth + 1 || scroller.scrollHeight > scroller.clientHeight + 1);
17995
+ if (scrolls) {
17996
+ scroller.tabIndex = 0;
17997
+ scroller.setAttribute("role", "region");
17998
+ scroller.setAttribute("aria-label", m.caption ?? "Table");
17999
+ } else {
18000
+ scroller.removeAttribute("tabindex");
18001
+ scroller.removeAttribute("role");
18002
+ scroller.removeAttribute("aria-label");
18003
+ }
18004
+ };
18005
+ if (m.stack === "always")
18006
+ box.dataset.stacked = "";
18007
+ const ro = win && "ResizeObserver" in win ? new win.ResizeObserver(measure) : null;
18008
+ ro?.observe(box);
18009
+ measure();
18010
+ doc.fonts?.ready.then(() => {
18011
+ if (!box.isConnected)
18012
+ return;
18013
+ natural = 0;
18014
+ measure();
18015
+ });
18016
+ return {
18017
+ dispose: () => {
18018
+ ro?.disconnect();
18019
+ box.remove();
18020
+ },
18021
+ context: () => ({ kind: "table" })
18022
+ };
18023
+ }
18024
+ function chapterPills(host, chapters, io) {
18025
+ const doc = host.ownerDocument;
18026
+ const list = el(doc, "div", "knobs-wt-chapters");
18027
+ list.setAttribute("role", "group");
18028
+ list.setAttribute("aria-label", "Chapters");
18029
+ let at = -2;
18030
+ const rows = chapters.map((c, i) => {
18031
+ const b = el(doc, "button", "knobs-wt-chapter");
18032
+ b.type = "button";
18033
+ b.append(text(doc, "span", "knobs-wt-chapter-n", String(i + 1)));
18034
+ b.append(text(doc, "span", "knobs-wt-chapter-label", c.label));
18035
+ b.append(text(doc, "span", "knobs-wt-chapter-time", clock(c.start)));
18036
+ b.addEventListener("click", () => {
18037
+ io.onEvent?.({ what: "seek", time: Math.round(c.start * 100) / 100, detail: c.label });
18038
+ io.seek(c.start);
18039
+ select(i);
18040
+ });
18041
+ list.append(b);
18042
+ return b;
18043
+ });
18044
+ host.append(list);
18045
+ const pill = selectionPill(list, {
18046
+ selected: () => rows[at] ?? null,
18047
+ rows: () => rows
18048
+ });
18049
+ const select = (i) => {
18050
+ if (i === at)
18051
+ return;
18052
+ at = i;
18053
+ rows.forEach((b, k) => k === at ? b.setAttribute("aria-current", "true") : b.removeAttribute("aria-current"));
18054
+ pill.update();
18055
+ };
18056
+ const sync = () => select(chapterAt(chapters, io.now()));
18057
+ sync();
18058
+ return {
18059
+ sync,
18060
+ get current() {
18061
+ return chapters[at] ?? null;
18062
+ },
18063
+ dispose() {
18064
+ pill.dispose();
18065
+ list.remove();
18066
+ }
18067
+ };
18068
+ }
18069
+ function disclosure(host, options) {
18070
+ const doc = host.ownerDocument;
18071
+ const win = doc.defaultView;
18072
+ let open = options.open === true;
18073
+ const root = el(doc, "div", "knobs-wt-disc");
18074
+ const toggle = el(doc, "button", "knobs-wt-disc-toggle");
18075
+ toggle.type = "button";
18076
+ toggle.innerHTML = GLYPH.caret;
18077
+ toggle.append(text(doc, "span", "", options.label));
18078
+ if (options.count !== undefined)
18079
+ toggle.append(text(doc, "span", "knobs-wt-disc-count", String(options.count)));
18080
+ const viewport = el(doc, "div", "knobs-wt-disc-body");
18081
+ const content = el(doc, "div", "knobs-wt-disc-content");
18082
+ content.append(options.content);
18083
+ viewport.append(content);
18084
+ root.append(toggle, viewport);
18085
+ host.append(root);
18086
+ const STATE = springMotion(SPRINGS.default);
18087
+ const h = new Spring(0, STATE.omega, STATE.zeta, 0.25);
18088
+ let full = 0;
18089
+ const paint = (moving) => {
18090
+ const k = full > 0 ? Math.max(0, Math.min(1, h.x / full)) : open ? 1 : 0;
18091
+ viewport.style.height = !moving && open ? "" : `${Math.max(0, h.x).toFixed(2)}px`;
18092
+ viewport.toggleAttribute("data-moving", moving);
18093
+ content.style.opacity = (open ? Math.min(1, k * 1.25) : Math.max(0, k * 1.5 - 0.5)).toFixed(3);
18094
+ };
18095
+ const loop = frameLoop(win, (dt) => {
18096
+ h.step(dt);
18097
+ const moving = !h.resting;
18098
+ paint(moving);
18099
+ return moving;
18100
+ });
18101
+ const sync = () => {
18102
+ root.toggleAttribute("data-open", open);
18103
+ toggle.setAttribute("aria-expanded", String(open));
18104
+ viewport.inert = !open;
18105
+ };
18106
+ sync();
18107
+ h.snap(0);
18108
+ paint(false);
18109
+ if (open) {
18110
+ viewport.style.height = "";
18111
+ content.style.opacity = "1";
18112
+ }
18113
+ const api = {
18114
+ element: root,
18115
+ get open() {
18116
+ return open;
18117
+ },
18118
+ toggle(next = !open) {
18119
+ if (next === open)
18120
+ return;
18121
+ open = next;
18122
+ sync();
18123
+ full = content.offsetHeight;
18124
+ if (reducedMotion(win)) {
18125
+ loop.stop();
18126
+ h.snap(open ? full : 0);
18127
+ viewport.style.height = open ? "" : "0px";
18128
+ content.style.opacity = open ? "1" : "0";
18129
+ return;
18130
+ }
18131
+ if (h.resting)
18132
+ h.snap(open ? 0 : full);
18133
+ h.set(open ? full : 0);
18134
+ paint(true);
18135
+ loop.wake();
18136
+ },
18137
+ dispose() {
18138
+ loop.stop();
18139
+ root.remove();
18140
+ }
18141
+ };
18142
+ toggle.addEventListener("click", () => api.toggle());
18143
+ return api;
18144
+ }
18145
+
18146
+ // src/ui/walkthrough-media.ts
18147
+ init_compare_model();
18148
+ init_glide();
18149
+ init_icons();
18150
+ init_look();
18151
+
18152
+ // src/ui/walkthrough-css.ts
18153
+ init_motion();
18154
+ var snappy3 = springCss(SPRINGS.snappy);
18155
+ var calm = springCss(SPRINGS.calm);
18156
+ var state = springCss(SPRINGS.default);
18157
+ var quick = springCss(FADE);
18158
+ var fade2 = "-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 1.5em),transparent)";
18159
+ var walkthroughCss = `
18160
+ :where(.knobs-walkthrough,.knobs-walkthrough *,.knobs-wt-slot,.knobs-wt-slot *){box-sizing:border-box}
18161
+ .knobs-ui.knobs-walkthrough{--kw-snappy:${snappy3.easing};--kw-snappy-ms:${snappy3.duration}ms;--kw-calm:${calm.easing};--kw-calm-ms:${calm.duration}ms;--kw-state:${state.easing};--kw-state-ms:${state.duration}ms;--kw-quick:${quick.easing};--kw-quick-ms:${quick.duration}ms;
18162
+ --kw-measure:var(--k-walkthrough-measure,68ch);--kw-width:var(--k-walkthrough-width,680px);--kw-rail-w:220px;--kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));
18163
+ --kw-card:var(--_ku-pane-bg);--kw-card-ring:var(--_ku-pane-ring);--kw-under:var(--_k-bg-page);--kw-ink:var(--k-walkthrough-ink,#ff375f);--kw-strong:var(--_k-weight-label);
18164
+ container:knobs-wt / inline-size;display:block;width:100%;color:var(--_k-fg);font:var(--_k-weight) var(--_k-type-body)/var(--_k-leading-body) var(--_k-font);font-variant-numeric:tabular-nums;text-align:left;
18165
+ letter-spacing:normal;-webkit-font-smoothing:antialiased}
18166
+ .knobs-walkthrough [hidden]{display:none!important}
18167
+ /* (0,0,1): a host's element rules stop here, and every class rule below wins over it. */
18168
+ :where(.knobs-walkthrough) :is(h1,h2,h3,p,ol,ul,dl,dd){margin:0;font:inherit;letter-spacing:inherit}
18169
+ .knobs-walkthrough a{color:inherit;text-decoration:underline;text-decoration-color:color-mix(in srgb,currentColor 35%,transparent);text-underline-offset:2px;transition:text-decoration-color .15s}
18170
+ .knobs-walkthrough a:hover{text-decoration-color:currentColor}
18171
+ .knobs-walkthrough code,.knobs-wt-slot code{font:var(--_k-type-label) var(--_k-mono);padding:1px 5px;border-radius:calc(var(--_k-radius) / 2);background:var(--_k-fill)}
18172
+ :where(.knobs-ui) :is(.knobs-wt-btn,.knobs-wt-pick,.knobs-wt-toc-row,button.knobs-wt-stitle,.knobs-wt-chapter,.knobs-wt-disc-toggle){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;
18173
+ background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
18174
+
18175
+ /* ——— layout: a readable column, centred, with a slim contents rail beside it (right by default);
18176
+ one column where it's narrow ——— */
18177
+ .knobs-wt-layout{display:grid;grid-template-columns:minmax(0,var(--kw-width)) var(--kw-rail-w);justify-content:center;gap:var(--_k-space-column);align-items:start}
18178
+ .knobs-walkthrough[data-rail="right"] .knobs-wt-main{grid-column:1;grid-row:1}
18179
+ .knobs-walkthrough[data-rail="right"] .knobs-wt-rail{grid-column:2;grid-row:1}
18180
+ .knobs-walkthrough[data-rail="left"] .knobs-wt-layout{grid-template-columns:var(--kw-rail-w) minmax(0,var(--kw-width))}
18181
+ .knobs-walkthrough[data-rail="none"] .knobs-wt-layout{grid-template-columns:minmax(0,var(--kw-width))}
18182
+ .knobs-wt-main{display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0;padding-bottom:12px}
18183
+ /* The intro, the updates and each section: a section's room apart, the hairline in the middle. */
18184
+ .knobs-wt-flow{display:flex;flex-direction:column;gap:calc(var(--_k-space-section) / 2);min-width:0}
18185
+ .knobs-wt-flow>*+*{padding-top:calc(var(--_k-space-section) / 2);box-shadow:inset 0 1px 0 var(--_k-border-subtle)}
18186
+ @container knobs-wt (max-width:940px){.knobs-walkthrough .knobs-wt-layout{grid-template-columns:minmax(0,1fr);gap:var(--_k-space-title)}
18187
+ .knobs-walkthrough[data-rail] .knobs-wt-layout>*{grid-column:auto;grid-row:auto}}
18188
+
18189
+ /* ——— one line of facts: small, faint, joined by ·, its trailing edge fading ——— */
18190
+ .knobs-wt-metaline{display:flex;align-items:baseline;gap:var(--_k-space-inside);min-width:0;overflow:hidden;white-space:nowrap;font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3);${fade2}}
18191
+ .knobs-wt-metaline>*{flex:none}
18192
+ .knobs-wt-metaline>*+*::before{content:"·";margin-right:var(--_k-space-inside);color:var(--_k-fg-3)}
18193
+ .knobs-walkthrough .knobs-wt-metaline a{display:inline-flex;align-items:baseline;color:inherit;text-decoration:none;transition:color calc(var(--_k-hover-out) * 1ms)}
18194
+ .knobs-walkthrough .knobs-wt-metaline a:hover{color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18195
+ .knobs-wt-metaline svg{align-self:center;width:12px;height:12px;margin-left:2px}
18196
+ .knobs-wt-kicker-lead{color:var(--_k-fg);font-weight:var(--kw-strong)}
18197
+ .knobs-wt-metaline [data-status="on"]{color:var(--_k-fg)}
18198
+ .knobs-wt-metaline [data-status="warn"]{color:var(--_k-warn)}
18199
+
18200
+ /* ——— the page's head ——— */
18201
+ .knobs-wt-head{display:flex;flex-direction:column;gap:var(--_k-space-title)}
18202
+ .knobs-wt-title{font-size:var(--_k-type-title)!important;line-height:var(--_k-leading-title);font-weight:var(--kw-strong)!important;text-wrap:balance}
18203
+ .knobs-wt-summary{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
18204
+ .knobs-wt-meta{display:flex;flex-wrap:wrap;gap:var(--_k-space-inside) var(--_k-space-title);color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18205
+ .knobs-wt-meta>span{display:inline-flex;gap:var(--_k-space-inside);white-space:nowrap}
18206
+ .knobs-wt-meta b{font-weight:var(--_k-weight);color:var(--_k-fg-2);font-family:var(--_k-mono)}
18207
+
18208
+ /* ——— a section: a PR or topic. Its kicker leads the meta line; its title outranks the page's ——— */
18209
+ .knobs-wt-section{display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0}
18210
+ .knobs-wt-sintro{display:flex;flex-direction:column;gap:var(--_k-space-title);min-width:0}
18211
+ .knobs-wt-shead{display:flex;flex-direction:column;gap:var(--_k-space-inside);min-width:0;scroll-margin-top:16px}
18212
+ .knobs-wt-heading{font-size:var(--_k-type-heading)!important;line-height:var(--_k-leading-heading);font-weight:var(--kw-strong)!important;letter-spacing:-.01em!important;text-wrap:balance}
18213
+ .knobs-wt-ssum{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
18214
+ .knobs-wt-chip{display:inline-flex;align-items:center;gap:6px;height:20px;padding:0 8px;border-radius:var(--_k-round);font-size:var(--_k-type-small);font-weight:var(--kw-strong);white-space:nowrap;
18215
+ --kw-tone:var(--_k-fg-2);color:var(--kw-tone);background:color-mix(in srgb,var(--kw-tone) 14%,transparent)}
18216
+ .knobs-wt-chip::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
18217
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="good"]{--kw-tone:var(--kw-good)}
18218
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="warn"]{--kw-tone:var(--_k-warn)}
18219
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="bad"]{--kw-tone:var(--_k-danger)}
18220
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="info"]{--kw-tone:var(--kw-info)}
18221
+ :is(.knobs-wt-chip,.knobs-wt-toc-glyph)[data-tone="dim"]{--kw-tone:var(--_k-fg-2)}
18222
+
18223
+ /* ——— a step: on the page by default, a group's room between steps with a hairline in the middle;
18224
+ or (cards) one card each, the one surface its pieces sit on ——— */
18225
+ .knobs-walkthrough:not([data-cards]) .knobs-wt-step:not([data-full]){padding:0;border-radius:0;background:transparent;box-shadow:none}
18226
+ .knobs-walkthrough:not([data-cards]) .knobs-wt-step+.knobs-wt-step::before{content:"";position:absolute;left:0;right:0;top:calc(var(--_k-space-group) / -2);height:1px;background:var(--_k-border-subtle);pointer-events:none}
18227
+ .knobs-wt-step{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-title);padding:var(--_k-pad-card);border-radius:var(--_k-radius-panel);background:var(--kw-card);box-shadow:var(--kw-card-ring);
18228
+ scroll-margin-top:16px;outline:none;container:knobs-wt-step / inline-size}
18229
+ .knobs-walkthrough[data-cards]{--kw-under:var(--kw-card)}
18230
+ .knobs-walkthrough[data-cards][data-frame="flat"] .knobs-wt-step{box-shadow:inset 0 0 0 1px var(--_k-border)}
18231
+ .knobs-wt-step-head{display:flex;flex-direction:column;gap:var(--_k-space-inside);min-width:0}
18232
+ .knobs-wt-stepTitle{min-width:0;font-weight:var(--kw-strong)!important;text-wrap:balance}
18233
+ .knobs-wt-caption{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
18234
+ .knobs-wt-question{font-weight:var(--kw-strong)!important;max-width:var(--kw-measure)}
18235
+ .knobs-wt-media{display:grid;grid-template-columns:repeat(var(--kw-n,1),minmax(0,1fr));gap:var(--_k-space-group) var(--_k-space-column);min-width:0}
18236
+ @container knobs-wt-step (max-width:640px){.knobs-wt-media{grid-template-columns:minmax(0,1fr)}}
18237
+
18238
+ /* ——— the feed: updates and answers, newest first. Tight inside an item (4 / 8 / 4), a group's
18239
+ room between items with the hairline in the middle; no boxes ——— */
18240
+ .knobs-wt-feed{display:flex;flex-direction:column;gap:var(--_k-space-title);min-width:0;scroll-margin-top:16px}
18241
+ .knobs-wt-feed-head{display:flex;align-items:baseline;gap:var(--_k-space-sibling)}
18242
+ .knobs-wt-feed-title{font-weight:var(--kw-strong)!important}
18243
+ .knobs-wt-feed-count{font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
18244
+ .knobs-wt-feed-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0}
18245
+ .knobs-wt-fi{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0}
18246
+ .knobs-wt-fi+.knobs-wt-fi::before{content:"";position:absolute;left:0;right:0;top:calc(var(--_k-space-group) / -2);height:1px;background:var(--_k-border-subtle)}
18247
+ .knobs-wt-fi-q,.knobs-wt-fi-a{display:flex;flex-direction:column;gap:var(--_k-space-inside);min-width:0}
18248
+ .knobs-wt-fi-ask{max-width:var(--kw-measure);color:var(--_k-fg);font-weight:var(--kw-strong);text-wrap:pretty}
18249
+ .knobs-wt-fi-text{max-width:var(--kw-measure);color:var(--_k-fg);text-wrap:pretty}
18250
+ .knobs-wt-fi-answer{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
18251
+ :where(.knobs-ui) .knobs-wt-fi-step{all:revert;box-sizing:border-box;font:inherit;color:inherit;background:transparent;border:0;margin:0;padding:0;cursor:pointer;outline:none;
18252
+ transition:color calc(var(--_k-hover-out) * 1ms)}
18253
+ .knobs-ui .knobs-wt-fi-step:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18254
+ :where(.knobs-ui) .knobs-wt-steplink{all:revert;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin:0 0 0 2px;padding:0;vertical-align:-5px;border:0;
18255
+ border-radius:calc(var(--_k-radius) / 2);background:transparent;color:var(--_k-fg-3);cursor:pointer;outline:none;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
18256
+ .knobs-ui .knobs-wt-steplink:is(:hover,:focus-visible){background:var(--_k-fill);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18257
+ .knobs-wt-steplink svg{width:12px;height:12px}
18258
+
18259
+ /* ——— a disclosure: a small faint toggle with a caret, no fill. The body's height springs (in
18260
+ script) and its content fades in place, top-anchored, under a soft bottom mask ——— */
18261
+ .knobs-wt-disc{display:flex;flex-direction:column;min-width:0}
18262
+ .knobs-ui .knobs-wt-disc-toggle{display:inline-flex;align-items:center;gap:var(--_k-space-inside);width:max-content;height:20px;color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);cursor:pointer;
18263
+ border-radius:calc(var(--_k-radius) / 2);transition:color calc(var(--_k-hover-out) * 1ms)}
18264
+ .knobs-ui .knobs-wt-disc-toggle:is(:hover,:focus-visible),.knobs-wt-disc[data-open]>.knobs-wt-disc-toggle{color:var(--_k-fg-2);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18265
+ .knobs-ui .knobs-wt-disc-toggle:focus-visible{box-shadow:0 0 0 2px var(--_k-focus)}
18266
+ .knobs-wt-disc-toggle svg{flex:none;width:12px;height:12px;transition:rotate var(--kw-state-ms) var(--kw-state)}
18267
+ .knobs-wt-disc[data-open]>.knobs-wt-disc-toggle svg{rotate:90deg}
18268
+ .knobs-wt-disc-count{color:var(--_k-fg-3)}
18269
+ .knobs-wt-disc-body{overflow:hidden}
18270
+ .knobs-wt-disc[data-open]>.knobs-wt-disc-body:not([data-moving]){overflow:visible}
18271
+ .knobs-wt-disc-body[data-moving]{-webkit-mask-image:linear-gradient(#000 calc(100% - 16px),transparent);mask-image:linear-gradient(#000 calc(100% - 16px),transparent)}
18272
+ .knobs-wt-disc-content{padding-top:var(--_k-space-inside)}
18273
+ /* A trail sits on the toggle's label edge: faint lines, the time trailing. */
18274
+ .knobs-wt-trail{display:flex;flex-direction:column;list-style:none;padding:0 0 0 calc(12px + var(--_k-space-inside));font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3);min-width:0}
18275
+ .knobs-wt-trail li{display:flex;align-items:baseline;gap:var(--_k-space-sibling);min-width:0;white-space:nowrap}
18276
+ .knobs-wt-trail-text{flex:0 1 auto;min-width:0;overflow:hidden;${fade2}}
18277
+ .knobs-wt-trail-at{flex:none}
18278
+
18279
+ /* ——— a step's sent notes ——— */
18280
+ .knobs-wt-notes{display:flex;flex-direction:column}
18281
+ .knobs-wt-notes:empty{display:none}
18282
+ .knobs-wt-n{display:flex;align-items:flex-start;gap:var(--_k-space-sibling);padding:6px 0;overflow:hidden;
18283
+ transition:height var(--kw-snappy-ms) var(--kw-snappy),opacity var(--kw-snappy-ms) var(--kw-snappy),filter var(--kw-snappy-ms) var(--kw-snappy)}
18284
+ .knobs-wt-n[data-entering]{opacity:0;filter:blur(3px)}
18285
+ .knobs-wt-n[data-leaving]{opacity:0;filter:blur(3px);padding-block:0}
18286
+ .knobs-wt-n+.knobs-wt-n{box-shadow:inset 0 1px 0 var(--_k-border-subtle)}
18287
+ .knobs-wt-n-meta{flex:none;min-width:44px;color:var(--_k-fg-3);font:var(--_k-weight) var(--_k-type-small)/var(--_k-leading-body) var(--_k-mono)}
18288
+ .knobs-wt-n-meta[data-seek]{cursor:pointer}
18289
+ .knobs-wt-n-meta[data-seek]:hover{color:var(--_k-fg)}
18290
+ .knobs-wt-n-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
18291
+ .knobs-wt-n-voice{height:30px;max-width:320px}
18292
+ .knobs-wt-n-ink{display:block;width:120px;max-height:80px;border-radius:calc(var(--_k-radius) / 2);background:var(--_k-fill)}
18293
+ .knobs-wt-n-ink path{fill:none;stroke:var(--kw-ink);stroke-width:2px;vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round}
18294
+
18295
+ /* ——— the note bar, docked at the bottom of the column. It holds its own place at the page's end
18296
+ (so the last step stays clear of it), and script keeps its height as the scroller's
18297
+ scroll-padding-bottom ——— */
18298
+ .knobs-wt-dock{position:sticky;bottom:12px;z-index:40;display:grid;pointer-events:none}
18299
+ .knobs-wt-dock>*{pointer-events:auto}
18300
+ .knobs-wt-dock>:is(.knobs-wt-notebar,.knobs-wt-launch){grid-area:1/1}
18301
+ .knobs-wt-dock[data-mode="docked"]>.knobs-wt-launch{display:none}
18302
+ /* Collapsed: the launcher and the bar swap in place, a quick fade with a little scale and blur
18303
+ from the launcher's corner; the exit is the shorter. */
18304
+ :where(.knobs-ui) .knobs-wt-launch{all:revert;box-sizing:border-box;justify-self:end;align-self:end;display:inline-flex;align-items:center;gap:6px;height:32px;margin:0;padding:0 10px 0 9px;border:0;border-radius:var(--_k-round);
18305
+ background:var(--_k-bg);color:var(--_k-fg-2);box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay);font:var(--kw-strong) var(--_k-type-small)/1 var(--_k-font);cursor:pointer;outline:none;transform-origin:100% 100%;
18306
+ transition:color calc(var(--_k-hover-out) * 1ms),opacity var(--kw-quick-ms) var(--kw-quick),scale var(--kw-quick-ms) var(--kw-quick),filter var(--kw-quick-ms) var(--kw-quick)}
18307
+ .knobs-ui .knobs-wt-launch:is(:hover,:focus-visible){color:var(--_k-fg)}
18308
+ .knobs-ui .knobs-wt-launch:focus-visible{box-shadow:var(--_k-focus-ring)}
18309
+ .knobs-wt-launch svg{width:16px;height:16px}
18310
+ .knobs-wt-launch kbd{font:inherit;font-weight:var(--_k-weight);color:var(--_k-fg-3)}
18311
+ .knobs-wt-dock[data-mode="collapsed"] .knobs-wt-notebar{transform-origin:100% 100%;transition:opacity var(--kw-quick-ms) var(--kw-quick),scale var(--kw-quick-ms) var(--kw-quick),filter var(--kw-quick-ms) var(--kw-quick)}
18312
+ .knobs-wt-dock[data-mode="collapsed"]:not([data-open])>.knobs-wt-notebar,.knobs-wt-dock[data-mode="collapsed"][data-open]>.knobs-wt-launch{opacity:0;scale:.98;filter:blur(3px);pointer-events:none;
18313
+ transition-duration:calc(var(--kw-quick-ms) * .8)}
18314
+ .knobs-wt-notebar{display:flex;flex-direction:column;gap:2px;padding:6px 6px 6px 14px;border-radius:calc(var(--_k-radius-panel) + 2px);background:var(--_k-bg);
18315
+ box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay)}
18316
+ .knobs-wt-on{padding-top:3px;color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);white-space:nowrap;overflow:hidden;${fade2}}
18317
+ .knobs-wt-row{display:flex;align-items:flex-end;gap:4px}
18318
+ :where(.knobs-ui) :is(.knobs-wt-input,.knobs-wt-tool,.knobs-wt-x,.knobs-wt-back){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;
18319
+ border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
18320
+ .knobs-ui .knobs-wt-input{flex:1;min-width:0;height:32px;max-height:140px;padding:6px 0;resize:none;color:var(--_k-fg);caret-color:var(--_k-accent);line-height:var(--_k-leading-body);font-size:var(--_k-type-body)}
18321
+ .knobs-ui .knobs-wt-input::placeholder{color:var(--_k-fg-3)}
18322
+ .knobs-wt-attach{display:flex;gap:4px;align-items:center;align-self:center}
18323
+ .knobs-wt-attach:empty{display:none}
18324
+ .knobs-wt-chip-a{display:inline-flex;align-items:center;gap:2px;height:24px;padding:0 2px 0 9px;border-radius:var(--_k-round);background:var(--_k-fill-selected);font-size:var(--_k-type-small);white-space:nowrap}
18325
+ .knobs-ui .knobs-wt-x{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;color:var(--_k-fg-3);cursor:pointer;transition:background .15s,color .15s}
18326
+ .knobs-ui .knobs-wt-x:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
18327
+ .knobs-wt-x svg{width:12px;height:12px}
18328
+ .knobs-ui .knobs-wt-tool{flex:none;display:inline-flex;align-items:center;justify-content:center;gap:5px;min-width:32px;height:32px;padding:0 8px;border-radius:calc(var(--_k-radius) + 2px);
18329
+ color:var(--_k-fg-2);cursor:pointer;transition:background .15s,color .15s}
18330
+ .knobs-ui .knobs-wt-tool:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
18331
+ .knobs-ui .knobs-wt-tool[aria-pressed="true"]{background:var(--_k-fill-selected);color:var(--_k-fg)}
18332
+ .knobs-wt-tool svg{width:16px;height:16px}
18333
+ .knobs-wt-rec{font-size:var(--_k-type-small)}
18334
+ .knobs-wt-rec:empty{display:none}
18335
+ .knobs-ui .knobs-wt-tool[data-send]{color:var(--_k-fg-3)}
18336
+ .knobs-ui .knobs-wt-tool[data-send][data-ready]{background:var(--_k-accent);color:var(--_k-bg)}
18337
+ /* Back: in place over the bar for a few seconds after a note moves the page on. */
18338
+ .knobs-ui .knobs-wt-back{position:absolute;left:8px;bottom:calc(100% + 8px);display:inline-flex;align-items:center;gap:var(--_k-space-inside);max-width:min(360px,calc(100% - 16px));height:28px;padding:0 12px 0 9px;
18339
+ border-radius:var(--_k-round);background:var(--_k-bg);color:var(--_k-fg);box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay);font-size:var(--_k-type-small);font-weight:var(--kw-strong);cursor:pointer;
18340
+ opacity:0;scale:.96;filter:blur(3px);pointer-events:none;transition:opacity .11s cubic-bezier(.4,0,1,1),scale .11s cubic-bezier(.4,0,1,1),filter .11s cubic-bezier(.4,0,1,1)}
18341
+ .knobs-ui .knobs-wt-back[data-on]{opacity:1;scale:1;filter:none;pointer-events:auto;transition:opacity .15s cubic-bezier(.2,.8,.2,1),scale .15s cubic-bezier(.2,.8,.2,1),filter .15s cubic-bezier(.2,.8,.2,1)}
18342
+ .knobs-ui .knobs-wt-back:hover{background:var(--_k-fill-selected)}
18343
+ .knobs-wt-back svg{flex:none;width:12px;height:12px}
18344
+ .knobs-wt-back-label{min-width:0;overflow:hidden;white-space:nowrap;${fade2}}
18345
+
18346
+ /* ——— ink over a step's media ——— */
18347
+ .knobs-wt-media,.knobs-wt-options{position:relative}
18348
+ .knobs-wt-ink{position:absolute;inset:0;z-index:5;width:100%;height:100%;overflow:visible;pointer-events:none;touch-action:none}
18349
+ .knobs-wt-ink[data-on]{pointer-events:auto;cursor:crosshair}
18350
+ .knobs-wt-ink path{fill:none;stroke:var(--kw-ink);stroke-width:3px;vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round}
18351
+
18352
+ /* ——— full-window: the step over the page above the note bar, its media filling it; its place held
18353
+ in the page. The one raised surface ——— */
18354
+ .knobs-wt-scrim{position:fixed;inset:0;z-index:30;background:color-mix(in srgb,var(--_k-bg-page) 82%,transparent);opacity:0;pointer-events:none}
18355
+ .knobs-wt-scrim[data-on]{pointer-events:auto}
18356
+ .knobs-wt-hold-card{flex:none}
18357
+ .knobs-wt-step[data-full]{position:fixed;z-index:35;margin:0;overflow:hidden;overscroll-behavior:contain;padding:var(--_k-pad-card);border-radius:var(--_k-radius-panel);box-shadow:var(--_ku-pane-ring),var(--_k-shadow-overlay);background:var(--_k-bg)}
18358
+ .knobs-wt-step[data-full]::before{content:none}
18359
+ .knobs-wt-step[data-full]>:not(.knobs-wt-media){flex:none}
18360
+ .knobs-wt-step[data-full] .knobs-wt-notes{display:none}
18361
+ .knobs-wt-step[data-full]>.knobs-wt-media{flex:1;min-height:0;grid-auto-rows:minmax(0,1fr)}
18362
+ .knobs-wt-step[data-full] .knobs-wt-slot{display:flex;flex-direction:column;min-height:0}
18363
+ .knobs-wt-step[data-full] .knobs-wt-body{flex:1;min-height:0}
18364
+ .knobs-wt-step[data-full] .knobs-wt-fit{height:100%;transition:none}
18365
+ .knobs-wt-step[data-full] .knobs-wt-frame{height:100%;aspect-ratio:auto}
18366
+
18367
+ /* ——— a taste test: the options side by side, Kit's pick checked and the rest stepping back. On
18368
+ the step's card, so no fill of their own ——— */
18369
+ .knobs-wt-options{display:grid;grid-template-columns:repeat(var(--kw-n,2),minmax(0,1fr));gap:var(--_k-space-group) var(--_k-space-column)}
18370
+ @container knobs-wt-step (max-width:640px){.knobs-wt-options{grid-template-columns:minmax(0,1fr)}}
18371
+ .knobs-wt-option{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0}
18372
+ .knobs-wt-option>:not(.knobs-wt-pick){transition:opacity var(--kw-snappy-ms) var(--kw-snappy)}
18373
+ .knobs-wt-options[data-has-pick] .knobs-wt-option:not([data-picked])>:not(.knobs-wt-pick){opacity:.55}
18374
+ .knobs-ui .knobs-wt-pick{display:flex;align-items:center;gap:8px;width:100%;min-height:28px;margin:0 -6px;width:calc(100% + 12px);padding:0 6px;border-radius:var(--_k-radius);cursor:pointer;color:var(--_k-fg-2);text-align:left;
18375
+ transition:background calc(var(--_k-hover-out) * 1ms)}
18376
+ .knobs-ui .knobs-wt-pick:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_k-fill);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18377
+ .knobs-wt-letter{flex:none;font-weight:var(--kw-strong);color:var(--_k-fg)}
18378
+ .knobs-wt-olabel{flex:1;min-width:0;font-weight:var(--kw-strong);color:var(--_k-fg)}
18379
+ .knobs-wt-suggested{flex:none;font-size:var(--_k-type-small);color:var(--_k-fg-3)}
18380
+ .knobs-wt-check{flex:none;display:flex;width:18px;height:18px;align-items:center;justify-content:center;border-radius:50%;background:var(--_k-fill);color:transparent;
18381
+ transition:background var(--kw-snappy-ms) var(--kw-snappy),color var(--kw-snappy-ms) var(--kw-snappy)}
18382
+ .knobs-wt-check svg{width:12px;height:12px}
18383
+ .knobs-wt-option[data-picked] .knobs-wt-check{background:var(--_k-accent);color:var(--_k-bg)}
18384
+ .knobs-wt-key{display:inline-flex;align-items:center;justify-content:center;min-width:15px;height:15px;padding:0 3px;border-radius:4px;background:var(--_k-fill);color:var(--_k-fg-3);
18385
+ font:var(--kw-strong) var(--_k-type-small)/1 var(--_k-font);opacity:.5;transition:opacity .15s}
18386
+ .knobs-wt-step[data-current] .knobs-wt-key{opacity:1}
18387
+ .knobs-wt-ocap{color:var(--_k-fg-2);max-width:var(--kw-measure)}
18388
+
18389
+ /* ——— the rail: progress, contents, export ——— */
18390
+ .knobs-wt-rail{position:sticky;top:16px;display:flex;flex-direction:column;gap:var(--_k-space-title);max-height:calc(100vh - 32px);min-height:0;font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18391
+ .knobs-wt-progress{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
18392
+ .knobs-wt-count{display:flex;align-items:baseline;justify-content:space-between;color:var(--_k-fg-2)}
18393
+ .knobs-wt-count b{color:var(--_k-fg);font-weight:var(--kw-strong)}
18394
+ .knobs-wt-bar{position:relative;height:4px;border-radius:2px;overflow:hidden;background:var(--_k-fill)}
18395
+ .knobs-wt-fill{position:absolute;inset:0;border-radius:inherit;background:var(--_k-accent);transform-origin:0 50%;transform:scaleX(var(--kw-done,0))}
18396
+ .knobs-walkthrough[data-settled] .knobs-wt-fill{transition:transform var(--kw-calm-ms) var(--kw-calm)}
18397
+ /* The contents: rows centred on one spine, title rows (32 high, the control radius) and the
18398
+ steps under them (20 high, half of it), bled out so their text sits on the rail's edge; the
18399
+ selection pill slides to the current step, and base rows never brighten for being current. */
18400
+ .knobs-wt-toc{position:relative;flex:1 1 auto;min-height:0;overflow:auto;scrollbar-width:none;display:flex;flex-direction:column;gap:var(--_k-space-sibling);margin:0 calc(var(--_k-space-sibling) * -1);padding:0 0 8px}
18401
+ .knobs-wt-toc::-webkit-scrollbar{display:none}
18402
+ .knobs-wt-toc-group{display:flex;flex-direction:column;min-width:0}
18403
+ .knobs-ui .knobs-wt-toc-row{position:relative;display:grid;grid-template-columns:var(--kw-toc-n,0px) minmax(0,1fr) auto;column-gap:var(--_k-space-sibling);align-items:center;flex:none;width:100%;height:32px;
18404
+ padding:0 var(--_k-space-sibling);border-radius:var(--_k-radius);color:var(--_k-fg-2);font-size:var(--_k-type-body);line-height:var(--_k-leading-body);font-weight:var(--kw-strong);text-align:left;white-space:nowrap;cursor:pointer;
18405
+ transition:color calc(var(--_k-hover-out) * 1ms)}
18406
+ .knobs-ui .knobs-wt-toc-sub{height:20px;border-radius:calc(var(--_k-radius) / 2);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight);color:var(--_k-fg-3)}
18407
+ .knobs-wt-toc[data-bare] .knobs-wt-toc-row{grid-template-columns:minmax(0,1fr) auto}
18408
+ .knobs-wt-toc[data-bare] .knobs-wt-toc-n{display:none}
18409
+ .knobs-ui .knobs-wt-toc-row:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18410
+ .knobs-ui .knobs-wt-toc-sub:is(:hover,:focus-visible){color:var(--_k-fg-2)}
18411
+ .knobs-ui .knobs-wt-toc-row:focus-visible{box-shadow:inset 0 0 0 1px var(--_k-focus)}
18412
+ .knobs-wt-toc-n,.knobs-wt-toc-end{display:flex;align-items:center;font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight);color:var(--_k-fg-3)}
18413
+ .knobs-wt-toc-row[data-active] .knobs-wt-toc-n{color:var(--_k-fg-2)}
18414
+ .knobs-wt-toc-label{min-width:0;overflow:hidden;${fade2}}
18415
+ .knobs-pick-copy .knobs-wt-toc-n,.knobs-pick-copy .knobs-wt-toc-end{color:var(--_k-fg-2)}
18416
+ /* A step's state: an open ring, a filled dot once answered, nothing to answer a faint ring. */
18417
+ .knobs-wt-toc-glyph{--kw-tone:var(--_k-fg-3);display:block;width:6px;height:6px;border-radius:50%;box-shadow:inset 0 0 0 1.5px var(--kw-tone);
18418
+ transition:background var(--kw-snappy-ms) var(--kw-snappy),box-shadow var(--kw-snappy-ms) var(--kw-snappy)}
18419
+ .knobs-wt-toc-glyph[data-tone="accent"]{--kw-tone:var(--_k-accent)}
18420
+ .knobs-wt-toc-glyph[data-done]{background:var(--kw-tone)}
18421
+ .knobs-wt-toc-glyph[data-tone="none"]{box-shadow:inset 0 0 0 1px var(--_k-border)}
18422
+ .knobs-wt-actions{display:flex;flex-wrap:wrap;gap:4px}
18423
+ .knobs-ui .knobs-wt-btn{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 9px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:pointer;white-space:nowrap;
18424
+ transition:background .15s,color .15s}
18425
+ .knobs-ui .knobs-wt-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
18426
+ .knobs-ui .knobs-wt-btn[data-primary]{background:var(--_k-fill-selected);color:var(--_k-fg)}
18427
+ .knobs-ui .knobs-wt-btn[data-primary]:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_k-fg) 16%,transparent)}
18428
+ .knobs-wt-keys{color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18429
+ .knobs-wt-keys kbd{font:inherit;color:var(--_k-fg-2)}
18430
+ /* Narrow: the rail becomes a bar along the top, with the progress and the export only. */
18431
+ @container knobs-wt (max-width:940px){
18432
+ .knobs-wt-rail{z-index:4;flex-direction:row;align-items:center;gap:var(--_k-space-title);max-height:none;margin:0 calc(var(--_k-pad-page) * -1);padding:8px var(--_k-pad-page);top:0;background:var(--_k-bg-page)}
18433
+ .knobs-wt-progress{flex:1}.knobs-wt-toc,.knobs-wt-keys{display:none}
18434
+ .knobs-wt-step,.knobs-wt-shead,.knobs-wt-feed{scroll-margin-top:60px}
18435
+ }
18436
+
18437
+ /* ——— media slots. Only a picture's frame (a quiet fill while it loads or letterboxes) and a code
18438
+ block (a well) are bounded; a metric, a callout, a table, a stack and a history sit on the page,
18439
+ set apart by space, hairlines and their own colours. On a card (.knobs-surface) code drops its
18440
+ well for hairlines, so nothing draws a fill and a ring of its own there ——— */
18441
+ .knobs-ui.knobs-wt-slot{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0;container:knobs-wt-slot / inline-size;color:var(--_k-fg);
18442
+ font:var(--_k-weight) var(--_k-type-body)/var(--_k-leading-body) var(--_k-font);font-variant-numeric:tabular-nums;--kw-measure:var(--k-walkthrough-measure,68ch);
18443
+ --kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info))}
18444
+ .knobs-wt-slot-label{display:block;font:var(--_k-weight-label) var(--_k-type-label)/var(--_k-leading-label) var(--_k-font);color:var(--_k-fg-2)}
18445
+ .knobs-wt-body{min-width:0}
18446
+ /* A compare is exactly as tall as its pictures at this width, so there's no letterbox: the two
18447
+ sides edge to edge (side) or one (overlaid), plus its mode bar, the timeline and their two gaps
18448
+ (92 px), and the frame strip with its gap (158 px). */
18449
+ .knobs-wt-fit{width:100%;height:calc(100cqw / 2 / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
18450
+ .knobs-wt-fit[data-overlay]{height:calc(100cqw / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
18451
+ .knobs-wt-fit[data-strip]{--kw-chrome:158px}
18452
+ .knobs-wt-fit[data-settled]{transition:height var(--k-wt-calm-ms,${calm.duration}ms) ${calm.easing}}
18453
+ .knobs-wt-frame{position:relative;width:100%;overflow:hidden;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill);aspect-ratio:var(--kw-aspect,16/9)}
18454
+ .knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
18455
+ .knobs-wt-frame[data-natural]{aspect-ratio:auto;background:transparent}
18456
+ .knobs-wt-frame[data-natural]>img{position:static;height:auto}
18457
+ .knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill);color:var(--_k-fg);
18458
+ font:var(--_k-type-label)/var(--_k-leading-body) var(--_k-mono);white-space:pre;tab-size:2;scrollbar-width:thin}
18459
+ .knobs-surface .knobs-wt-code{padding-inline:0;border-radius:0;background:transparent;box-shadow:inset 0 1px 0 var(--_k-border-subtle),inset 0 -1px 0 var(--_k-border-subtle)}
18460
+ .knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
18461
+ .knobs-wt-prompt{color:var(--_k-fg-3);user-select:none;-webkit-user-select:none}
18462
+ .knobs-wt-cmd{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
18463
+ .knobs-wt-out{color:var(--_k-fg-2)}
18464
+ .knobs-wt-metric{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
18465
+ .knobs-wt-mlabel{color:var(--_k-fg-2)}
18466
+ .knobs-wt-mvalue{display:flex;align-items:baseline;flex-wrap:wrap;gap:var(--_k-space-sibling);font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}
18467
+ .knobs-wt-mfrom{color:var(--_k-fg-3);font-size:var(--_k-type-title);line-height:var(--_k-leading-title)}
18468
+ .knobs-wt-mto{font-size:var(--_k-type-heading);line-height:var(--_k-leading-heading);font-weight:var(--_k-weight-label)}
18469
+ .knobs-wt-mof{color:var(--_k-fg-3)}
18470
+ .knobs-wt-mdelta{--kw-tone:var(--_k-fg-2);font-size:var(--_k-type-small);font-weight:var(--_k-weight-label);color:var(--kw-tone)}
18471
+ .knobs-wt-mdelta[data-tone="good"]{--kw-tone:var(--kw-good)}
18472
+ .knobs-wt-mdelta[data-tone="bad"]{--kw-tone:var(--_k-danger)}
18473
+ .knobs-wt-mtrack{position:relative;height:4px;border-radius:2px;background:color-mix(in srgb,var(--_k-fg) 8%,transparent)}
18474
+ .knobs-wt-mtrack>span{position:absolute;top:0;bottom:0;left:0;border-radius:inherit}
18475
+ .knobs-wt-mtrack>.knobs-wt-mafter{background:var(--_k-accent)}
18476
+ .knobs-wt-mtrack>.knobs-wt-mbefore{background:color-mix(in srgb,var(--_k-fg) 30%,transparent)}
18477
+ .knobs-wt-callout{--kw-tone:var(--kw-info);display:flex;gap:var(--_k-space-sibling);max-width:var(--kw-measure);color:var(--_k-fg)}
18478
+ .knobs-wt-callout::before{content:"";flex:none;width:6px;height:6px;margin-top:7px;border-radius:50%;background:var(--kw-tone)}
18479
+ .knobs-wt-callout[data-tone="good"]{--kw-tone:var(--kw-good)}
18480
+ .knobs-wt-callout[data-tone="warn"]{--kw-tone:var(--_k-warn)}
18481
+ .knobs-wt-callout[data-tone="bad"]{--kw-tone:var(--_k-danger)}
18482
+ .knobs-wt-text{max-width:var(--kw-measure);color:var(--_k-fg-2)}
18483
+ .knobs-wt-hold{border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill)}
18484
+ .knobs-wt-failed{display:flex;align-items:center;justify-content:center;min-height:80px;color:var(--_k-fg-3);font-size:var(--_k-type-small)}
18485
+
18486
+ /* ——— data blocks: history, stack, table, chapters. On the step's card, so no card of their own:
18487
+ hairlines and fills only. Tones read the status tokens; hover fades are the panel's own. ——— */
18488
+ :where(.knobs-wt-history,.knobs-wt-stack,.knobs-wt-tablebox,.knobs-wt-chapters){--kw-line:var(--_k-leading-body);--kw-rail:var(--_k-border-strong)}
18489
+ :where(.knobs-wt-slot) [data-tone="good"]{--kw-tone:var(--kw-good)}
18490
+ :where(.knobs-wt-slot) [data-tone="warn"]{--kw-tone:var(--_k-warn)}
18491
+ :where(.knobs-wt-slot) [data-tone="bad"]{--kw-tone:var(--_k-danger)}
18492
+ :where(.knobs-wt-slot) [data-tone="info"]{--kw-tone:var(--kw-info)}
18493
+ :where(.knobs-wt-slot) [data-tone="dim"]{--kw-tone:var(--_k-fg-3)}
18494
+ .knobs-wt-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
18495
+ .knobs-ui .knobs-wt-tag{display:inline-flex;align-items:center;height:20px;padding:0 6px;border-radius:5px;background:var(--_k-fill);color:var(--_k-fg-2);font:var(--_k-weight) var(--_k-type-small)/1 var(--_k-mono);
18496
+ text-decoration:none;white-space:nowrap;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
18497
+ .knobs-ui a.knobs-wt-tag:is(:hover,:focus-visible){background:var(--_k-fill-selected);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18498
+
18499
+ /* A history: dates down the left, a spine of tone dots joined by a hairline (broken around each
18500
+ dot), the event beside it. Inferred events take a ring for a dot and a dashed tag. */
18501
+ .knobs-wt-history{--kw-hy:calc(5px + var(--kw-line) / 2);display:flex;flex-direction:column;gap:18px;font-size:var(--_k-type-body);line-height:var(--kw-line);color:var(--_k-fg)}
18502
+ .knobs-wt-hgroup{display:flex;flex-direction:column;gap:6px;min-width:0}
18503
+ .knobs-wt-hhead{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px}
18504
+ .knobs-ui .knobs-wt-hheading{margin:0;font:var(--_k-weight-label) var(--_k-type-small)/var(--_k-leading-small) var(--_k-font);letter-spacing:.04em;text-transform:uppercase;color:var(--_k-fg-2)}
18505
+ .knobs-wt-hsum{color:var(--_k-fg-3);font-size:var(--_k-type-small);font-variant-numeric:tabular-nums}
18506
+ .knobs-wt-hlist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
18507
+ .knobs-wt-hev{display:grid;grid-template-columns:var(--kw-hw,11ch) 14px minmax(0,1fr);grid-template-areas:"when spine main";column-gap:10px;align-items:baseline;padding:5px 0}
18508
+ .knobs-wt-hwhen{grid-area:when;display:flex;flex-direction:column;min-width:0;font:var(--_k-weight) var(--_k-type-label)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-2)}
18509
+ .knobs-wt-hage{color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18510
+ .knobs-wt-hspine{grid-area:spine;position:relative;align-self:stretch;margin-block:-5px}
18511
+ .knobs-wt-hspine::before,.knobs-wt-hspine::after{content:"";position:absolute;left:6.5px;width:1px;background:var(--kw-rail)}
18512
+ .knobs-wt-hspine::before{top:0;height:calc(var(--kw-hy) - 6px)}
18513
+ .knobs-wt-hspine::after{top:calc(var(--kw-hy) + 6px);bottom:0}
18514
+ .knobs-wt-hev:first-child .knobs-wt-hspine::before,.knobs-wt-hev:last-child .knobs-wt-hspine::after{content:none}
18515
+ .knobs-wt-hdot{position:absolute;left:3.5px;top:calc(var(--kw-hy) - 3.5px);width:7px;height:7px;border-radius:50%;background:var(--kw-tone,var(--_k-fg-3))}
18516
+ .knobs-wt-hev[data-type="release"] .knobs-wt-hdot{border-radius:1.5px;rotate:45deg;scale:.9}
18517
+ .knobs-wt-hev[data-type="audit"] .knobs-wt-hdot{border-radius:1.5px}
18518
+ .knobs-wt-hev[data-provenance="inferred"] .knobs-wt-hdot{background:transparent;box-shadow:inset 0 0 0 1.5px var(--kw-tone,var(--_k-fg-3))}
18519
+ .knobs-wt-hmain{grid-area:main;display:flex;flex-direction:column;gap:2px;min-width:0}
18520
+ .knobs-wt-htitle{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 8px;font-weight:var(--_k-weight-label);color:var(--_k-fg)}
18521
+ .knobs-wt-prov{padding:0 6px;border-radius:5px;background:var(--_k-fill);color:var(--_k-fg-3);font:var(--_k-weight) var(--_k-type-small)/18px var(--_k-mono);white-space:nowrap}
18522
+ .knobs-wt-hev[data-provenance="inferred"] .knobs-wt-prov{background:transparent;color:var(--_k-warn);outline:1px dashed color-mix(in oklch,var(--_k-warn) 50%,transparent);outline-offset:-1px}
18523
+ .knobs-ui .knobs-wt-hdetail{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
18524
+ @container knobs-wt-slot (max-width:520px){
18525
+ .knobs-wt-hev{grid-template-columns:14px minmax(0,1fr);grid-template-areas:"spine when" "spine main"}
18526
+ .knobs-wt-hwhen{flex-direction:row;flex-wrap:wrap;align-items:baseline;column-gap:8px}
18527
+ }
18528
+
18529
+ /* A stack: the trunk at the top, each PR hanging off the branch it merges into, rails drawn as a
18530
+ git graph (solid, so where two meet they don't darken). A PR with a step goes there on a click. */
18531
+ .knobs-wt-stack{--kw-spt:7px;--kw-spb:7px;--kw-sy:calc(var(--kw-spt) + var(--kw-line) / 2);display:flex;flex-direction:column;font-size:var(--_k-type-body);line-height:var(--kw-line);color:var(--_k-fg)}
18532
+ .knobs-wt-srow{display:grid;grid-template-columns:auto minmax(0,1fr) auto;column-gap:10px;align-items:start;margin:0 -8px;padding:var(--kw-spt) 8px var(--kw-spb);
18533
+ border-radius:max(0px,calc(var(--_k-radius-panel) - 8px));transition:background calc(var(--_k-hover-out) * 1ms)}
18534
+ .knobs-wt-srow[data-trunk]{--kw-spb:0px;grid-template-columns:auto minmax(0,1fr)}
18535
+ .knobs-wt-srow[data-go]{cursor:pointer}
18536
+ .knobs-wt-srow[data-go]:hover{background:var(--_ku-wash);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18537
+ .knobs-wt-lanes{display:flex;align-self:stretch;margin-block:calc(var(--kw-spt) * -1) calc(var(--kw-spb) * -1)}
18538
+ .knobs-wt-lane{position:relative;flex:none;width:16px}
18539
+ .knobs-wt-lane:is([data-rail="pass"],[data-rail="tee"],[data-down])::before{content:"";position:absolute;left:7.5px;top:0;bottom:0;width:1px;background:var(--kw-rail)}
18540
+ .knobs-wt-lane[data-down]::before{top:calc(var(--kw-sy) + 3.5px)}
18541
+ .knobs-wt-lane:is([data-rail="tee"],[data-rail="end"])::after{content:"";position:absolute;left:7.5px;top:0;width:13px;height:calc(var(--kw-sy) + .5px);border:solid var(--kw-rail);border-width:0 0 1px 1px;border-bottom-left-radius:6px}
18542
+ .knobs-wt-lane[data-rail="node"]::after{content:"";position:absolute;left:4.5px;top:calc(var(--kw-sy) - 3.5px);width:7px;height:7px;border-radius:50%;background:var(--_k-fg-3)}
18543
+ .knobs-wt-srow[data-trunk] .knobs-wt-lane[data-rail="node"]::after{background:var(--_k-fg-2)}
18544
+ .knobs-wt-trunk{font:var(--_k-weight-label) var(--_k-type-label)/var(--kw-line) var(--_k-mono);color:var(--_k-fg-2)}
18545
+ .knobs-wt-smain{display:flex;flex-direction:column;gap:1px;min-width:0}
18546
+ .knobs-wt-sline{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 8px}
18547
+ .knobs-wt-sord{font:var(--_k-weight) var(--_k-type-label)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-3);white-space:nowrap}
18548
+ .knobs-ui .knobs-wt-spr{font:var(--_k-weight-label) var(--_k-type-label)/var(--kw-line) var(--_k-mono);color:var(--_k-fg-2);text-decoration:none;white-space:nowrap}
18549
+ .knobs-ui a.knobs-wt-spr:is(:hover,:focus-visible){color:var(--_k-fg);text-decoration:underline}
18550
+ .knobs-ui .knobs-wt-stitle{font-weight:var(--_k-weight-label);line-height:var(--kw-line);color:var(--_k-fg);text-align:left}
18551
+ .knobs-ui button.knobs-wt-stitle{cursor:pointer;text-decoration:underline;text-decoration-color:transparent;text-underline-offset:3px;transition:text-decoration-color calc(var(--_k-hover-out) * 1ms)}
18552
+ .knobs-ui :is(.knobs-wt-srow[data-go]:hover button.knobs-wt-stitle,button.knobs-wt-stitle:focus-visible){text-decoration-color:color-mix(in oklch,currentColor 45%,transparent);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18553
+ .knobs-ui .knobs-wt-ssummary{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
18554
+ .knobs-wt-smeta{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 12px;font:var(--_k-weight) var(--_k-type-small)/var(--kw-line) var(--_k-mono);font-variant-numeric:tabular-nums;color:var(--_k-fg-3)}
18555
+ .knobs-wt-delta{display:inline-flex;gap:6px}
18556
+ .knobs-wt-add{color:var(--_k-diff-add)}
18557
+ .knobs-wt-del{color:var(--_k-diff-del)}
18558
+ .knobs-wt-sstatus{margin-top:calc((var(--kw-line) - 20px) / 2)}
18559
+ .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr) auto}
18560
+ .knobs-wt-stack[data-flat] .knobs-wt-srow+.knobs-wt-srow{box-shadow:inset 0 1px 0 var(--_k-border)}
18561
+ @container knobs-wt-slot (max-width:520px){
18562
+ .knobs-wt-srow:not([data-trunk]){grid-template-columns:auto minmax(0,1fr)}
18563
+ .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr)}
18564
+ .knobs-wt-sstatus{grid-column:-2;justify-self:start;margin-top:4px}
17669
18565
  }
17670
- var grid = (v) => Math.round(v * 64) / 64;
17671
- function chapterPills(host, chapters, io) {
17672
- const doc = host.ownerDocument;
17673
- const win = doc.defaultView;
17674
- const bar = el(doc, "div", "knobs-wt-chapters");
17675
- bar.setAttribute("role", "group");
17676
- bar.setAttribute("aria-label", "Chapters");
17677
- const mark = el(doc, "span", "knobs-wt-chapter-mark");
17678
- mark.setAttribute("aria-hidden", "true");
17679
- bar.append(mark);
17680
- const pills = chapters.map((c) => {
17681
- const b = el(doc, "button", "knobs-wt-chapter");
17682
- b.type = "button";
17683
- b.append(text(doc, "span", "knobs-wt-chapter-time", clock(c.start)));
17684
- b.append(text(doc, "span", "knobs-wt-chapter-label", c.label));
17685
- b.addEventListener("click", () => {
17686
- io.onEvent?.({ what: "seek", time: Math.round(c.start * 100) / 100, detail: c.label });
17687
- io.seek(c.start);
17688
- sync();
17689
- });
17690
- bar.append(b);
17691
- return b;
17692
- });
17693
- host.append(bar);
17694
- const x = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
17695
- const y = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
17696
- const w = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
17697
- const draw = () => {
17698
- const left = grid(x.x);
17699
- mark.style.transform = `translate(${left}px,${grid(y.x)}px)`;
17700
- mark.style.width = `${grid(x.x + w.x) - left}px`;
17701
- };
17702
- const loop = frameLoop(win, (dt) => {
17703
- x.step(dt);
17704
- y.step(dt);
17705
- w.step(dt);
17706
- draw();
17707
- return !x.resting || !y.resting || !w.resting;
17708
- });
17709
- let at = -2;
17710
- let shown = false;
17711
- const place = (snap) => {
17712
- const p = pills[at];
17713
- if (!p) {
17714
- mark.removeAttribute("data-on");
17715
- pills.forEach((b) => b.removeAttribute("aria-current"));
17716
- shown = false;
17717
- return;
17718
- }
17719
- pills.forEach((b, i) => i === at ? b.setAttribute("aria-current", "true") : b.removeAttribute("aria-current"));
17720
- const to = [p.offsetLeft, p.offsetTop, p.offsetWidth];
17721
- if (snap || !shown || reducedMotion(win)) {
17722
- x.snap(to[0]);
17723
- y.snap(to[1]);
17724
- w.snap(to[2]);
17725
- draw();
17726
- } else {
17727
- x.set(to[0]);
17728
- y.set(to[1]);
17729
- w.set(to[2]);
17730
- loop.wake();
17731
- }
17732
- mark.style.height = `${p.offsetHeight}px`;
17733
- mark.dataset.on = "";
17734
- shown = true;
17735
- };
17736
- const sync = () => {
17737
- const next = chapterAt(chapters, io.now());
17738
- if (next === at)
17739
- return;
17740
- at = next;
17741
- place(false);
17742
- };
17743
- sync();
17744
- const ro = "ResizeObserver" in win ? new win.ResizeObserver(() => place(true)) : null;
17745
- ro?.observe(bar);
17746
- return {
17747
- sync,
17748
- get current() {
17749
- return chapters[at] ?? null;
17750
- },
17751
- dispose() {
17752
- loop.stop();
17753
- ro?.disconnect();
17754
- bar.remove();
17755
- }
17756
- };
18566
+
18567
+ /* A table: a real one, its head stuck while it scrolls, both ways; edges fade where there's more
18568
+ (the top one under the head), and thin bars show on hover. */
18569
+ /* Too narrow for its columns, each row becomes a block: its primary cell titling the rest as term
18570
+ and value, the blocks parted by hairlines. Scrolling sideways is the last resort (stack: never). */
18571
+ .knobs-wt-tablebox{min-width:0}
18572
+ .knobs-wt-tablebox[data-measure] table{width:min-content}
18573
+ .knobs-wt-tstack{display:none;flex-direction:column;min-width:0}
18574
+ .knobs-wt-tablebox[data-stacked]>.knobs-wt-table{display:none}
18575
+ .knobs-wt-tablebox[data-stacked]>.knobs-wt-tstack{display:flex}
18576
+ .knobs-wt-trow{display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0;padding-block:var(--_k-space-title);box-shadow:inset 0 -1px 0 var(--_k-border-subtle)}
18577
+ .knobs-wt-trow:first-child{padding-top:0}
18578
+ .knobs-wt-trow:last-of-type{padding-bottom:0;box-shadow:none}
18579
+ .knobs-wt-tline{display:flex;align-items:baseline;justify-content:space-between;gap:var(--_k-space-title);min-width:0}
18580
+ .knobs-wt-tbadges{flex:none;display:flex;align-items:center;gap:var(--_k-space-inside)}
18581
+ .knobs-wt-ttitle{flex:1 1 auto;min-width:0;color:var(--_k-fg);font-weight:var(--_k-weight-label);overflow-wrap:anywhere}
18582
+ .knobs-wt-ttitle[data-mono]{font-family:var(--_k-mono);font-size:var(--_k-type-label)}
18583
+ .knobs-wt-tterms{display:grid;grid-template-columns:minmax(6ch,max-content) minmax(0,1fr);gap:var(--_k-space-inside) var(--_k-space-title);min-width:0}
18584
+ .knobs-wt-tterms dt{color:var(--_k-fg-3);font-size:var(--_k-type-small)}
18585
+ .knobs-wt-tterms dd{min-width:0;color:var(--_k-fg-2);overflow-wrap:anywhere}
18586
+ .knobs-wt-tterms dd[data-mono]{font-family:var(--_k-mono);font-size:var(--_k-type-label);color:var(--_k-fg)}
18587
+ .knobs-wt-tcaption{padding-top:var(--_k-space-sibling);color:var(--_k-fg-3);font-size:var(--_k-type-small)}
18588
+ @property --kw-fade-l{syntax:"<number>";inherits:false;initial-value:1}
18589
+ @property --kw-fade-r{syntax:"<number>";inherits:false;initial-value:1}
18590
+ @keyframes kw-fade-l{from{--kw-fade-l:1}to{--kw-fade-l:0}}
18591
+ @keyframes kw-fade-r{from{--kw-fade-r:0}to{--kw-fade-r:1}}
18592
+ .knobs-wt-table{max-height:520px;overflow:auto;overscroll-behavior-x:contain;outline:none;scrollbar-width:thin;scrollbar-color:transparent transparent;
18593
+ transition:scrollbar-color calc(var(--_k-hover-out) * 1ms)}
18594
+ .knobs-wt-table:is(:hover,:focus-visible){scrollbar-color:color-mix(in oklch,var(--_k-fg) 28%,transparent) transparent;transition-duration:calc(var(--_k-hover-in) * 1ms)}
18595
+ .knobs-wt-table:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus);border-radius:calc(var(--_k-radius-panel) - 4px)}
18596
+ @supports (animation-timeline:scroll()){
18597
+ .knobs-wt-table{animation:knobs-fade-a linear both,knobs-fade-b linear both,kw-fade-l linear both,kw-fade-r linear both;animation-duration:auto;
18598
+ animation-timeline:scroll(self block),scroll(self block),scroll(self inline),scroll(self inline);animation-range:0 24px,calc(100% - 24px) 100%,0 8px,calc(100% - 8px) 100%;
18599
+ --kw-table-mask-y:linear-gradient(to bottom,#000 var(--kw-thead,0px),rgb(0 0 0/var(--knobs-fade-a)) var(--kw-thead,0px),#000 calc(var(--kw-thead,0px) + 20px),#000 calc(100% - 20px),rgb(0 0 0/var(--knobs-fade-b)));
18600
+ --kw-table-mask-x:linear-gradient(to right,rgb(0 0 0/var(--kw-fade-l)),#000 24px,#000 calc(100% - 24px),rgb(0 0 0/var(--kw-fade-r)));
18601
+ -webkit-mask-image:var(--kw-table-mask-y),var(--kw-table-mask-x);mask-image:var(--kw-table-mask-y),var(--kw-table-mask-x);-webkit-mask-composite:source-in;mask-composite:intersect}
17757
18602
  }
18603
+ .knobs-wt-table table{width:100%;margin:0;border-collapse:separate;border-spacing:0;font-size:var(--_k-type-body);line-height:var(--_k-leading-body);font-variant-numeric:tabular-nums;color:var(--_k-fg-2)}
18604
+ .knobs-wt-table caption{caption-side:bottom;padding-top:8px;text-align:left;color:var(--_k-fg-3);font-size:var(--_k-type-small)}
18605
+ .knobs-wt-table th{position:sticky;top:0;padding:0 12px 7px;text-align:left;vertical-align:bottom;font-weight:var(--_k-weight-label);font-size:var(--_k-type-small);color:var(--_k-fg-3);white-space:nowrap;
18606
+ background:var(--kw-under,var(--_k-pane-bg));box-shadow:inset 0 -1px 0 var(--_k-border)}
18607
+ .knobs-wt-table td{padding:8px 12px;vertical-align:baseline;box-shadow:inset 0 -1px 0 var(--_k-border)}
18608
+ .knobs-wt-table tr:last-child td{box-shadow:none}
18609
+ .knobs-wt-table :is(th,td):first-child{padding-left:0}
18610
+ .knobs-wt-table :is(th,td):last-child{padding-right:0}
18611
+ .knobs-wt-table td:first-child{color:var(--_k-fg)}
18612
+ .knobs-wt-table [data-align="right"]{text-align:right}
18613
+ .knobs-wt-table [data-align="center"]{text-align:center}
18614
+ .knobs-wt-table td[data-mono]{font-family:var(--_k-mono);font-size:var(--_k-type-label);color:var(--_k-fg);white-space:nowrap}
18615
+ .knobs-wt-table td[data-long]{min-width:24ch}
18616
+ .knobs-ui :is(.knobs-wt-table,.knobs-wt-tstack) a.knobs-wt-chip{text-decoration:none}
18617
+ .knobs-wt-ctone{color:var(--kw-tone)}
18618
+
18619
+ /* Chapters: a readout list under a video or compare: number, label, start; the selection pill
18620
+ on the one playing, its rows bled out so their text sits on the media's edge. */
18621
+ .knobs-wt-body:has(>.knobs-wt-chapters){display:flex;flex-direction:column;gap:var(--_k-space-title)}
18622
+ .knobs-wt-chapters{position:relative;display:flex;flex-direction:column;margin:0 calc(var(--_k-space-sibling) * -1)}
18623
+ .knobs-ui .knobs-wt-chapter{position:relative;display:grid;grid-template-columns:2ch minmax(0,1fr) auto;column-gap:var(--_k-space-sibling);align-items:center;height:32px;padding:0 var(--_k-space-sibling);
18624
+ border-radius:var(--_k-radius);color:var(--_k-fg-2);text-align:left;white-space:nowrap;cursor:pointer;transition:color calc(var(--_k-hover-out) * 1ms)}
18625
+ .knobs-ui .knobs-wt-chapter:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18626
+ .knobs-ui .knobs-wt-chapter:focus-visible{box-shadow:inset 0 0 0 1px var(--_k-focus)}
18627
+ .knobs-wt-chapter-n{font-size:var(--_k-type-small);color:var(--_k-fg-3)}
18628
+ .knobs-wt-chapter-label{min-width:0;overflow:hidden;${fade2}}
18629
+ .knobs-wt-chapter-time{font:var(--_k-weight) var(--_k-type-small)/1 var(--_k-mono);color:var(--_k-fg-3)}
18630
+ .knobs-pick-copy :is(.knobs-wt-chapter-n,.knobs-wt-chapter-time){color:var(--_k-fg-2)}
18631
+ @media(prefers-reduced-motion:reduce){:is(.knobs-walkthrough,.knobs-wt-slot) :where(*,::before,::after){transition-duration:.15s!important;transition-property:opacity,background,color!important}}
18632
+ @media(prefers-reduced-motion:reduce){.knobs-wt-disc-content{transition:opacity .15s}}
18633
+ `;
17758
18634
 
17759
18635
  // src/ui/walkthrough-media.ts
17760
18636
  function injectWalkthroughStyles(doc) {
@@ -18283,7 +19159,7 @@ function mediaSlot(container, options) {
18283
19159
  }
18284
19160
  };
18285
19161
  }
18286
- var CHECK3 = svg('<path d="m3.5 8.4 3 3 6-6.6"/>');
19162
+ var CHECK3 = icon("check", { small: true });
18287
19163
 
18288
19164
  // src/ui/walkthrough.ts
18289
19165
  var el2 = (doc, tag, cls) => {
@@ -18303,14 +19179,18 @@ var lookOf2 = (o) => ({
18303
19179
  });
18304
19180
  var grid2 = (v) => Math.round(v * 64) / 64;
18305
19181
  var ICON = {
18306
- full: svg('<path d="M3 6.5V3h3.5M13 6.5V3H9.5M3 9.5V13h3.5M13 9.5V13H9.5"/>'),
18307
- exit: svg('<path d="M6.5 3v3.5H3M9.5 3v3.5H13M6.5 13V9.5H3M9.5 13V9.5H13"/>'),
18308
- pen: svg('<path d="M10.5 3.2 12.8 5.5 5.8 12.5 3 13l.5-2.8z"/><path d="M9.3 4.4l2.3 2.3"/>'),
18309
- mic: svg('<rect x="6" y="2.5" width="4" height="7" rx="2"/><path d="M3.8 8a4.2 4.2 0 0 0 8.4 0M8 12.2v1.8"/>'),
19182
+ full: icon("cornersOut"),
19183
+ exit: icon("cornersIn"),
19184
+ pen: icon("pencilSimple"),
19185
+ mic: icon("microphone"),
18310
19186
  stop: svg('<rect x="4.5" y="4.5" width="7" height="7" rx="1.5" fill="currentColor" stroke="none"/>'),
18311
- send: svg('<path d="M8 13V3.5M4 7.5 8 3.5l4 4"/>'),
18312
- close: svg('<path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/>')
19187
+ send: icon("paperPlaneRight"),
19188
+ close: icon("x", { small: true }),
19189
+ back: icon("arrowLeft", { small: true }),
19190
+ out: icon("arrowUpRight", { small: true })
18313
19191
  };
19192
+ var DOCK_INSET = 12;
19193
+ var DOCK_GAP = 12;
18314
19194
  var CALM2 = springMotion(SPRINGS.calm);
18315
19195
  var SMOOTH = springMotion(SPRINGS.smooth);
18316
19196
  var BACK_MS = 4500;
@@ -18443,8 +19323,12 @@ function walkthrough(container, options) {
18443
19323
  let count = null;
18444
19324
  let fill = null;
18445
19325
  let tocEl = null;
18446
- let mark = null;
19326
+ let pill = null;
19327
+ let tocRows = [];
19328
+ let discs = [];
18447
19329
  let full = null;
19330
+ const noteMode = options.noteBar ?? "collapsed";
19331
+ let reserve = noteBarReserve(52, DOCK_INSET, DOCK_GAP);
18448
19332
  const viewOf = (key) => views.find((v) => v.step.key === key);
18449
19333
  const feedback = () => collectFeedback(page, state);
18450
19334
  const markdown = () => feedbackMarkdown(feedback(), page.verdicts);
@@ -18468,9 +19352,6 @@ function walkthrough(container, options) {
18468
19352
  else
18469
19353
  run();
18470
19354
  };
18471
- const pillY = new Spring(0, CALM2.omega, CALM2.zeta, 0.25);
18472
- const pillH = new Spring(0, CALM2.omega, CALM2.zeta, 0.25);
18473
- let pillPlaced = false;
18474
19355
  const scroll = {
18475
19356
  spring: new Spring(0, CALM2.omega, CALM2.zeta, 0.5),
18476
19357
  to: null,
@@ -18478,13 +19359,6 @@ function walkthrough(container, options) {
18478
19359
  };
18479
19360
  const motion = frameLoop(win, (dt) => {
18480
19361
  let more = false;
18481
- if (mark) {
18482
- pillY.step(dt);
18483
- pillH.step(dt);
18484
- mark.style.transform = `translateY(${grid2(pillY.x)}px)`;
18485
- mark.style.height = `${grid2(pillY.x + pillH.x) - grid2(pillY.x)}px`;
18486
- more ||= !pillY.resting || !pillH.resting;
18487
- }
18488
19362
  if (scroll.to && scroll.el) {
18489
19363
  scroll.spring.set(scrollTarget(scroll.to, scroll.el));
18490
19364
  scroll.spring.step(dt);
@@ -18548,21 +19422,12 @@ function walkthrough(container, options) {
18548
19422
  v.glyph.dataset.tone = v.step.ask === "none" ? "none" : verdict ? verdictTone(verdict, page.verdicts.findIndex((x) => x.id === verdict)) : done ? "accent" : "dim";
18549
19423
  };
18550
19424
  const placeMark = () => {
18551
- if (!mark || !tocEl)
19425
+ if (!pill || !tocEl)
18552
19426
  return;
19427
+ pill.update();
18553
19428
  const row = viewOf(current)?.toc;
18554
19429
  if (!row)
18555
- return void mark.removeAttribute("data-on");
18556
- mark.dataset.on = "";
18557
- if (!pillPlaced || reduced()) {
18558
- pillY.snap(row.offsetTop);
18559
- pillH.snap(row.offsetHeight);
18560
- pillPlaced = true;
18561
- } else {
18562
- pillY.set(row.offsetTop);
18563
- pillH.set(row.offsetHeight);
18564
- }
18565
- motion.wake();
19430
+ return;
18566
19431
  const top = row.offsetTop;
18567
19432
  const bottom = top + row.offsetHeight;
18568
19433
  if (top < tocEl.scrollTop + 8)
@@ -18579,6 +19444,9 @@ function walkthrough(container, options) {
18579
19444
  const v = viewOf(key);
18580
19445
  v?.card.setAttribute("data-current", "");
18581
19446
  v?.toc?.setAttribute("aria-current", "step");
19447
+ for (const r of tocRows)
19448
+ if (r.dataset.section)
19449
+ r.toggleAttribute("data-active", r.dataset.section === v?.step.section);
18582
19450
  if (key)
18583
19451
  record({ what: "step", step: key });
18584
19452
  placeMark();
@@ -18595,6 +19463,16 @@ function walkthrough(container, options) {
18595
19463
  else
18596
19464
  break;
18597
19465
  }
19466
+ const s = scrollerOf(root);
19467
+ if (s.scrollTop > 0 && s.scrollTop >= s.scrollHeight - s.clientHeight - 2) {
19468
+ const bottom = (s === doc.scrollingElement ? win.innerHeight : s.getBoundingClientRect().bottom) - reserve;
19469
+ for (let i = views.length - 1;i >= 0; i--) {
19470
+ if (views[i].card.getBoundingClientRect().top < bottom) {
19471
+ pick = views[i].step.key;
19472
+ break;
19473
+ }
19474
+ }
19475
+ }
18598
19476
  setCurrent(pick);
18599
19477
  };
18600
19478
  let scrollRaf = 0;
@@ -18637,7 +19515,12 @@ function walkthrough(container, options) {
18637
19515
  };
18638
19516
  const scrim = el2(doc, "div", "knobs-wt-scrim");
18639
19517
  scrim.addEventListener("click", () => setFull(false));
18640
- const fullRect = () => ({ x: 12, y: 12, w: win.innerWidth - 24, h: win.innerHeight - 24 });
19518
+ const fullRect = () => ({
19519
+ x: DOCK_INSET,
19520
+ y: DOCK_INSET,
19521
+ w: win.innerWidth - DOCK_INSET * 2,
19522
+ h: win.innerHeight - DOCK_INSET - reserve
19523
+ });
18641
19524
  const placeFull = (f) => {
18642
19525
  const a = f.hold.getBoundingClientRect();
18643
19526
  const b = fullRect();
@@ -18782,6 +19665,7 @@ function walkthrough(container, options) {
18782
19665
  ink.on = true;
18783
19666
  ink.layer.dataset.on = "";
18784
19667
  root.dataset.inking = "";
19668
+ bar.setOpen(true);
18785
19669
  bar.sync();
18786
19670
  };
18787
19671
  const voice = {
@@ -18821,6 +19705,7 @@ function walkthrough(container, options) {
18821
19705
  });
18822
19706
  voice.recorder = rec;
18823
19707
  voice.started = performance.now();
19708
+ bar.setOpen(true);
18824
19709
  rec.start();
18825
19710
  voice.timer = win.setInterval(() => bar.sync(), 250);
18826
19711
  bar.sync();
@@ -18847,7 +19732,10 @@ function walkthrough(container, options) {
18847
19732
  if (!v)
18848
19733
  return;
18849
19734
  backTo = key;
18850
- backPill.textContent = `← Back to ${v.step.number}`;
19735
+ backPill.innerHTML = ICON.back;
19736
+ const words = el2(doc, "span", "knobs-wt-back-label");
19737
+ words.textContent = `Back to ${plainMarkdown(v.step.title)}`;
19738
+ backPill.append(words);
18851
19739
  backPill.dataset.on = "";
18852
19740
  win.clearTimeout(backTimer);
18853
19741
  backTimer = win.setTimeout(hideBack, BACK_MS);
@@ -18879,7 +19767,41 @@ function walkthrough(container, options) {
18879
19767
  sendBtn.dataset.send = "";
18880
19768
  row.append(input, attach, fullBtn, penBtn, micBtn, sendBtn);
18881
19769
  box.append(on, row);
18882
- dock.append(backPill, box);
19770
+ const launch = el2(doc, "button", "knobs-wt-launch");
19771
+ launch.type = "button";
19772
+ launch.innerHTML = ICON.pen;
19773
+ launch.append("Note");
19774
+ const kbd = el2(doc, "kbd");
19775
+ kbd.textContent = "n";
19776
+ launch.append(kbd);
19777
+ launch.setAttribute("aria-label", "Write a note (n)");
19778
+ launch.addEventListener("click", () => setOpen(true, true));
19779
+ dock.dataset.mode = noteMode;
19780
+ dock.append(backPill, box, launch);
19781
+ let open = noteMode !== "collapsed";
19782
+ const setOpen = (on, focus = false) => {
19783
+ if (noteMode === "collapsed" && on !== open) {
19784
+ open = on;
19785
+ dock.toggleAttribute("data-open", on);
19786
+ box.inert = !on;
19787
+ launch.inert = on;
19788
+ syncReserve();
19789
+ }
19790
+ if (on && focus)
19791
+ input.focus({ preventScroll: true });
19792
+ };
19793
+ dock.toggleAttribute("data-open", open);
19794
+ box.inert = !open;
19795
+ launch.inert = open;
19796
+ const empty = () => !input.value.trim() && !voice.pending && !voice.recorder && !ink.on && !ink.paths.length;
19797
+ const onOutside = (e) => {
19798
+ if (open && noteMode === "collapsed" && !dock.contains(e.target) && empty())
19799
+ setOpen(false);
19800
+ };
19801
+ doc.addEventListener("pointerdown", onOutside, true);
19802
+ const ro = "ResizeObserver" in win ? new win.ResizeObserver(() => syncReserve()) : null;
19803
+ ro?.observe(box);
19804
+ ro?.observe(launch);
18883
19805
  const fit = () => {
18884
19806
  input.style.height = "auto";
18885
19807
  input.style.height = `${Math.min(140, input.scrollHeight)}px`;
@@ -18895,6 +19817,8 @@ function walkthrough(container, options) {
18895
19817
  } else if (e.key === "Escape") {
18896
19818
  e.preventDefault();
18897
19819
  input.blur();
19820
+ if (empty())
19821
+ setOpen(false);
18898
19822
  viewOf(current)?.card.focus({ preventScroll: true });
18899
19823
  }
18900
19824
  });
@@ -18914,13 +19838,13 @@ function walkthrough(container, options) {
18914
19838
  const sync = () => {
18915
19839
  const v = viewOf(current);
18916
19840
  const ctx = describeMedia(mediaContext(v));
18917
- const where = v ? `${v.step.number} ${v.step.title}` : "General note";
19841
+ const where = v ? plainMarkdown(v.step.title) : "General note";
18918
19842
  const text = ctx ? `${where} · ${ctx}` : where;
18919
19843
  if (text !== lastOn) {
18920
19844
  on.textContent = text;
18921
19845
  lastOn = text;
18922
19846
  }
18923
- input.placeholder = v ? `Note on ${v.step.number}… (n)` : "A note on the whole page… (n)";
19847
+ input.placeholder = v ? "A note on this step… (n)" : "A note on the whole page… (n)";
18924
19848
  const attached = `${voice.pending?.url ?? ""}|${ink.paths.length}`;
18925
19849
  if (attached !== lastAttached) {
18926
19850
  lastAttached = attached;
@@ -18959,14 +19883,45 @@ function walkthrough(container, options) {
18959
19883
  };
18960
19884
  return {
18961
19885
  dock,
19886
+ box,
19887
+ launch,
18962
19888
  input,
18963
19889
  sync,
19890
+ get open() {
19891
+ return open;
19892
+ },
19893
+ setOpen,
19894
+ empty,
19895
+ unobserve: () => {
19896
+ ro?.disconnect();
19897
+ doc.removeEventListener("pointerdown", onOutside, true);
19898
+ },
18964
19899
  clear() {
18965
19900
  input.value = "";
18966
19901
  fit();
18967
19902
  }
18968
19903
  };
18969
19904
  })();
19905
+ let padded = null;
19906
+ function syncReserve() {
19907
+ if (disposed)
19908
+ return;
19909
+ const h = (bar.open ? bar.box : bar.launch).offsetHeight;
19910
+ if (noteMode === "hidden")
19911
+ reserve = 0;
19912
+ else if (h)
19913
+ reserve = noteBarReserve(h, DOCK_INSET, DOCK_GAP);
19914
+ root.style.setProperty("--kw-reserve", `${reserve}px`);
19915
+ const s = scrollerOf(root);
19916
+ if (padded?.el !== s) {
19917
+ if (padded)
19918
+ padded.el.style.scrollPaddingBottom = padded.was;
19919
+ padded = { el: s, was: s.style.scrollPaddingBottom };
19920
+ }
19921
+ s.style.scrollPaddingBottom = `${reserve}px`;
19922
+ if (full)
19923
+ placeFull(full);
19924
+ }
18970
19925
  const ticker = win.setInterval(() => !doc.hidden && bar.sync(), 500);
18971
19926
  const send = (text = bar.input.value) => {
18972
19927
  if (voice.recorder)
@@ -19002,6 +19957,10 @@ function walkthrough(container, options) {
19002
19957
  syncProgress();
19003
19958
  emit();
19004
19959
  bar.sync();
19960
+ if (noteMode === "collapsed") {
19961
+ bar.input.blur();
19962
+ bar.setOpen(false);
19963
+ }
19005
19964
  if (v && options.advance !== false) {
19006
19965
  const to = adjacentStep(views.map((x) => x.step.key), v.step.key, 1);
19007
19966
  if (to && to !== v.step.key) {
@@ -19009,7 +19968,8 @@ function walkthrough(container, options) {
19009
19968
  setFull(false);
19010
19969
  go(to);
19011
19970
  showBack(v.step.key);
19012
- bar.input.focus({ preventScroll: true });
19971
+ if (noteMode === "docked")
19972
+ bar.input.focus({ preventScroll: true });
19013
19973
  }
19014
19974
  }
19015
19975
  };
@@ -19096,22 +20056,105 @@ function walkthrough(container, options) {
19096
20056
  row.addEventListener("transitionend", done, { once: true });
19097
20057
  win.setTimeout(done, 600);
19098
20058
  };
20059
+ const metaLine = (cls) => el2(doc, "div", `knobs-wt-metaline ${cls}`);
20060
+ const metaText = (line, text, cls) => {
20061
+ const s = el2(doc, "span", cls);
20062
+ s.textContent = text;
20063
+ line.append(s);
20064
+ return s;
20065
+ };
20066
+ const metaLink = (line, text, href) => {
20067
+ const a = el2(doc, "a");
20068
+ a.href = href;
20069
+ a.target = "_blank";
20070
+ a.rel = "noreferrer";
20071
+ a.textContent = text;
20072
+ a.insertAdjacentHTML("beforeend", ICON.out);
20073
+ line.append(a);
20074
+ return a;
20075
+ };
20076
+ const feedItem = (f, now) => {
20077
+ const li = el2(doc, "li", "knobs-wt-fi");
20078
+ li.dataset.kind = f.kind;
20079
+ const q = el2(doc, "div", "knobs-wt-fi-q");
20080
+ const meta = metaLine("knobs-wt-fi-meta");
20081
+ if (f.at !== null) {
20082
+ const when = metaText(meta, ago(f.at, now));
20083
+ when.title = new Date(f.at).toLocaleString();
20084
+ } else
20085
+ metaText(meta, f.kind === "qa" ? "Answer" : "Update");
20086
+ const step = f.step ? page.steps.find((s) => s.key === f.step) : undefined;
20087
+ if (step) {
20088
+ const section = page.sections.find((s) => s.id === step.section);
20089
+ const b = el2(doc, "button", "knobs-wt-fi-step");
20090
+ b.type = "button";
20091
+ b.textContent = section?.link?.label ?? plainMarkdown(step.title);
20092
+ b.title = `Go to “${plainMarkdown(step.title)}”`;
20093
+ b.addEventListener("click", () => go(step.key));
20094
+ meta.append(b);
20095
+ }
20096
+ if (f.status) {
20097
+ const id = f.status.toLowerCase();
20098
+ const shipped = /^(ship|shipped|merged|done)$/.test(id);
20099
+ const changes = /^(changes?|changes queued)$/.test(id);
20100
+ const s = metaText(meta, shipped ? "Shipped" : changes ? "Changes queued" : f.status);
20101
+ s.dataset.status = shipped ? "on" : changes ? "warn" : "";
20102
+ }
20103
+ q.append(meta);
20104
+ if (f.q)
20105
+ q.append(prose2(doc, "p", "knobs-wt-fi-ask", f.q));
20106
+ else
20107
+ q.append(prose2(doc, "p", "knobs-wt-fi-text", f.text));
20108
+ li.append(q);
20109
+ if (f.q || f.trail.length) {
20110
+ const a = el2(doc, "div", "knobs-wt-fi-a");
20111
+ if (f.q)
20112
+ a.append(prose2(doc, "div", "knobs-wt-fi-answer", f.text));
20113
+ if (f.trail.length) {
20114
+ const list = el2(doc, "ol", "knobs-wt-trail");
20115
+ for (const line of f.trail) {
20116
+ const i = line.indexOf(" · ");
20117
+ const row = el2(doc, "li");
20118
+ const words = el2(doc, "span", "knobs-wt-trail-text");
20119
+ words.textContent = i < 0 ? line : line.slice(i + 3);
20120
+ row.append(words);
20121
+ if (i >= 0) {
20122
+ const at = el2(doc, "span", "knobs-wt-trail-at");
20123
+ at.textContent = line.slice(0, i);
20124
+ row.append(at);
20125
+ }
20126
+ list.append(row);
20127
+ }
20128
+ discs.push(disclosure(a, { label: "Context", count: f.trail.length, content: list }));
20129
+ }
20130
+ li.append(a);
20131
+ }
20132
+ return li;
20133
+ };
19099
20134
  const build = () => {
19100
20135
  for (const v of views) {
19101
20136
  v.slots.forEach((s) => s.dispose());
19102
20137
  v.taste?.dispose();
19103
20138
  }
20139
+ for (const d of discs)
20140
+ d.dispose();
20141
+ discs = [];
20142
+ pill?.dispose();
20143
+ pill = null;
20144
+ tocRows = [];
19104
20145
  if (full) {
19105
20146
  full.p.snap(0);
19106
20147
  closeFull();
19107
20148
  }
19108
20149
  clearInk();
19109
20150
  views = [];
19110
- pillPlaced = false;
19111
20151
  root.textContent = "";
19112
20152
  root.removeAttribute("data-settled");
20153
+ root.dataset.rail = options.rail === false ? "none" : options.rail === "left" ? "left" : "right";
20154
+ root.toggleAttribute("data-cards", options.cards === true);
19113
20155
  const layout = el2(doc, "div", "knobs-wt-layout");
19114
20156
  const main = el2(doc, "div", "knobs-wt-main");
20157
+ const flow = el2(doc, "div", "knobs-wt-flow");
19115
20158
  const head = el2(doc, "header", "knobs-wt-head");
19116
20159
  const h1 = el2(doc, "h1", "knobs-wt-title");
19117
20160
  h1.textContent = page.title;
@@ -19138,42 +20181,27 @@ function walkthrough(container, options) {
19138
20181
  }
19139
20182
  head.append(meta);
19140
20183
  }
19141
- main.append(head);
20184
+ flow.append(head);
20185
+ let feedEl = null;
19142
20186
  if (page.feed.length) {
19143
- const feed = el2(doc, "section", "knobs-wt-feed");
19144
- feed.setAttribute("aria-label", "Latest");
19145
- const list = el2(doc, "ol", "knobs-wt-feed-list knobs-scroll-fade");
20187
+ feedEl = el2(doc, "section", "knobs-wt-feed");
20188
+ feedEl.id = "wt-latest";
20189
+ feedEl.setAttribute("aria-label", "Updates and answers");
20190
+ const fh = el2(doc, "header", "knobs-wt-feed-head");
20191
+ const h = el2(doc, "h2", "knobs-wt-feed-title");
20192
+ h.textContent = page.feed.some((f) => f.kind === "qa") ? "Updates & Q&A" : "Updates";
20193
+ const n = el2(doc, "span", "knobs-wt-feed-count");
20194
+ n.textContent = String(page.feed.length);
20195
+ fh.append(h, n);
20196
+ const list = el2(doc, "ol", "knobs-wt-feed-list");
19146
20197
  const now = Date.now();
19147
- for (const f of page.feed) {
19148
- const li = el2(doc, "li", "knobs-wt-fi");
19149
- li.dataset.kind = f.kind;
19150
- const when = el2(doc, "span", "knobs-wt-fi-when");
19151
- when.textContent = f.at === null ? "" : ago(f.at, now);
19152
- if (f.at !== null)
19153
- when.title = new Date(f.at).toLocaleString();
19154
- const kind = el2(doc, "span", "knobs-wt-fi-kind");
19155
- kind.textContent = f.kind === "qa" ? "Answer" : "Update";
19156
- const body = el2(doc, "div", "knobs-wt-fi-body");
19157
- if (f.q)
19158
- body.append(prose2(doc, "p", "knobs-wt-fi-q", f.q));
19159
- body.append(prose2(doc, "p", "knobs-wt-fi-text", f.text));
19160
- li.append(when, kind, body);
19161
- if (f.step) {
19162
- const target = f.step;
19163
- const link = el2(doc, "button", "knobs-wt-fi-step");
19164
- link.type = "button";
19165
- link.textContent = page.steps.find((s) => s.key === target)?.number ?? "";
19166
- link.title = "Go to the step";
19167
- link.addEventListener("click", () => go(target));
19168
- li.append(link);
19169
- }
19170
- list.append(li);
19171
- }
19172
- feed.append(list);
19173
- main.append(feed);
20198
+ for (const f of page.feed)
20199
+ list.append(feedItem(f, now));
20200
+ feedEl.append(fh, list);
20201
+ flow.append(feedEl);
19174
20202
  }
19175
20203
  let rail = null;
19176
- count = fill = tocEl = mark = null;
20204
+ count = fill = tocEl = null;
19177
20205
  if (options.rail !== false) {
19178
20206
  rail = el2(doc, "aside", "knobs-wt-rail");
19179
20207
  rail.setAttribute("aria-label", "Progress");
@@ -19185,8 +20213,17 @@ function walkthrough(container, options) {
19185
20213
  progress.append(count, meter);
19186
20214
  tocEl = el2(doc, "nav", "knobs-wt-toc");
19187
20215
  tocEl.setAttribute("aria-label", "Contents");
19188
- mark = el2(doc, "div", "knobs-wt-toc-mark");
19189
- tocEl.append(mark);
20216
+ const ns = page.sections.map((s) => s.kicker && s.link ? s.link.label : "");
20217
+ const nw = Math.max(0, ...ns.map((n) => n.length));
20218
+ tocEl.style.setProperty("--kw-toc-n", nw ? `${nw + 0.5}ch` : "0px");
20219
+ if (!nw)
20220
+ tocEl.dataset.bare = "";
20221
+ if (feedEl) {
20222
+ const group = el2(doc, "div", "knobs-wt-toc-group");
20223
+ const target = feedEl;
20224
+ group.append(tocRow("Latest", "", String(page.feed.length), false, () => target.scrollIntoView({ block: "start", behavior: reduced() ? "auto" : "smooth" })));
20225
+ tocEl.append(group);
20226
+ }
19190
20227
  rail.append(progress, tocEl, actions(true));
19191
20228
  if (options.keys !== false) {
19192
20229
  const keys = el2(doc, "div", "knobs-wt-keys");
@@ -19198,45 +20235,44 @@ function walkthrough(container, options) {
19198
20235
  const sec = el2(doc, "section", "knobs-wt-section");
19199
20236
  sec.id = `wt-${section.id}`;
19200
20237
  const sh = el2(doc, "header", "knobs-wt-shead");
19201
- if (section.link || section.status) {
19202
- const kicker = el2(doc, "div", "knobs-wt-kicker");
19203
- if (section.link) {
19204
- const a = el2(doc, "a");
19205
- a.href = section.link.href;
19206
- a.target = "_blank";
19207
- a.rel = "noreferrer";
19208
- a.textContent = `${section.link.label} ↗`;
19209
- kicker.append(a);
19210
- }
20238
+ if (section.kicker || section.link || section.status) {
20239
+ const meta = metaLine("knobs-wt-kicker");
20240
+ if (section.kicker)
20241
+ metaText(meta, section.kicker, "knobs-wt-kicker-lead");
20242
+ if (section.link)
20243
+ metaLink(meta, section.link.label, section.link.href);
19211
20244
  if (section.status) {
19212
20245
  const s = statusLook(section.status);
19213
- const chip = el2(doc, "span", "knobs-wt-chip");
19214
- chip.dataset.tone = s.tone;
19215
- chip.textContent = s.label;
19216
- kicker.append(chip);
20246
+ metaText(meta, s.label, "knobs-wt-status").dataset.tone = s.tone;
19217
20247
  }
19218
- sh.append(kicker);
20248
+ sh.append(meta);
19219
20249
  }
19220
- const h2 = el2(doc, "h2", "knobs-wt-stitle");
19221
- h2.textContent = section.title;
20250
+ const h2 = el2(doc, "h2", "knobs-wt-heading");
20251
+ h2.innerHTML = inlineMarkdown(section.title);
19222
20252
  sh.append(h2);
20253
+ const intro = el2(doc, "div", "knobs-wt-sintro");
20254
+ intro.append(sh);
19223
20255
  if (section.summary)
19224
- sh.append(prose2(doc, "p", "knobs-wt-ssum", section.summary));
19225
- sec.append(sh);
20256
+ intro.append(prose2(doc, "p", "knobs-wt-ssum", section.summary));
20257
+ sec.append(intro);
20258
+ let group = null;
19226
20259
  if (tocEl) {
19227
- const t = el2(doc, "div", "knobs-wt-toc-section");
19228
- t.textContent = section.link ? `${section.link.label} ${section.title}` : section.title;
19229
- t.title = section.title;
19230
- tocEl.append(t);
20260
+ group = el2(doc, "div", "knobs-wt-toc-group");
20261
+ const first = section.steps[0]?.key;
20262
+ const n = section.kicker && section.link ? section.link.label : "";
20263
+ group.append(tocRow(plainMarkdown(section.title), n, "", false, () => first ? go(first) : undefined, section.id));
20264
+ tocEl.append(group);
19231
20265
  }
19232
20266
  for (const step of section.steps) {
19233
- const v = stepView(step);
20267
+ const v = stepView(step, group);
19234
20268
  sec.append(v.card);
19235
20269
  views.push(v);
19236
20270
  }
19237
- main.append(sec);
20271
+ flow.append(sec);
19238
20272
  }
19239
- main.append(bar.dock);
20273
+ main.append(flow);
20274
+ if (noteMode !== "hidden")
20275
+ main.append(bar.dock);
19240
20276
  if (rail)
19241
20277
  layout.append(rail);
19242
20278
  layout.append(main);
@@ -19246,16 +20282,40 @@ function walkthrough(container, options) {
19246
20282
  for (const n of notesOn(state, v.step.key))
19247
20283
  renderNote(v, n);
19248
20284
  }
20285
+ if (tocEl)
20286
+ pill = selectionPill(tocEl, {
20287
+ selected: () => viewOf(current)?.toc ?? null,
20288
+ rows: () => tocRows
20289
+ });
19249
20290
  syncProgress();
19250
20291
  current = null;
19251
20292
  fromScroll();
19252
20293
  placeMark();
19253
20294
  bar.sync();
20295
+ syncReserve();
19254
20296
  win.requestAnimationFrame(() => win.requestAnimationFrame(() => {
19255
20297
  if (!disposed && !reduced())
19256
20298
  root.dataset.settled = "";
19257
20299
  }));
19258
20300
  };
20301
+ const tocRow = (label, n, end, sub, run, section) => {
20302
+ const b = el2(doc, "button", sub ? "knobs-wt-toc-row knobs-wt-toc-sub" : "knobs-wt-toc-row");
20303
+ b.type = "button";
20304
+ if (section)
20305
+ b.dataset.section = section;
20306
+ const ncol = el2(doc, "span", "knobs-wt-toc-n");
20307
+ ncol.textContent = n;
20308
+ const t = el2(doc, "span", "knobs-wt-toc-label");
20309
+ t.textContent = label;
20310
+ const e = el2(doc, "span", "knobs-wt-toc-end");
20311
+ e.textContent = end;
20312
+ b.title = label;
20313
+ b.append(ncol, t, e);
20314
+ b.addEventListener("mousedown", (ev) => ev.preventDefault());
20315
+ b.addEventListener("click", run);
20316
+ tocRows.push(b);
20317
+ return b;
20318
+ };
19259
20319
  const actions = (rail) => {
19260
20320
  const row = el2(doc, "div", "knobs-wt-actions");
19261
20321
  const btn = (text, run, primary = false) => {
@@ -19296,30 +20356,38 @@ function walkthrough(container, options) {
19296
20356
  toast("Couldn't reach the clipboard", { tone: "warn", look, doc });
19297
20357
  }
19298
20358
  };
19299
- const stepView = (step) => {
19300
- const card = el2(doc, "article", "knobs-wt-step knobs-surface");
20359
+ const stepView = (step, group) => {
20360
+ const card = el2(doc, "article", options.cards ? "knobs-wt-step knobs-surface" : "knobs-wt-step");
19301
20361
  card.id = `wt-${step.key.replace("/", "--")}`;
19302
20362
  card.tabIndex = -1;
19303
20363
  card.dataset.key = step.key;
19304
- card.setAttribute("aria-label", `${step.number} ${step.title}`);
20364
+ card.setAttribute("aria-label", plainMarkdown(step.title));
19305
20365
  const head = el2(doc, "header", "knobs-wt-step-head");
19306
- const num = el2(doc, "span", "knobs-wt-num");
19307
- num.textContent = step.number;
19308
20366
  const h3 = el2(doc, "h3", "knobs-wt-stepTitle");
19309
- h3.textContent = step.title;
19310
- head.append(num, h3);
19311
- card.append(head);
20367
+ h3.innerHTML = inlineMarkdown(step.title);
20368
+ head.append(h3);
19312
20369
  if (step.caption)
19313
- card.append(prose2(doc, "p", "knobs-wt-caption", step.caption));
20370
+ head.append(prose2(doc, "p", "knobs-wt-caption", step.caption));
20371
+ card.append(head);
19314
20372
  const slots = [];
19315
20373
  const onEvent = (e) => record({ ...e, step: step.key });
19316
20374
  let media = null;
19317
20375
  if (step.media.length) {
19318
- media = el2(doc, "div", "knobs-wt-media");
19319
- media.style.setProperty("--kw-n", String(Math.min(step.media.length, 3)));
19320
- for (const m of step.media)
19321
- slots.push(mediaSlot(media, { ...look, media: m, onEvent, steps: stepLinks }));
20376
+ const grid = (list) => {
20377
+ const g = el2(doc, "div", "knobs-wt-media");
20378
+ g.style.setProperty("--kw-n", String(Math.min(list.length, step.columns)));
20379
+ for (const m of list)
20380
+ slots.push(mediaSlot(g, { ...look, media: m, onEvent, steps: stepLinks }));
20381
+ return g;
20382
+ };
20383
+ const [first, ...rest] = step.media;
20384
+ media = grid(step.more ? [first] : step.media);
19322
20385
  card.append(media);
20386
+ if (step.more && rest.length) {
20387
+ const holder = el2(doc, "div", "knobs-wt-more");
20388
+ discs.push(disclosure(holder, { label: step.more, count: rest.length, content: grid(rest) }));
20389
+ card.append(holder);
20390
+ }
19323
20391
  }
19324
20392
  let taste = null;
19325
20393
  if (step.question)
@@ -19344,16 +20412,11 @@ function walkthrough(container, options) {
19344
20412
  });
19345
20413
  let toc = null;
19346
20414
  let glyph = null;
19347
- if (tocEl) {
19348
- toc = el2(doc, "button", "knobs-wt-toc-step");
19349
- toc.type = "button";
20415
+ if (group) {
20416
+ toc = tocRow(plainMarkdown(step.title), "", "", true, () => go(step.key));
19350
20417
  glyph = el2(doc, "span", "knobs-wt-toc-glyph");
19351
- const label = el2(doc, "span", "knobs-wt-toc-label");
19352
- label.textContent = step.title;
19353
- toc.title = `${step.number} ${step.title}`;
19354
- toc.append(glyph, label);
19355
- toc.addEventListener("click", () => go(step.key));
19356
- tocEl.append(toc);
20418
+ toc.querySelector(".knobs-wt-toc-end").append(glyph);
20419
+ group.append(toc);
19357
20420
  }
19358
20421
  return { step, card, media, notes, toc, glyph, taste, slots };
19359
20422
  };
@@ -19412,15 +20475,21 @@ function walkthrough(container, options) {
19412
20475
  setFull(false);
19413
20476
  return go(next);
19414
20477
  }
20478
+ if ((key === "Home" || key === "End") && views.length) {
20479
+ claim();
20480
+ if (full)
20481
+ setFull(false);
20482
+ return go(views[key === "Home" ? 0 : views.length - 1].step.key);
20483
+ }
19415
20484
  if (key === "ArrowLeft" || key === "ArrowRight") {
19416
20485
  const d = (key === "ArrowLeft" ? -1 : 1) * (e.shiftKey ? 2 : 0.5);
19417
20486
  if (seekBy(d))
19418
20487
  claim();
19419
20488
  return;
19420
20489
  }
19421
- if (key === "n") {
20490
+ if (key === "n" && noteMode !== "hidden") {
19422
20491
  claim();
19423
- bar.input.focus({ preventScroll: true });
20492
+ bar.setOpen(true, true);
19424
20493
  return;
19425
20494
  }
19426
20495
  const v = viewOf(current);
@@ -19499,6 +20568,12 @@ function walkthrough(container, options) {
19499
20568
  win.removeEventListener(type, stopScroll);
19500
20569
  doc.removeEventListener("keydown", onKey);
19501
20570
  unregisterHints();
20571
+ bar.unobserve();
20572
+ pill?.dispose();
20573
+ for (const d of discs)
20574
+ d.dispose();
20575
+ if (padded)
20576
+ padded.el.style.scrollPaddingBottom = padded.was;
19502
20577
  for (const v of views) {
19503
20578
  v.slots.forEach((s) => s.dispose());
19504
20579
  v.taste?.dispose();