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

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-solid.js CHANGED
@@ -577,11 +577,11 @@ var compareCss = `
577
577
  .knobs-cmp-modes[hidden]{display:none}
578
578
  .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;
579
579
  transition:color calc(var(--_k-hover-out) * 1ms)}
580
- .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible){color:var(--_k-fg)}
580
+ .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
581
581
  /* Toggles: icon buttons, the accent's wash when on. */
582
582
  .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;
583
583
  transition:color calc(var(--_k-hover-out) * 1ms),background calc(var(--_k-hover-out) * 1ms)}
584
- .knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash)}
584
+ .knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash);transition-duration:calc(var(--_k-hover-in) * 1ms)}
585
585
  .knobs-ui .knobs-cmp-toggle[aria-pressed="true"]{color:var(--_k-accent-fg);background:var(--_k-accent-soft)}
586
586
  .knobs-cmp-toggle svg{display:block}
587
587
  .knobs-cmp-fill{flex:1}
@@ -595,6 +595,8 @@ var compareCss = `
595
595
  .knobs-cmp-stage{position:relative;flex:1;min-height:96px;overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_ku-cmp-stage);touch-action:pan-y;outline:none}
596
596
  .knobs-compare[data-frame="flat"] .knobs-cmp-stage{border-radius:0}
597
597
  .knobs-compare[data-mode="wipe"] .knobs-cmp-stage{cursor:ew-resize}
598
+ /* Videos: a click on the stage plays and pauses. */
599
+ .knobs-compare[data-video]:not([data-mode="wipe"],[data-zoomed]) .knobs-cmp-stage{cursor:pointer}
598
600
  /* Unzoomed, a finger still scrolls the page up and down; zoomed, the stage takes every gesture. */
599
601
  .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:grab;touch-action:none}
600
602
  .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:grabbing!important}
@@ -604,6 +606,9 @@ var compareCss = `
604
606
  .knobs-cmp-canvas{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
605
607
  .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}
606
608
  .knobs-compare[data-renderer="gpu"] .knobs-cmp-media{opacity:0}
609
+ /* The media comes in with its first frames: a fade and a 4px blur on the default spring. */
610
+ .knobs-cmp-canvas,.knobs-compare[data-renderer="dom"] .knobs-cmp-media{transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
611
+ .knobs-compare:not([data-ready]) :is(.knobs-cmp-canvas,.knobs-cmp-media){opacity:0;filter:blur(4px)}
607
612
  /* Glass: what's behind shows through, blurred, on the knobs tokens. */
608
613
  .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);
609
614
  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)))}
@@ -612,11 +617,17 @@ var compareCss = `
612
617
  white-space:nowrap;pointer-events:none;will-change:transform}
613
618
  .knobs-cmp-label[data-side="before"]{color:var(--_k-fg-3)}
614
619
  .knobs-cmp-label[data-side="after"]{color:var(--_k-fg)}
615
- .knobs-cmp-mark svg{display:block;width:12px;height:12px}
616
- /* The wipe: a hairline down the line with a raised grip at the pointer's height; a wider strip
617
- takes the hand and the keys (a slider). It fades with the mode; wiping, line and grip brighten. */
618
- .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)}
619
- .knobs-cmp-handle[data-on]{opacity:1;pointer-events:auto;transition:opacity .22s cubic-bezier(.2,.8,.2,1)}
620
+ /* Fusing, the mark folds away and the label is its name. */
621
+ .knobs-cmp-mark{flex:none;display:flex;justify-content:flex-end;overflow:hidden}
622
+ .knobs-cmp-mark svg{display:block;flex:none;width:12px;height:12px}
623
+ /* The fuse's seam: a blurred strip over where the sides meet, soft at both sides. */
624
+ .knobs-cmp-seam{position:absolute;left:0;top:0;width:8px;height:0;opacity:0;pointer-events:none;will-change:transform,opacity;
625
+ -webkit-mask-image:linear-gradient(to right,transparent,#000 35%,#000 65%,transparent);mask-image:linear-gradient(to right,transparent,#000 35%,#000 65%,transparent)}
626
+ /* The wipe: a hairline down the line with a raised grip in its middle (at the pointer's height
627
+ while dragged); a wider strip takes the hand and the keys (a slider). It fades in with the
628
+ fuse; wiping, line and grip brighten. */
629
+ .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,opacity}
630
+ .knobs-cmp-handle[data-on]{pointer-events:auto}
620
631
  .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)}
621
632
  .knobs-compare[data-dragging] .knobs-cmp-handle::before{background:var(--_ku-cmp-line)}
622
633
  .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);
@@ -636,7 +647,7 @@ var compareCss = `
636
647
  .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)}
637
648
  :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;
638
649
  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}
639
- :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)}
650
+ :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);transition-duration:calc(var(--_k-hover-in) * 1ms)}
640
651
  .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}
641
652
  .knobs-cmp-note:empty{display:none}
642
653
  .knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
@@ -648,6 +659,31 @@ var compareCss = `
648
659
  @media(prefers-reduced-motion:reduce){:is(.knobs-compare,.knobs-cmp-strip) :where(*,::before,::after){transition-duration:.12s!important}}
649
660
  `;
650
661
 
662
+ // src/ui/player-css.ts
663
+ var playerCss = `
664
+ :where(.knobs-player,.knobs-player *){box-sizing:border-box}
665
+ .knobs-ui.knobs-player{--_kp-stage:var(--_k-compare-stage);display:flex;flex-direction:column;gap:6px;width:100%;outline:none;
666
+ font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
667
+ :where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-ui.knobs-player,.knobs-ui.knobs-player[data-nested]{--_kp-stage:var(--k-compare-stage,transparent)}
668
+ .knobs-player-frame{position:relative;width:100%;aspect-ratio:var(--kp-aspect,16/9);overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_kp-stage);cursor:pointer}
669
+ .knobs-player[data-frame="flat"] .knobs-player-frame{border-radius:0}
670
+ .knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
671
+ opacity:0;filter:blur(4px);transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
672
+ .knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}
673
+ .knobs-player-glyph{position:absolute;left:50%;top:50%;display:grid;place-items:center;width:44px;height:44px;margin:-22px 0 0 -22px;border-radius:50%;color:var(--_k-fg);pointer-events:none;
674
+ opacity:0;filter:blur(4px);scale:.96;
675
+ 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);
676
+ box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim);
677
+ transition:opacity calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy),filter calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy),scale calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy)}
678
+ .knobs-player-glyph svg{width:16px;height:16px}
679
+ .knobs-player-glyph[data-flash]{opacity:1;filter:none;scale:1}
680
+ .knobs-player-line{flex:none;min-height:0}
681
+ /* Hover in is immediate (0 ms), out eases. The timeline's own stylesheet is look.ts's; its hover
682
+ states are set here, after it, so the player's line and every timeline share them. */
683
+ .knobs-ui .knobs-tl-play:hover,.knobs-tl-edge:hover::after,.knobs-tl-rlabel:hover,.knobs-tl-track:hover .knobs-tl-hover,
684
+ .knobs-timeline[data-variant="line"] .knobs-tl-track:hover .knobs-tl-segs{transition-duration:calc(var(--_k-hover-in) * 1ms)}
685
+ `;
686
+
651
687
  // src/ui/overlay-css.ts
652
688
  var overlayCss = `
653
689
  .knobs-layers{position:fixed;inset:0;z-index:2147480005;pointer-events:none}
@@ -1512,7 +1548,7 @@ var init_schema = __esm(() => {
1512
1548
  type: "length",
1513
1549
  group: "Type",
1514
1550
  source: value("20px"),
1515
- doc: "Section headings (a PR's title, under its kicker), weight 500."
1551
+ doc: "A page's title, weight 500."
1516
1552
  },
1517
1553
  {
1518
1554
  name: "leading-heading",
@@ -1528,7 +1564,7 @@ var init_schema = __esm(() => {
1528
1564
  type: "length",
1529
1565
  group: "Type",
1530
1566
  source: value("16px"),
1531
- doc: "A page's title, weight 500."
1567
+ doc: "Section titles (under their kicker), weight 500."
1532
1568
  },
1533
1569
  {
1534
1570
  name: "leading-title",
@@ -1560,7 +1596,7 @@ var init_schema = __esm(() => {
1560
1596
  type: "length",
1561
1597
  group: "Type",
1562
1598
  source: value("12px"),
1563
- doc: "Labels over media, weight 500; mono inside body text."
1599
+ doc: "Mono inside body text: code and paths."
1564
1600
  },
1565
1601
  {
1566
1602
  name: "leading-label",
@@ -3003,8 +3039,6 @@ var css2 = `
3003
3039
  .knobs-tl-hover{position:absolute;left:0;top:0;bottom:0;width:1px;margin-left:-.5px;background:var(--_k-border-strong);transform:translateX(-100px);pointer-events:none;opacity:0;transition:opacity .15s}
3004
3040
  .knobs-tl-track:hover .knobs-tl-hover{opacity:1}
3005
3041
  .knobs-timeline[data-scrubbing] .knobs-tl-hover{opacity:0}
3006
- @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
3007
- @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
3008
3042
  .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}
3009
3043
  .knobs-tl-tag[data-on]{opacity:1}
3010
3044
  /* The lane (variant: "lane"): one slim track of ticks under a compare or a video; markers are
@@ -3020,7 +3054,26 @@ var css2 = `
3020
3054
  .knobs-timeline[data-variant="lane"] .knobs-tl-head::before{top:0}
3021
3055
  .knobs-timeline[data-variant="lane"] .knobs-tl-head::after{content:none}
3022
3056
  .knobs-timeline[data-variant="lane"] .knobs-tl-tag{top:2px;bottom:auto}
3023
- `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + selectionPillCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
3057
+ /* The line (variant: "line"): a player's progress under its media. One 2px line, fg-3 with fg
3058
+ filling it, a segment per chapter; it thickens under the pointer and while scrubbing. The
3059
+ chapter and time under the pointer float over it; nothing else shows. */
3060
+ .knobs-ui.knobs-timeline[data-variant="line"]{height:auto;min-height:0;padding:0;gap:0}
3061
+ .knobs-timeline[data-variant="line"] .knobs-tl-transport{display:none}
3062
+ .knobs-ui.knobs-timeline[data-variant="line"] .knobs-tl-track{height:16px;overflow:visible;border-radius:var(--_k-radius);background:transparent;box-shadow:none;cursor:pointer}
3063
+ .knobs-timeline[data-variant="line"] :is(.knobs-tl-ruler,.knobs-tl-lane,.knobs-tl-hover,.knobs-tl-head,.knobs-tl-rlabel){display:none}
3064
+ .knobs-tl-segs{position:absolute;left:0;right:0;top:50%;height:2px;margin-top:-1px;pointer-events:none;transition:scale calc(var(--_k-hover-out) * 1ms) var(--_ku-ease)}
3065
+ .knobs-tl-seg{position:absolute;left:0;top:0;height:100%;overflow:hidden;border-radius:1px;background:var(--_k-fg-3);will-change:transform}
3066
+ .knobs-tl-seg-fill{position:absolute;inset:0;background:var(--_k-fg);transform-origin:0 0;will-change:transform}
3067
+ .knobs-timeline[data-variant="line"] .knobs-tl-track:hover .knobs-tl-segs,.knobs-timeline[data-variant="line"][data-scrubbing] .knobs-tl-segs{scale:1 2}
3068
+ .knobs-timeline[data-variant="line"] .knobs-tl-range{top:3px;bottom:3px;border-radius:3px}
3069
+ .knobs-timeline[data-variant="line"]:focus-visible .knobs-tl-track,.knobs-timeline[data-variant="line"][data-keyboard]:focus .knobs-tl-track{background:transparent;box-shadow:0 0 0 2px color-mix(in srgb,var(--_k-accent) 60%,transparent)}
3070
+ .knobs-timeline[data-variant="line"] .knobs-tl-tag{top:auto;bottom:calc(100% + 2px);display:flex;gap:6px;padding:0 6px;border-radius:calc(var(--_k-radius) * 999);background:color-mix(in srgb,var(--_k-bg) 72%,transparent);
3071
+ -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:inset 0 0 0 .5px var(--_k-border)}
3072
+ .knobs-tl-tag-label{color:var(--_k-fg-2)}
3073
+ .knobs-tl-tag-label:empty{display:none}
3074
+ @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
3075
+ @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
3076
+ `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + playerCss + selectionPillCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
3024
3077
  s.omega = m.omega;
3025
3078
  s.zeta = m.zeta;
3026
3079
  return s;
@@ -3143,19 +3196,19 @@ var init_diff_css = __esm(() => {
3143
3196
  @property --kd-fb{syntax:"<length>";inherits:false;initial-value:0px}
3144
3197
  :where(.knobs-ui){--kd-add:var(--_k-diff-add);--kd-del:var(--_k-diff-del);--kd-mod:var(--_k-diff-mod);--kd-move:var(--k-diff-move,var(--_k-info))}
3145
3198
  :where(.knobs-diff,.knobs-diff *,.knobs-diff-files,.knobs-diff-files *){box-sizing:border-box}
3146
- .knobs-ui.knobs-diff{--kd-bg:var(--k-diff-bg,var(--_kd-bg,var(--_k-bg)));--kd-solid:var(--kd-fill,var(--kd-bg));--kd-line:calc(var(--_k-font-size) * 1.5);
3199
+ .knobs-ui.knobs-diff{--kd-bg:var(--k-diff-bg,var(--_kd-bg,transparent));--kd-solid:var(--kd-fill,var(--kd-bg));--kd-line:calc(var(--_k-font-size) * 1.5);
3147
3200
  --kd-i:var(--k-diff-inset,8px);--kd-x:var(--kd-i);--kd-inner:max(3px,calc(var(--_ku-pane-radius) - var(--kd-i)));--kd-ctl:var(--kd-inner);
3148
3201
  --kd-head:calc(var(--_k-control-h) + 2 * var(--kd-i));--kd-fade:var(--k-diff-fade,20px);--kd-stick:0px;position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;
3149
3202
  background:var(--kd-bg);border-radius:var(--_ku-pane-radius);font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);text-align:left;
3150
3203
  letter-spacing:normal;word-spacing:normal;text-transform:none;-webkit-font-smoothing:antialiased}
3151
3204
  .knobs-ui.knobs-diff[data-height="auto"]{height:auto}
3152
- .knobs-diff::after{content:"";position:absolute;inset:0;z-index:3;border-radius:inherit;box-shadow:var(--_ku-pane-ring);pointer-events:none}
3205
+ .knobs-ui.knobs-diff:not([data-on="pane"])::after{content:"";position:absolute;inset:0;z-index:3;box-shadow:inset 0 -1px 0 var(--_k-border-subtle);pointer-events:none}
3153
3206
  .knobs-diff [hidden],.knobs-diff-files [hidden]{display:none!important}
3154
3207
  .knobs-diff svg{display:block;flex:none}
3155
- .knobs-ui.knobs-diff[data-on="surface"]{--kd-x:0px;--kd-head:var(--_k-control-h);--kd-ctl:var(--_k-radius)}
3156
- /* The header's hairline shows only once the code has scrolled under it. */
3208
+ .knobs-ui.knobs-diff:not([data-on="pane"]){--kd-x:0px;--kd-head:calc(var(--_k-control-h) + var(--kd-i));--kd-ctl:var(--_k-radius)}
3209
+ /* A hairline under the header, firmer once the code has scrolled under it. */
3157
3210
  .knobs-diff-head{position:relative;z-index:1;flex:none;display:flex;align-items:center;gap:8px;height:var(--kd-head);padding:0 var(--kd-x);
3158
- box-shadow:inset 0 -1px transparent;transition:box-shadow .15s;user-select:none;-webkit-user-select:none}
3211
+ box-shadow:inset 0 -1px var(--_k-border-subtle);transition:box-shadow .15s;user-select:none;-webkit-user-select:none}
3159
3212
  .knobs-diff[data-scrolled]>.knobs-diff-head{box-shadow:inset 0 -1px var(--_k-border)}
3160
3213
  .knobs-diff-path{flex:0 1 auto;min-width:0;overflow:hidden;white-space:nowrap}
3161
3214
  .knobs-diff-dir,.knobs-diff-from{color:var(--_k-fg-3)}
@@ -3182,17 +3235,26 @@ var init_diff_css = __esm(() => {
3182
3235
  margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
3183
3236
  .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;
3184
3237
  transition:background .15s,color .15s,opacity .15s}
3185
- .knobs-ui .knobs-diff-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
3238
+ .knobs-ui .knobs-diff-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
3186
3239
  .knobs-ui .knobs-diff-btn[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
3187
3240
  .knobs-ui .knobs-diff-btn:disabled{opacity:.35;cursor:default;background:transparent}
3188
3241
  .knobs-diff-btn svg{width:16px;height:16px}
3189
3242
  /* Viewed: a check with its word, as on GitHub; the path dims once it's ticked. */
3190
3243
  .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);
3191
3244
  font-size:var(--_k-text-meta);font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:background .15s,color .15s}
3192
- .knobs-ui .knobs-diff-viewed:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
3245
+ .knobs-ui .knobs-diff-viewed:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
3193
3246
  .knobs-ui .knobs-diff-viewed[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
3194
3247
  .knobs-diff-viewed svg{width:16px;height:16px}
3195
3248
  .knobs-diff-viewed[aria-pressed="true"] svg{color:var(--kd-add)}
3249
+ /* Narrow, the header gives up words before anything is cut off at its end: Viewed keeps its
3250
+ check, then the counts' bar goes, then the counts; the path fades at its end throughout. */
3251
+ .knobs-diff-head,.knobs-diff-part-head{container-type:inline-size}
3252
+ @container (max-width:520px){
3253
+ .knobs-diff-viewed-label{display:none}
3254
+ .knobs-ui .knobs-diff-viewed{width:var(--_k-control-h);padding:0;justify-content:center}
3255
+ .knobs-diff-counts .knobs-diff-bar{display:none}
3256
+ }
3257
+ @container (max-width:400px){.knobs-diff-counts{display:none}.knobs-diff-nav>span{min-width:2.6em}}
3196
3258
  .knobs-diff-path{transition:opacity .2s}
3197
3259
  .knobs-diff[data-viewed]>.knobs-diff-head .knobs-diff-path,.knobs-diff-part[data-viewed] .knobs-diff-path{opacity:.6}
3198
3260
  /* Split | Unified: the selection pill on the one picked, its bright copy the picked one's text; a
@@ -3200,7 +3262,7 @@ var init_diff_css = __esm(() => {
3200
3262
  .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)}
3201
3263
  .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);
3202
3264
  font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:color calc(var(--_k-hover-out) * 1ms)}
3203
- .knobs-ui .knobs-diff-choice:is(:hover,:focus-visible){color:var(--_k-fg)}
3265
+ .knobs-ui .knobs-diff-choice:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
3204
3266
  /* The diff: one layer per file or layout on show, the old fading under the new. */
3205
3267
  .knobs-diff-body{position:relative;flex:1;min-height:0}
3206
3268
  .knobs-diff-probe{position:absolute;left:0;top:0;width:0;height:var(--kd-line);visibility:hidden;pointer-events:none}
@@ -3221,8 +3283,8 @@ var init_diff_css = __esm(() => {
3221
3283
  .knobs-diff-hbar{position:absolute;height:5px;pointer-events:none}
3222
3284
  .knobs-diff-hbar>i{position:absolute;left:0;top:0;height:5px;border-radius:3px;background:color-mix(in srgb,var(--_k-fg) 30%,transparent);opacity:0;pointer-events:none;
3223
3285
  transition:opacity .3s,background .15s}
3224
- .knobs-diff-body:hover .knobs-diff-hbar>i,.knobs-diff-hbar:is([data-active],[data-dragging])>i{opacity:1;pointer-events:auto;transition-duration:.12s,.15s}
3225
- .knobs-diff-hbar>i:hover,.knobs-diff-hbar[data-dragging]>i{background:color-mix(in srgb,var(--_k-fg) 48%,transparent)}
3286
+ .knobs-diff-body:hover .knobs-diff-hbar>i,.knobs-diff-hbar:is([data-active],[data-dragging])>i{opacity:1;pointer-events:auto;transition-duration:calc(var(--_k-hover-in) * 1ms)}
3287
+ .knobs-diff-hbar>i:hover,.knobs-diff-hbar[data-dragging]>i{background:color-mix(in srgb,var(--_k-fg) 48%,transparent);transition-duration:calc(var(--_k-hover-in) * 1ms)}
3226
3288
  /* stack: every file down one scroller, each header sticking until the next pushes it off. It's
3227
3289
  flush (square, no card around its file), opaque in the code's own colour, with a hairline
3228
3290
  under it only while it's stuck; a gap, not a line, parts one file from the next. */
@@ -3257,24 +3319,26 @@ var init_diff_css = __esm(() => {
3257
3319
  --diffs-bg-deletion-number-override:color-mix(in srgb,var(--kd-del) 55%,var(--kd-solid));
3258
3320
  --diffs-bg-addition-emphasis-override:color-mix(in srgb,var(--kd-add) 22%,transparent);
3259
3321
  --diffs-bg-deletion-emphasis-override:color-mix(in srgb,var(--kd-del) 22%,transparent);
3260
- --diffs-bg-separator-override:color-mix(in srgb,var(--_k-fg) 4.5%,var(--kd-solid));
3261
- --diffs-bg-context-override:color-mix(in srgb,var(--_k-fg) 3%,var(--kd-solid));
3262
- --diffs-bg-buffer-override:color-mix(in srgb,var(--_k-fg) 3.5%,var(--kd-solid));
3322
+ /* Unchanged lines and the folds between hunks sit on the void; only a split's missing lines
3323
+ are faintly filled. */
3324
+ --diffs-bg-separator-override:var(--kd-solid);
3325
+ --diffs-bg-context-override:var(--kd-solid);
3326
+ --diffs-bg-buffer-override:color-mix(in srgb,var(--_k-fg) 2.5%,var(--kd-solid));
3263
3327
  --diffs-bg-selection-override:var(--_k-accent);
3264
3328
  --diffs-fg-number-override:var(--_k-fg-3);
3265
3329
  }
3266
- /* diffReview: one card, the file list a drawer at its left (its width springs; the list inside
3267
- is pinned to its right edge, so it slides out from under the diff), parted from the diff by a
3268
- hairline. The diff in it draws no card of its own. */
3269
- .knobs-ui.knobs-diff-review{position:relative;display:flex;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;border-radius:var(--_ku-pane-radius);background:var(--_ku-pane-bg);
3330
+ /* diffReview, on the void as the diff is: the file list a drawer at its left (its width springs;
3331
+ the list inside is pinned to its right edge, so it slides out from under the diff), parted
3332
+ from the diff by a hairline; one hairline runs under both headers and one under it all. */
3333
+ .knobs-ui.knobs-diff-review{position:relative;display:flex;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;
3270
3334
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);text-align:left;letter-spacing:normal;-webkit-font-smoothing:antialiased}
3271
- .knobs-diff-review::after{content:"";position:absolute;inset:0;z-index:5;border-radius:inherit;box-shadow:var(--_ku-pane-ring);pointer-events:none}
3335
+ .knobs-diff-review::after{content:"";position:absolute;inset:0;z-index:5;box-shadow:inset 0 -1px 0 var(--_k-border-subtle);pointer-events:none}
3272
3336
  :where(.knobs-diff-review,.knobs-diff-review *){box-sizing:border-box}
3273
- .knobs-diff-drawer{position:relative;flex:none;width:0;height:100%;overflow:hidden;box-shadow:inset -1px 0 var(--_k-border)}
3337
+ .knobs-diff-drawer{position:relative;flex:none;width:0;height:100%;overflow:hidden;box-shadow:inset -1px 0 var(--_k-border-subtle)}
3274
3338
  .knobs-diff-drawer[data-shut]{visibility:hidden}
3275
3339
  .knobs-diff-drawer-in{position:absolute;top:0;bottom:0;right:0;display:flex;flex-direction:column}
3276
3340
  .knobs-diff-drawer-head{flex:none;display:flex;align-items:center;gap:6px;height:calc(var(--_k-control-h) + 2 * var(--k-diff-inset,8px));padding:0 var(--k-diff-inset,8px) 0 calc(var(--_ku-row-x) + 8px);
3277
- user-select:none;-webkit-user-select:none}
3341
+ box-shadow:inset 0 -1px var(--_k-border-subtle);user-select:none;-webkit-user-select:none}
3278
3342
  .knobs-diff-drawer-title{font-weight:var(--_k-weight-title)}
3279
3343
  .knobs-diff-drawer-count{color:var(--_k-fg-3);font-size:var(--_k-text-meta);font-variant-numeric:tabular-nums}
3280
3344
  .knobs-diff-drawer-in>.knobs-diff-files{flex:1;min-height:0;height:auto}
@@ -3286,15 +3350,16 @@ var init_diff_css = __esm(() => {
3286
3350
  .knobs-ui .knobs-diff-toggle[aria-pressed]:not(:hover,:focus-visible){background:transparent}
3287
3351
  /* diffReview's file list: a filter over a compact tree, a status letter, the counts and their
3288
3352
  bar on each file; one highlight glides to the file on show. Viewed files dim, with a tick. */
3289
- .knobs-diff-files{--k-row:var(--k-diff-row,28px);display:flex;flex-direction:column;height:100%;min-height:0;padding-top:2px}
3353
+ .knobs-diff-files{--k-row:var(--k-diff-row,28px);display:flex;flex-direction:column;height:100%;min-height:0;padding-top:4px}
3290
3354
  .knobs-diff-filter{position:relative;flex:none;display:flex;align-items:center;gap:6px;height:26px;margin:0 var(--_ku-row-x) 4px;padding:0 4px 0 8px;
3291
- border-radius:max(6px,var(--_ku-row-radius));background:var(--_ku-wash);color:var(--_k-fg-3);box-shadow:inset 0 0 0 1px transparent;transition:background .15s,box-shadow .15s}
3292
- .knobs-diff-filter:focus-within{background:var(--_ku-wash-hi);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_k-accent) 45%,transparent)}
3355
+ border-radius:max(6px,var(--_ku-row-radius));color:var(--_k-fg-3);box-shadow:inset 0 0 0 1px transparent;transition:box-shadow .15s}
3356
+ /* The filter is a line of text, no box; focused, a hairline ring. */
3357
+ .knobs-diff-filter:focus-within{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_k-accent) 45%,transparent)}
3293
3358
  .knobs-diff-filter svg{width:12px;height:12px;flex:none}
3294
3359
  .knobs-ui .knobs-diff-filter>input{flex:1;min-width:0;height:100%;font:var(--_k-weight) var(--_k-font-size)/1 var(--_k-font);color:var(--_k-fg);caret-color:var(--_k-accent);background:transparent}
3295
3360
  .knobs-ui .knobs-diff-filter>input::placeholder{color:var(--_k-fg-3)}
3296
3361
  .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}
3297
- .knobs-ui .knobs-diff-filter>button:hover{background:var(--_ku-wash-hi);color:var(--_k-fg)}
3362
+ .knobs-ui .knobs-diff-filter>button:hover{background:var(--_ku-wash-hi);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
3298
3363
  .knobs-diff-filter>button svg{width:12px;height:12px}
3299
3364
  .knobs-diff-list{position:relative;flex:1;min-height:0}
3300
3365
  .knobs-diff-files .knobs-diff-counts{font-size:var(--_k-text-meta);align-items:center}
@@ -6214,7 +6279,10 @@ function codeDiff(container, options) {
6214
6279
  const viewedToggle = () => {
6215
6280
  const b = make("button", "knobs-diff-viewed");
6216
6281
  b.type = "button";
6217
- b.append(make("span", ""), "Viewed");
6282
+ const label = make("span", "knobs-diff-viewed-label");
6283
+ label.textContent = "Viewed";
6284
+ b.append(make("span", ""), label);
6285
+ b.setAttribute("aria-label", "Viewed");
6218
6286
  return b;
6219
6287
  };
6220
6288
  const paintViewed = (b, on) => {
@@ -8501,6 +8569,17 @@ function currentMarker(markers, t, fps) {
8501
8569
  best = m;
8502
8570
  return best;
8503
8571
  }
8572
+ function minutes(t) {
8573
+ const s = Math.max(0, Math.floor(t + 0.000001));
8574
+ const h = Math.floor(s / 3600);
8575
+ const m = Math.floor(s % 3600 / 60);
8576
+ const ss = String(s % 60).padStart(2, "0");
8577
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
8578
+ }
8579
+ function chapterBounds(times, duration) {
8580
+ const inside = times.filter((t) => t > 0.000001 && t < duration - 0.000001);
8581
+ return [0, ...[...new Set(inside)].sort((a, b) => a - b), duration];
8582
+ }
8504
8583
  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;
8505
8584
  var init_timeline_model = __esm(() => {
8506
8585
  SECOND_STEPS = [0.1, 0.2, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600];
@@ -8518,7 +8597,8 @@ function timeline(container, options) {
8518
8597
  let speed = options.speed ?? 1;
8519
8598
  let repeat = options.repeat ?? true;
8520
8599
  const arrows = options.arrows ?? "frames";
8521
- const lane = options.variant === "lane";
8600
+ const line = options.variant === "line";
8601
+ const lane = line || options.variant === "lane";
8522
8602
  let time = Math.min(duration, Math.max(0, options.time ?? 0));
8523
8603
  let playing = false;
8524
8604
  let markers = [...options.markers ?? []];
@@ -8553,7 +8633,7 @@ function timeline(container, options) {
8553
8633
  root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight , . Home End [ ] I O M");
8554
8634
  applyLook2(root, options);
8555
8635
  if (lane)
8556
- root.dataset.variant = "lane";
8636
+ root.dataset.variant = line ? "line" : "lane";
8557
8637
  const transport = el("div", "knobs-tl-transport");
8558
8638
  const playBtn = el("button", "knobs-tl-play");
8559
8639
  playBtn.type = "button";
@@ -8566,6 +8646,10 @@ function timeline(container, options) {
8566
8646
  const hover = el("div", "knobs-tl-hover");
8567
8647
  const tag = el("span", "knobs-tl-tag");
8568
8648
  tag.setAttribute("aria-hidden", "true");
8649
+ const tagLabel = el("span", "knobs-tl-tag-label");
8650
+ const tagTime = el("span", "knobs-tl-tag-time");
8651
+ tag.append(tagLabel, tagTime);
8652
+ const segs = el("div", "knobs-tl-segs");
8569
8653
  const head = el("div", "knobs-tl-head");
8570
8654
  head.id = `${id}-head`;
8571
8655
  head.setAttribute("role", "slider");
@@ -8573,11 +8657,13 @@ function timeline(container, options) {
8573
8657
  head.setAttribute("aria-valuemin", "0");
8574
8658
  head.setAttribute("aria-valuemax", String(duration));
8575
8659
  track.append(canvas, markerLane, hover, head, tag);
8660
+ if (line)
8661
+ track.insertBefore(segs, markerLane);
8576
8662
  root.append(transport, track);
8577
8663
  container.append(root);
8578
8664
  let width = 1;
8579
8665
  let height = 1;
8580
- const fitView = () => clampView({ start: 0, span: duration }, duration, { pad: 0.02 });
8666
+ const fitView = () => clampView({ start: 0, span: duration }, duration, { pad: line ? 0 : 0.02 });
8581
8667
  let target = fitView();
8582
8668
  const vStart = new Spring(target.start, INSTANT2.omega, 1, 0.00001);
8583
8669
  const vSpan = new Spring(target.span, INSTANT2.omega, 1, 0.00001);
@@ -8586,13 +8672,13 @@ function timeline(container, options) {
8586
8672
  const xOf = (t) => (t - vStart.x) * pxPerSec();
8587
8673
  const tOf = (x) => vStart.x + x / pxPerSec();
8588
8674
  const minSpan = () => Math.max(4 / fps, 0.05);
8589
- const retarget = (next, motion = INSTANT2) => {
8590
- target = clampView(next, duration, { pad: 0.04, minSpan: minSpan() });
8675
+ const retarget = (next, motion = INSTANT2, hand = false) => {
8676
+ target = clampView(next, duration, { pad: line ? 0 : 0.04, minSpan: minSpan() });
8591
8677
  for (const s of [vStart, vSpan]) {
8592
8678
  s.omega = motion.omega;
8593
8679
  s.zeta = motion.zeta;
8594
8680
  }
8595
- if (reducedMotion(win) && motion === CALM) {
8681
+ if (hand || reducedMotion(win) && motion === CALM) {
8596
8682
  vStart.snap(target.start);
8597
8683
  vSpan.snap(target.span);
8598
8684
  } else {
@@ -8618,6 +8704,8 @@ function timeline(container, options) {
8618
8704
  };
8619
8705
  let drawn = "";
8620
8706
  const drawRuler = () => {
8707
+ if (line)
8708
+ return;
8621
8709
  const v = view();
8622
8710
  const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${height}:${units}:${fps}`;
8623
8711
  if (key === drawn)
@@ -8705,6 +8793,8 @@ function timeline(container, options) {
8705
8793
  n.title = `${m.label ? `${m.label} · ` : ""}${clock(m.time)}`;
8706
8794
  }
8707
8795
  placeMarkers();
8796
+ if (line)
8797
+ syncSegments();
8708
8798
  };
8709
8799
  const placeMarkers = () => {
8710
8800
  for (const m of markers) {
@@ -8713,6 +8803,36 @@ function timeline(container, options) {
8713
8803
  n.style.transform = `translateX(${xOf(m.time).toFixed(2)}px)`;
8714
8804
  }
8715
8805
  };
8806
+ let bounds = [];
8807
+ const segEls = [];
8808
+ const syncSegments = () => {
8809
+ bounds = chapterBounds(markers.map((m) => m.time), duration);
8810
+ while (segEls.length > bounds.length - 1)
8811
+ segEls.pop().el.remove();
8812
+ while (segEls.length < bounds.length - 1) {
8813
+ const n = el("i", "knobs-tl-seg");
8814
+ const fill = el("b", "knobs-tl-seg-fill");
8815
+ n.append(fill);
8816
+ segs.append(n);
8817
+ segEls.push({ el: n, fill });
8818
+ }
8819
+ placeSegments();
8820
+ };
8821
+ const placeSegments = () => {
8822
+ if (!line)
8823
+ return;
8824
+ const last = segEls.length - 1;
8825
+ segEls.forEach((s, i) => {
8826
+ const a = bounds[i];
8827
+ const b = bounds[i + 1];
8828
+ const x0 = xOf(a) + (i > 0 ? SEG_GAP / 2 : 0);
8829
+ const x1 = xOf(b) - (i < last ? SEG_GAP / 2 : 0);
8830
+ s.el.style.transform = `translateX(${x0.toFixed(2)}px)`;
8831
+ s.el.style.width = `${Math.max(0, x1 - x0).toFixed(2)}px`;
8832
+ const k = Math.min(1, Math.max(0, (shownHead - a) / Math.max(0.000000001, b - a)));
8833
+ s.fill.style.transform = `scaleX(${k.toFixed(4)})`;
8834
+ });
8835
+ };
8716
8836
  const syncRanges = () => {
8717
8837
  const keep = new Set(ranges.map((r) => r.id));
8718
8838
  for (const [k, n] of rangeEls)
@@ -8729,7 +8849,7 @@ function timeline(container, options) {
8729
8849
  b.dataset.edge = "end";
8730
8850
  const label = el("span", "knobs-tl-rlabel");
8731
8851
  n.append(label, a, b);
8732
- track.insertBefore(n, markerLane);
8852
+ track.insertBefore(n, line ? segs : markerLane);
8733
8853
  rangeEls.set(r.id, n);
8734
8854
  if (built)
8735
8855
  enter(n);
@@ -8781,6 +8901,7 @@ function timeline(container, options) {
8781
8901
  let currentId = null;
8782
8902
  const renderHead = () => {
8783
8903
  head.style.transform = `translateX(${xOf(shownHead).toFixed(2)}px)`;
8904
+ placeSegments();
8784
8905
  head.setAttribute("aria-valuenow", time.toFixed(3));
8785
8906
  head.setAttribute("aria-valuetext", `${time.toFixed(2)} seconds, frame ${toFrame(time, fps)}`);
8786
8907
  const cur = currentMarker(markers, time, fps)?.id ?? null;
@@ -9044,14 +9165,25 @@ function timeline(container, options) {
9044
9165
  setTime(s.t, "scrub", s.shown, press);
9045
9166
  root.toggleAttribute("data-stuck", s.stop !== null);
9046
9167
  };
9168
+ let pointerX = null;
9169
+ const zoomAtHand = (factor) => zoom(factor, pointerX === null ? time : target.start + pointerX / width * target.span);
9047
9170
  const showTag = (x) => {
9048
9171
  const at = Math.min(duration, Math.max(0, tOf(x)));
9049
- const text = `${at.toFixed(2)} s`;
9050
- if (tag.textContent !== text)
9051
- tag.textContent = text;
9172
+ const text = line ? minutes(at) : `${at.toFixed(2)} s`;
9173
+ if (tagTime.textContent !== text)
9174
+ tagTime.textContent = text;
9175
+ const label = line ? currentMarker(markers, at, fps)?.label ?? "" : "";
9176
+ if (tagLabel.textContent !== label)
9177
+ tagLabel.textContent = label;
9052
9178
  const cx = Math.min(width, Math.max(0, x));
9053
- const flip = cx > width - tag.offsetWidth - 12;
9054
- tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
9179
+ if (line) {
9180
+ const w = tag.offsetWidth;
9181
+ const left = Math.min(Math.max(0, width - w), Math.max(0, cx - w / 2));
9182
+ tag.style.transform = `translateX(${left.toFixed(1)}px)`;
9183
+ } else {
9184
+ const flip = cx > width - tag.offsetWidth - 12;
9185
+ tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
9186
+ }
9055
9187
  tag.dataset.on = "";
9056
9188
  };
9057
9189
  const hideTag = () => delete tag.dataset.on;
@@ -9112,6 +9244,7 @@ function timeline(container, options) {
9112
9244
  });
9113
9245
  track.addEventListener("pointermove", (e) => {
9114
9246
  const x = localX(e);
9247
+ pointerX = x;
9115
9248
  hover.style.transform = `translateX(${x.toFixed(1)}px)`;
9116
9249
  if (!drag || drag.kind === "scrub")
9117
9250
  showTag(x);
@@ -9185,6 +9318,7 @@ function timeline(container, options) {
9185
9318
  track.addEventListener("pointercancel", endDrag);
9186
9319
  track.addEventListener("lostpointercapture", endDrag);
9187
9320
  track.addEventListener("pointerleave", () => {
9321
+ pointerX = null;
9188
9322
  if (drag)
9189
9323
  return;
9190
9324
  hover.style.transform = "translateX(-100px)";
@@ -9192,7 +9326,7 @@ function timeline(container, options) {
9192
9326
  });
9193
9327
  track.addEventListener("dblclick", (e) => {
9194
9328
  if (lane)
9195
- return;
9329
+ return fit();
9196
9330
  const markerEl = e.target.closest(".knobs-tl-marker");
9197
9331
  if (markerEl)
9198
9332
  return rename(markerEl.dataset.id);
@@ -9203,18 +9337,40 @@ function timeline(container, options) {
9203
9337
  rename(m.id);
9204
9338
  });
9205
9339
  track.addEventListener("wheel", (e) => {
9206
- if (lane)
9340
+ const zooming = e.ctrlKey || e.metaKey;
9341
+ if (zooming && pinching)
9342
+ return e.preventDefault();
9343
+ const sideways = Math.abs(e.deltaX) > Math.abs(e.deltaY);
9344
+ if (lane && !zooming && !sideways)
9207
9345
  return;
9208
9346
  e.preventDefault();
9209
9347
  const scale = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? height : 1;
9210
- if (e.ctrlKey || e.metaKey) {
9348
+ if (zooming) {
9211
9349
  const at = target.start + localX(e) / width * target.span;
9212
- retarget(zoomView(target, Math.exp(-e.deltaY * scale * (e.ctrlKey ? 0.01 : 0.002)), at));
9350
+ const k = Math.exp(-e.deltaY * scale * (e.ctrlKey ? 0.01 : 0.002));
9351
+ retarget(zoomView(target, k, at), INSTANT2, lane);
9213
9352
  } else {
9214
- const d = (Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY) * scale;
9215
- retarget({ ...target, start: target.start + d / width * target.span });
9353
+ const d = (sideways ? e.deltaX : e.deltaY) * scale;
9354
+ retarget({ ...target, start: target.start + d / width * target.span }, INSTANT2, lane);
9216
9355
  }
9217
9356
  }, { passive: false });
9357
+ let pinchScale = 1;
9358
+ let pinching = false;
9359
+ const onGesture = (e) => {
9360
+ const g = e;
9361
+ e.preventDefault();
9362
+ if (e.type === "gesturestart")
9363
+ pinchScale = 1;
9364
+ pinching = e.type !== "gestureend";
9365
+ if (e.type !== "gesturechange")
9366
+ return;
9367
+ const s = g.scale ?? 1;
9368
+ const x = g.clientX !== undefined ? localX({ clientX: g.clientX }) : width / 2;
9369
+ retarget(zoomView(target, s / pinchScale, target.start + x / width * target.span), INSTANT2, true);
9370
+ pinchScale = s;
9371
+ };
9372
+ for (const type of ["gesturestart", "gesturechange", "gestureend"])
9373
+ track.addEventListener(type, onGesture);
9218
9374
  root.addEventListener("keydown", (e) => {
9219
9375
  if (e.target !== root || e.metaKey || e.ctrlKey) {
9220
9376
  if ((e.metaKey || e.ctrlKey) && (e.key === "=" || e.key === "-" || e.key === "0") && e.target === root) {
@@ -9222,7 +9378,7 @@ function timeline(container, options) {
9222
9378
  if (e.key === "0")
9223
9379
  fit();
9224
9380
  else
9225
- zoom(e.key === "=" ? 1.5 : 1 / 1.5);
9381
+ zoomAtHand(e.key === "=" ? 1.5 : 1 / 1.5);
9226
9382
  }
9227
9383
  return;
9228
9384
  }
@@ -9304,18 +9460,12 @@ function timeline(container, options) {
9304
9460
  return;
9305
9461
  case "=":
9306
9462
  case "+":
9307
- if (lane)
9308
- return;
9309
9463
  claim();
9310
- return zoom(1.5);
9464
+ return zoomAtHand(1.5);
9311
9465
  case "-":
9312
- if (lane)
9313
- return;
9314
9466
  claim();
9315
- return zoom(1 / 1.5);
9467
+ return zoomAtHand(1 / 1.5);
9316
9468
  case "0":
9317
- if (lane)
9318
- return;
9319
9469
  claim();
9320
9470
  return fit();
9321
9471
  }
@@ -9531,7 +9681,7 @@ function timeline(container, options) {
9531
9681
  };
9532
9682
  return api;
9533
9683
  }
9534
- var RULER = 22, INSTANT2, CALM, GLIDE, uid2 = 0, PLAY2 = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.6v8.8c0 .5.5.8.9.5l6.6-4.4c.4-.3.4-.8 0-1L5.9 3.1c-.4-.3-.9 0-.9.5Z" fill="currentColor"/></svg>', PAUSE = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="4" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/><rect x="9.2" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/></svg>';
9684
+ var RULER = 22, SEG_GAP = 3, INSTANT2, CALM, GLIDE, uid2 = 0, PLAY2 = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.6v8.8c0 .5.5.8.9.5l6.6-4.4c.4-.3.4-.8 0-1L5.9 3.1c-.4-.3-.9 0-.9.5Z" fill="currentColor"/></svg>', PAUSE = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="4" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/><rect x="9.2" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/></svg>';
9535
9685
  var init_timeline = __esm(() => {
9536
9686
  init_rgba();
9537
9687
  init_motion();
@@ -9603,14 +9753,28 @@ function compareLayout(box, before, after) {
9603
9753
  const shape = after.w > 0 && after.h > 0 ? after : before;
9604
9754
  const pair = letterbox({ w: shape.w * 2, h: shape.h }, box);
9605
9755
  const half = pair.w / 2;
9756
+ const fused = { x: pair.x + half / 2, y: pair.y, w: half, h: pair.h };
9606
9757
  return {
9607
9758
  side: {
9608
9759
  before: letterbox(before, { x: pair.x, y: pair.y, w: half, h: pair.h }),
9609
9760
  after: letterbox(after, { x: pair.x + half, y: pair.y, w: half, h: pair.h }),
9610
9761
  split: pair.x + half
9611
9762
  },
9612
- overlay: { before: letterbox(before, box), after: letterbox(after, box) }
9613
- };
9763
+ overlay: { before: letterbox(before, fused), after: letterbox(after, fused) }
9764
+ };
9765
+ }
9766
+ function fuseFrame(L, before, after, k, wipe) {
9767
+ const t = Math.min(1, Math.max(0, k));
9768
+ const o = L.overlay;
9769
+ const left = Math.min(o.before.x, o.after.x);
9770
+ const line = wipeX({ x: left, y: 0, w: Math.max(o.before.x + o.before.w, o.after.x + o.after.w) - left, h: 0 }, wipe);
9771
+ const cut = L.side.split + (line - L.side.split) * t;
9772
+ const contact = Math.max(0, Math.min(cut - after.x, before.x + before.w - cut));
9773
+ const feather = Math.min(contact, 1 + FUSE.feather * Math.sin(Math.PI * t));
9774
+ const overlap = before.x + before.w - after.x;
9775
+ const seam = t > 0.001 && overlap >= 0 ? Math.min(1, Math.max(0, overlap / FUSE.seamFade)) * Math.min(1, t * 4) : 0;
9776
+ const blur = t > 0.005 && t < 0.995 ? FUSE.blur * Math.sin(Math.PI * t) : 0;
9777
+ return { cut, feather, seam, blur };
9614
9778
  }
9615
9779
  function clampZoom(r) {
9616
9780
  const w = Math.min(1, Math.max(1 / ZOOM_MAX, r.w));
@@ -9697,7 +9861,18 @@ function stripPeaks(diffs, o = {}) {
9697
9861
  });
9698
9862
  return out;
9699
9863
  }
9700
- 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({
9864
+ 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, FUSE, fuseCorners = (r, k) => {
9865
+ const inner = r * Math.min(1, Math.max(0, k));
9866
+ return { before: { left: r, right: inner }, after: { left: inner, right: r } };
9867
+ }, labelFold = (fold, mark, blur = 3) => {
9868
+ const f = Math.min(1, Math.max(0, fold));
9869
+ const open = 1 - f;
9870
+ return {
9871
+ width: mark * open,
9872
+ opacity: Math.min(1, Math.max(0, (open - 0.2) / 0.8)),
9873
+ blur: blur * Math.min(1, Math.max(0, (f - 0.25) / 0.75))
9874
+ };
9875
+ }, 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
9876
  u: r.u - dx / Math.max(1, base.w) * r.w,
9702
9877
  v: r.v - dy / Math.max(1, base.h) * r.h,
9703
9878
  w: r.w
@@ -9720,6 +9895,7 @@ var init_compare_model = __esm(() => {
9720
9895
  MODE_KEYS = { side: "s", wipe: "w", difference: "d" };
9721
9896
  VIDEO = /\.(mp4|m4v|mov|webm|ogv|mkv)$/i;
9722
9897
  SEEK_IDLE = { seeking: false, pending: null };
9898
+ FUSE = { feather: 32, blur: 8, seamFade: 48 };
9723
9899
  ZOOM_FIT = { u: 0, v: 0, w: 1, h: 1 };
9724
9900
  });
9725
9901
 
@@ -10452,8 +10628,8 @@ async function createCompareGpu(canvas, onLost) {
10452
10628
  onLost();
10453
10629
  };
10454
10630
  lostListeners.add(lost);
10455
- const uniforms = device.createBuffer({ size: 176, usage: 64 | 8 });
10456
- const data = new Float32Array(44);
10631
+ const uniforms = device.createBuffer({ size: 192, usage: 64 | 8 });
10632
+ const data = new Float32Array(48);
10457
10633
  const make = (w, h) => device.createTexture({
10458
10634
  size: [Math.max(1, w), Math.max(1, h)],
10459
10635
  format: "rgba8unorm",
@@ -10526,6 +10702,7 @@ async function createCompareGpu(canvas, onLost) {
10526
10702
  data.set([f.preview[0] ? 1 : 0, f.preview[1] ? 1 : 0, 0, 0], 32);
10527
10703
  r(36, f.beforeClip);
10528
10704
  r(40, f.afterClip);
10705
+ data.set(f.radii, 44);
10529
10706
  device.queue.writeBuffer(uniforms, 0, data);
10530
10707
  const encoder = device.createCommandEncoder();
10531
10708
  const pass = encoder.beginRenderPass({
@@ -10567,6 +10744,7 @@ struct U {
10567
10744
  loupe: vec4f, // x, y, r, zoom (0: none)
10568
10745
  src: vec4f, // Before, After from their previews
10569
10746
  ca: vec4f, cb: vec4f, // where each side may draw
10747
+ radii: vec4f, // the clips' corners: Before left, right; After left, right
10570
10748
  }
10571
10749
  @group(0) @binding(0) var<uniform> u: U;
10572
10750
  @group(0) @binding(1) var A: texture_2d<f32>;
@@ -10588,8 +10766,18 @@ fn cover(p: vec2f, r: vec4f) -> f32 {
10588
10766
  return clamp(d + 0.5, 0.0, 1.0);
10589
10767
  }
10590
10768
 
10591
- // A side's colour where rect r maps it to p, and its coverage inside clip c there.
10592
- fn take(t: texture_2d<f32>, p: vec2f, r: vec4f, c0: vec4f, near: bool, ready: f32) -> vec4f {
10769
+ // Coverage of rect r at p with its left corners rounded rl and its right ones rr.
10770
+ fn coverRound(p: vec2f, r: vec4f, rl: f32, rr: f32) -> f32 {
10771
+ let h = r.zw * 0.5;
10772
+ let q0 = p - (r.xy + h);
10773
+ let rad = min(select(rl, rr, q0.x > 0.0), min(h.x, h.y));
10774
+ let q = abs(q0) - h + vec2f(rad);
10775
+ let d = length(max(q, vec2f(0.0))) + min(max(q.x, q.y), 0.0) - rad;
10776
+ return clamp(0.5 - d, 0.0, 1.0);
10777
+ }
10778
+
10779
+ // A side's colour where rect r maps it to p, and its coverage inside clip c (corners rc) there.
10780
+ fn take(t: texture_2d<f32>, p: vec2f, r: vec4f, c0: vec4f, rc: vec2f, near: bool, ready: f32) -> vec4f {
10593
10781
  let uv = clamp((p - r.xy) / max(r.zw, vec2f(1e-3)), vec2f(0.0), vec2f(1.0));
10594
10782
  var c: vec3f;
10595
10783
  if (near) {
@@ -10598,27 +10786,22 @@ fn take(t: texture_2d<f32>, p: vec2f, r: vec4f, c0: vec4f, near: bool, ready: f3
10598
10786
  } else {
10599
10787
  c = textureSampleLevel(t, smp, uv, 0.0).rgb;
10600
10788
  }
10601
- return vec4f(c, min(cover(p, r), cover(p, c0)) * ready);
10789
+ return vec4f(c, min(cover(p, r), coverRound(p, c0, rc.x, rc.y)) * ready);
10602
10790
  }
10603
10791
 
10604
10792
  fn takeA(p: vec2f, r: vec4f, c0: vec4f, near: bool) -> vec4f {
10605
- if (u.src.x > 0.5) { return take(PA, p, r, c0, near, 1.0); }
10606
- return take(A, p, r, c0, near, u.diff.z);
10793
+ if (u.src.x > 0.5) { return take(PA, p, r, c0, u.radii.xy, near, 1.0); }
10794
+ return take(A, p, r, c0, u.radii.xy, near, u.diff.z);
10607
10795
  }
10608
10796
  fn takeB(p: vec2f, r: vec4f, c0: vec4f, near: bool) -> vec4f {
10609
- if (u.src.y > 0.5) { return take(PB, p, r, c0, near, 1.0); }
10610
- return take(B, p, r, c0, near, u.diff.w);
10797
+ if (u.src.y > 0.5) { return take(PB, p, r, c0, u.radii.zw, near, 1.0); }
10798
+ return take(B, p, r, c0, u.radii.zw, near, u.diff.w);
10611
10799
  }
10612
10800
 
10613
10801
  fn pm(c: vec4f) -> vec4f { return vec4f(c.rgb * c.a, c.a); }
10614
10802
 
10615
10803
  fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
10616
10804
  switch mode {
10617
- case 1u: {
10618
- let s = max(u.wipe.y, 0.5);
10619
- let m = smoothstep(u.wipe.x - s, u.wipe.x + s, p.x);
10620
- return pm(vec4f(mix(a.rgb, b.rgb, m), mix(a.a, b.a, m)));
10621
- }
10622
10805
  case 2u: {
10623
10806
  let d = abs(a.rgb - b.rgb);
10624
10807
  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);
@@ -10627,8 +10810,10 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
10627
10810
  }
10628
10811
  default: {}
10629
10812
  }
10630
- // Side by side: each side in its own place, Before over After where they meet.
10631
- return vec4f(a.rgb * a.a + b.rgb * b.a * (1.0 - a.a), a.a + b.a * (1.0 - a.a));
10813
+ // Side by side, fusing and wiping: Before over After, cut at the seam with its feather.
10814
+ let s = max(u.wipe.y, 0.5);
10815
+ let ka = a.a * (1.0 - smoothstep(u.wipe.x - s, u.wipe.x + s, p.x));
10816
+ return vec4f(a.rgb * ka + b.rgb * b.a * (1.0 - ka), ka + b.a * (1.0 - ka));
10632
10817
  }
10633
10818
 
10634
10819
  @fragment fn fs(@builtin(position) q: vec4f) -> @location(0) vec4f {
@@ -11030,6 +11215,29 @@ var init_compare_strip = __esm(() => {
11030
11215
  DIFF = { w: 32, h: 18 };
11031
11216
  });
11032
11217
 
11218
+ // src/ui/player.ts
11219
+ function playGlyph(doc) {
11220
+ const element = doc.createElement("span");
11221
+ element.className = "knobs-player-glyph";
11222
+ element.setAttribute("aria-hidden", "true");
11223
+ let timer = 0;
11224
+ const win = doc.defaultView;
11225
+ return {
11226
+ element,
11227
+ flash(playing) {
11228
+ element.innerHTML = playing ? PLAY2 : PAUSE;
11229
+ element.setAttribute("data-flash", "");
11230
+ win.clearTimeout(timer);
11231
+ timer = win.setTimeout(() => element.removeAttribute("data-flash"), FLASH_MS);
11232
+ },
11233
+ dispose: () => win.clearTimeout(timer)
11234
+ };
11235
+ }
11236
+ var FLASH_MS = 450;
11237
+ var init_player = __esm(() => {
11238
+ init_timeline();
11239
+ });
11240
+
11033
11241
  // src/ui/compare.ts
11034
11242
  function compare(container, options) {
11035
11243
  const doc = container.ownerDocument;
@@ -11137,6 +11345,8 @@ function compare(container, options) {
11137
11345
  if (name.textContent !== text)
11138
11346
  name.textContent = text;
11139
11347
  };
11348
+ const seam = el("div", "knobs-cmp-seam");
11349
+ seam.setAttribute("aria-hidden", "true");
11140
11350
  const handle = el("div", "knobs-cmp-handle");
11141
11351
  handle.setAttribute("role", "slider");
11142
11352
  handle.setAttribute("aria-label", "Wipe");
@@ -11146,7 +11356,8 @@ function compare(container, options) {
11146
11356
  grip.setAttribute("aria-hidden", "true");
11147
11357
  handle.append(grip);
11148
11358
  const note = el("div", "knobs-cmp-note");
11149
- stage.append(canvas, split, labels[0], labels[1], handle, zoomPill, note);
11359
+ const glyph = playGlyph(doc);
11360
+ stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note, glyph.element);
11150
11361
  root.append(bar, stage);
11151
11362
  const wantsStrip = options.strip === true;
11152
11363
  let tl = bound;
@@ -11161,7 +11372,7 @@ function compare(container, options) {
11161
11372
  root.append(host);
11162
11373
  tl = timeline(host, {
11163
11374
  ...look,
11164
- variant: "lane",
11375
+ variant: "line",
11165
11376
  duration: 1,
11166
11377
  fps: fps(),
11167
11378
  arrows: { step: 0.5, shift: 2 },
@@ -11182,12 +11393,13 @@ function compare(container, options) {
11182
11393
  let toMode = mode;
11183
11394
  const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
11184
11395
  let wipeCatching = false;
11185
- let gripY = null;
11396
+ const gripAt = new Spring(0.5, LAYOUT2.omega, LAYOUT2.zeta, 0.0001);
11186
11397
  let loupeHeld = false;
11187
11398
  let pointer = null;
11188
11399
  let drag = null;
11189
11400
  const touches = new Map;
11190
11401
  let pinch = null;
11402
+ let press = null;
11191
11403
  const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
11192
11404
  const zv = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
11193
11405
  const zw = new Spring(1, ZOOM.omega, ZOOM.zeta, 0.00001);
@@ -11199,6 +11411,7 @@ function compare(container, options) {
11199
11411
  let stageRect = null;
11200
11412
  let accent = [0.49, 0.81, 0.61];
11201
11413
  let labelW = [0, 0];
11414
+ let frameRadius = 0;
11202
11415
  let dirty = true;
11203
11416
  let disposed = false;
11204
11417
  let near = options.lazy === false;
@@ -11474,6 +11687,8 @@ function compare(container, options) {
11474
11687
  for (const s of sides) {
11475
11688
  const st = s.el.style;
11476
11689
  st.clipPath = "";
11690
+ st.maskImage = "";
11691
+ st.setProperty("-webkit-mask-image", "");
11477
11692
  st.mixBlendMode = "";
11478
11693
  st.opacity = "";
11479
11694
  s.shown = null;
@@ -11700,43 +11915,66 @@ function compare(container, options) {
11700
11915
  viewSent = key;
11701
11916
  options.onView({ zoom: clampZoom(z), loop: l });
11702
11917
  };
11703
- const labelsIn = (m, r, wr, wx) => {
11704
- if (m === "wipe") {
11705
- const at = seamLabels(wx, wr.x, wr.x + wr.w, labelW[0], labelW[1], INSET);
11706
- return [at.before, at.after];
11707
- }
11708
- const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * lay.x;
11709
- return [
11710
- { x: r.before.x + INSET, opacity: 1 },
11711
- { x: ax, opacity: 1 }
11712
- ];
11918
+ const differenceWeight = () => {
11919
+ const f = Math.min(1, Math.max(0, fade.x));
11920
+ return (fromMode === "difference" ? 1 - f : 0) + (toMode === "difference" ? f : 0);
11713
11921
  };
11714
11922
  const placeGrip = (wr) => {
11715
- const gy = gripTop(gripY ?? wr.y + wr.h / 2, wr, GRIP.h) - wr.y;
11923
+ const gy = gripTop(wr.y + gripAt.x * wr.h, wr, GRIP.h) - wr.y;
11716
11924
  grip.style.transform = `translate3d(0,${gy.toFixed(2)}px,0)`;
11717
11925
  };
11926
+ let folded = 0;
11718
11927
  const draw = () => {
11719
11928
  dirty = false;
11929
+ if (!root.hasAttribute("data-ready") && sides.every((x) => x.shown || x.failed))
11930
+ root.setAttribute("data-ready", "");
11720
11931
  const r = layoutNow();
11721
11932
  const wr = wipeRect(r.L.overlay);
11722
- const wx = wipeX(wr, wipeLine.x);
11723
- const k = lay.x;
11724
- const a = labelsIn(fromMode, r, wr, wx);
11725
- const b = labelsIn(toMode, r, wr, wx);
11726
- const f = Math.min(1, Math.max(0, fade.x));
11933
+ const k = Math.min(1, Math.max(0, lay.x));
11934
+ const fu = fuseFrame(r.L, r.before, r.after, k, wipeLine.x);
11935
+ const corners = fuseCorners(frameRadius, k);
11936
+ const dw = differenceWeight();
11937
+ const fusing = 1 - dw;
11938
+ folded = k * fusing;
11939
+ const fold = labelFold(folded, MARK.w);
11940
+ const w = labelW.map((x) => x - (MARK.w - fold.width) - MARK.gap * folded);
11941
+ const ride = seamLabels(fu.cut, r.before.x, r.after.x + r.after.w, w[0], w[1], INSET);
11942
+ const apart = [r.before.x + INSET, r.after.x + INSET];
11943
+ const corner = [r.before.x + INSET, r.after.x + r.after.w - labelW[1] - INSET];
11944
+ const rides = [ride.before, ride.after];
11727
11945
  const ys = [r.before.y + INSET, r.after.y + INSET];
11728
11946
  for (const i of [0, 1]) {
11729
- const x = a[i].x + (b[i].x - a[i].x) * f;
11730
- labels[i].style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
11731
- labels[i].style.opacity = (a[i].opacity + (b[i].opacity - a[i].opacity) * f).toFixed(3);
11947
+ const fused = apart[i] + (rides[i].x - apart[i]) * k;
11948
+ const x = fused + (corner[i] - fused) * dw;
11949
+ const opacity = (1 - k + k * rides[i].opacity) * fusing + dw;
11950
+ const l = labels[i];
11951
+ l.style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
11952
+ l.style.opacity = opacity.toFixed(3);
11953
+ const mark = l.firstElementChild;
11954
+ if (mark) {
11955
+ mark.style.width = `${fold.width.toFixed(2)}px`;
11956
+ mark.style.marginRight = `${(-MARK.gap * folded).toFixed(2)}px`;
11957
+ mark.style.opacity = fold.opacity.toFixed(3);
11958
+ mark.style.filter = fold.blur > 0.05 ? `blur(${fold.blur.toFixed(2)}px)` : "";
11959
+ }
11732
11960
  }
11733
11961
  const top = Math.min(r.L.side.before.y, r.L.side.after.y);
11734
11962
  split.style.transform = `translate(${r.L.side.split.toFixed(2)}px,${top.toFixed(2)}px)`;
11735
11963
  split.style.height = `${Math.max(0, r.L.side.after.h).toFixed(2)}px`;
11736
11964
  split.style.opacity = String(Math.max(0, 1 - k * 2));
11965
+ const seamOn = fu.seam * fusing;
11966
+ const blur = fu.blur > 0.01 ? `blur(${fu.blur.toFixed(2)}px)` : "none";
11967
+ seam.style.transform = `translate(${fu.cut.toFixed(2)}px,${wr.y.toFixed(2)}px) translateX(-50%)`;
11968
+ seam.style.width = `${(8 + 2 * fu.feather).toFixed(2)}px`;
11969
+ seam.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
11970
+ seam.style.opacity = (blur === "none" ? 0 : seamOn).toFixed(3);
11971
+ seam.style.backdropFilter = blur;
11972
+ seam.style.setProperty("-webkit-backdrop-filter", blur);
11737
11973
  handle.toggleAttribute("data-on", mode === "wipe");
11738
- handle.style.transform = `translate(${wx.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
11974
+ handle.style.transform = `translate(${fu.cut.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
11739
11975
  handle.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
11976
+ handle.style.opacity = seamOn.toFixed(3);
11977
+ handle.style.filter = blur;
11740
11978
  placeGrip(wr);
11741
11979
  handle.setAttribute("aria-valuenow", String(Math.round(wipe * 100)));
11742
11980
  handle.setAttribute("aria-valuetext", `${Math.round(wipe * 100)}% Before`);
@@ -11749,24 +11987,30 @@ function compare(container, options) {
11749
11987
  if (pending)
11750
11988
  return;
11751
11989
  if (renderer === "dom") {
11752
- const place = (n, rect, clip, cutAt) => {
11990
+ const cut = mode === "difference" ? null : fu;
11991
+ const place = (n, rect, clip, before) => {
11992
+ const round = before ? corners.before : corners.after;
11753
11993
  const st = n.style;
11754
11994
  st.transform = `translate(${rect.x.toFixed(2)}px,${rect.y.toFixed(2)}px)`;
11755
11995
  st.width = `${rect.w.toFixed(2)}px`;
11756
11996
  st.height = `${rect.h.toFixed(2)}px`;
11997
+ const edge = before && cut ? cut.cut + cut.feather : Number.POSITIVE_INFINITY;
11757
11998
  const t = Math.max(0, clip.y - rect.y);
11758
11999
  const l = Math.max(0, clip.x - rect.x);
11759
12000
  const b = Math.max(0, rect.y + rect.h - (clip.y + clip.h));
11760
- const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, cutAt));
11761
- 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)` : "";
12001
+ const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, edge));
12002
+ const rr = edge < clip.x + clip.w - 0.5 ? 0 : round.right;
12003
+ const rl = round.left;
12004
+ st.clipPath = t + l + b + right + rl + rr > 0.01 ? `inset(${t.toFixed(2)}px ${right.toFixed(2)}px ${b.toFixed(2)}px ${l.toFixed(2)}px round ${rl.toFixed(2)}px ${rr.toFixed(2)}px ${rr.toFixed(2)}px ${rl.toFixed(2)}px)` : "";
12005
+ const mask = before && cut && cut.feather > 0.5 ? `linear-gradient(to right,#000 ${(cut.cut - cut.feather - rect.x).toFixed(2)}px,transparent ${(edge - rect.x).toFixed(2)}px)` : "";
12006
+ st.maskImage = mask;
12007
+ st.setProperty("-webkit-mask-image", mask);
11762
12008
  };
11763
- const cut = mode === "wipe" ? wx : Number.POSITIVE_INFINITY;
11764
12009
  for (const s of sides) {
11765
12010
  const rect = s.index === 0 ? r.zBefore : r.zAfter;
11766
12011
  const clip = s.index === 0 ? r.before : r.after;
11767
- const at = s.index === 0 ? cut : Number.POSITIVE_INFINITY;
11768
- place(s.el, rect, clip, at);
11769
- place(s.preview, rect, clip, at);
12012
+ place(s.el, rect, clip, s.index === 0);
12013
+ place(s.preview, rect, clip, s.index === 0);
11770
12014
  if (s.index !== 0)
11771
12015
  continue;
11772
12016
  for (const n of [s.el, s.preview])
@@ -11786,8 +12030,8 @@ function compare(container, options) {
11786
12030
  width: Math.max(1, Math.round(stageW * dpr)),
11787
12031
  height: Math.max(1, Math.round(stageH * dpr)),
11788
12032
  dpr,
11789
- wipeX: wx * dpr,
11790
- soft: 0.75 * dpr,
12033
+ wipeX: fu.cut * dpr,
12034
+ soft: Math.max(0.75, fu.feather) * dpr,
11791
12035
  from: MODE_INDEX[fromMode],
11792
12036
  to: MODE_INDEX[toMode],
11793
12037
  mix: Math.min(1, Math.max(0, fade.x)),
@@ -11795,7 +12039,13 @@ function compare(container, options) {
11795
12039
  gain: 1,
11796
12040
  accent,
11797
12041
  loupe: loupeShown() && pointer ? { x: pointer.x * dpr, y: pointer.y * dpr, r: LOUPE.r * dpr, zoom: LOUPE.zoom } : null,
11798
- preview: [Boolean(sides[0].shown?.cached), Boolean(sides[1].shown?.cached)]
12042
+ preview: [Boolean(sides[0].shown?.cached), Boolean(sides[1].shown?.cached)],
12043
+ radii: [
12044
+ corners.before.left * dpr,
12045
+ corners.before.right * dpr,
12046
+ corners.after.left * dpr,
12047
+ corners.after.right * dpr
12048
+ ]
11799
12049
  });
11800
12050
  };
11801
12051
  const drawNow = () => {
@@ -11814,6 +12064,7 @@ function compare(container, options) {
11814
12064
  zu.step(dt);
11815
12065
  zv.step(dt);
11816
12066
  zw.step(dt);
12067
+ gripAt.step(dt);
11817
12068
  const playing = isPlaying();
11818
12069
  if (playing) {
11819
12070
  const m = master();
@@ -11844,11 +12095,9 @@ function compare(container, options) {
11844
12095
  pushFrame(s);
11845
12096
  }
11846
12097
  }
11847
- const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching;
12098
+ const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching || !gripAt.resting;
11848
12099
  if (moving)
11849
12100
  dirty = true;
11850
- if (auto && !lay.resting)
11851
- fitHeight();
11852
12101
  if (dirty)
11853
12102
  draw();
11854
12103
  return playing || moving;
@@ -11874,13 +12123,7 @@ function compare(container, options) {
11874
12123
  zw.set(c.w);
11875
12124
  requestDraw();
11876
12125
  };
11877
- const autoHeight = (w) => {
11878
- const shape = sizeOf(sides[1]);
11879
- const side = splitHeight(w, shape);
11880
- const over = w * shape.h / shape.w;
11881
- const h = side + (over - side) * lay.x;
11882
- return Math.max(60, Math.min(win.innerHeight * 0.8, h));
11883
- };
12126
+ const autoHeight = (w) => Math.max(60, Math.min(win.innerHeight * 0.8, splitHeight(w, sizeOf(sides[1]))));
11884
12127
  const fitHeight = () => {
11885
12128
  const h = autoHeight(stageW);
11886
12129
  if (Math.abs(h - stageH) < 0.25)
@@ -11892,7 +12135,9 @@ function compare(container, options) {
11892
12135
  const readColors = () => {
11893
12136
  const c = tokenColor(win.getComputedStyle(root), "--_ku-cmp-diff", [0.49, 0.81, 0.61, 1]);
11894
12137
  accent = [c[0], c[1], c[2]];
11895
- labelW = [labels[0].offsetWidth, labels[1].offsetWidth];
12138
+ const unfold = MARK.w - labelFold(folded, MARK.w).width + MARK.gap * folded;
12139
+ labelW = [labels[0].offsetWidth + unfold, labels[1].offsetWidth + unfold];
12140
+ frameRadius = Number.parseFloat(win.getComputedStyle(stage).borderTopLeftRadius) || 0;
11896
12141
  requestDraw();
11897
12142
  };
11898
12143
  const measure = () => {
@@ -11983,8 +12228,10 @@ function compare(container, options) {
11983
12228
  root.focus({ preventScroll: true });
11984
12229
  touches.set(e.pointerId, p);
11985
12230
  stage.setPointerCapture(e.pointerId);
12231
+ press = touches.size === 1 && mode !== "wipe" && master() ? p : null;
11986
12232
  if (touches.size >= 2) {
11987
12233
  e.preventDefault();
12234
+ press = null;
11988
12235
  return startPinch();
11989
12236
  }
11990
12237
  if (mode === "wipe") {
@@ -11994,7 +12241,7 @@ function compare(container, options) {
11994
12241
  if (inside && (grab.onLine || !zoomed())) {
11995
12242
  drag = { kind: "wipe", offset: grab.offset };
11996
12243
  root.dataset.dragging = "";
11997
- gripY = p.y;
12244
+ gripAt.snap(wr.h > 0 ? (p.y - wr.y) / wr.h : 0.5);
11998
12245
  e.preventDefault();
11999
12246
  if (!grab.onLine)
12000
12247
  setWipe(wipeAt(p.x, wr), true, true);
@@ -12012,6 +12259,8 @@ function compare(container, options) {
12012
12259
  const onMove = (e) => {
12013
12260
  const p = local(e);
12014
12261
  pointer = p;
12262
+ if (press && Math.hypot(p.x - press.x, p.y - press.y) > 4)
12263
+ press = null;
12015
12264
  if (touches.has(e.pointerId))
12016
12265
  touches.set(e.pointerId, p);
12017
12266
  if (pinch && touches.size >= 2) {
@@ -12025,14 +12274,12 @@ function compare(container, options) {
12025
12274
  pinch = { d, x: mid.x, y: mid.y };
12026
12275
  return handZoom(zoomAbout(panned, k, q.pu, q.pv));
12027
12276
  }
12028
- if (mode === "wipe" && drag?.kind !== "pan") {
12029
- gripY = p.y;
12030
- if (!drag)
12031
- placeGrip(wipeRect(layoutNow().L.overlay));
12032
- }
12033
- if (drag?.kind === "wipe")
12034
- setWipe(wipeAt(p.x, wipeRect(layoutNow().L.overlay), drag.offset));
12035
- else if (drag?.kind === "pan") {
12277
+ if (drag?.kind === "wipe") {
12278
+ const wr = wipeRect(layoutNow().L.overlay);
12279
+ gripAt.snap(wr.h > 0 ? (p.y - wr.y) / wr.h : 0.5);
12280
+ setWipe(wipeAt(p.x, wr, drag.offset));
12281
+ placeGrip(wr);
12282
+ } else if (drag?.kind === "pan") {
12036
12283
  const dx = p.x - drag.x;
12037
12284
  const dy = p.y - drag.y;
12038
12285
  drag.x = p.x;
@@ -12043,11 +12290,23 @@ function compare(container, options) {
12043
12290
  drawNow();
12044
12291
  };
12045
12292
  const endDrag = (e) => {
12293
+ if (press && e.type === "pointerup") {
12294
+ api.toggle();
12295
+ glyph.flash(isPlaying());
12296
+ }
12297
+ press = null;
12046
12298
  touches.delete(e.pointerId);
12047
12299
  if (touches.size < 2)
12048
12300
  pinch = null;
12049
12301
  if (!drag)
12050
12302
  return;
12303
+ if (drag.kind === "wipe") {
12304
+ if (reducedMotion(win))
12305
+ gripAt.snap(0.5);
12306
+ else
12307
+ gripAt.set(0.5);
12308
+ requestDraw();
12309
+ }
12051
12310
  drag = null;
12052
12311
  delete root.dataset.dragging;
12053
12312
  delete root.dataset.panning;
@@ -12197,6 +12456,7 @@ function compare(container, options) {
12197
12456
  old.el.replaceWith(s.el);
12198
12457
  old.preview.replaceWith(s.preview);
12199
12458
  sides[index] = s;
12459
+ root.toggleAttribute("data-video", sides.some((x) => x.kind === "video"));
12200
12460
  note.textContent = "";
12201
12461
  setLabel(index, labelOf(index));
12202
12462
  if (near)
@@ -12211,6 +12471,7 @@ function compare(container, options) {
12211
12471
  stage.insertBefore(sides[0].preview, labels[0]);
12212
12472
  setLabel(0, labelOf(0));
12213
12473
  setLabel(1, labelOf(1));
12474
+ root.toggleAttribute("data-video", sides.some((s) => s.kind === "video"));
12214
12475
  loupeBtn.setAttribute("aria-pressed", String(loupeOn));
12215
12476
  loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
12216
12477
  const modePill = modeList.length > 1 ? selectionPill(modes, {
@@ -12411,6 +12672,7 @@ function compare(container, options) {
12411
12672
  disposed = true;
12412
12673
  loop.stop();
12413
12674
  modePill?.dispose();
12675
+ glyph.dispose();
12414
12676
  win.clearTimeout(releaseTimer);
12415
12677
  ro.disconnect();
12416
12678
  nearIo?.disconnect();
@@ -12440,7 +12702,7 @@ function compare(container, options) {
12440
12702
  wake();
12441
12703
  return api;
12442
12704
  }
12443
- var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
12705
+ var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, MARK, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
12444
12706
  var init_compare = __esm(() => {
12445
12707
  init_motion();
12446
12708
  init_page_hints();
@@ -12451,15 +12713,17 @@ var init_compare = __esm(() => {
12451
12713
  init_icons();
12452
12714
  init_look();
12453
12715
  init_rgba();
12716
+ init_player();
12454
12717
  init_selection_pill();
12455
12718
  init_timeline();
12456
12719
  init_timeline_model();
12457
- LAYOUT2 = springMotion({ duration: 0.35, bounce: 0 });
12720
+ LAYOUT2 = springMotion(SPRINGS.default);
12458
12721
  FADE6 = springMotion(SPRINGS.snappy);
12459
12722
  FADE_REDUCED = springMotion(FADE);
12460
12723
  ZOOM = springMotion({ duration: 0.35, bounce: 0 });
12461
12724
  WIPE_CATCH = springMotion(CATCH_UP);
12462
12725
  GRIP = { w: 16, h: 28 };
12726
+ MARK = { w: 12, gap: 4 };
12463
12727
  LOUPE = { r: 88, zoom: 4 };
12464
12728
  MODE_LABEL = {
12465
12729
  side: { short: "Side", long: "Side by side" },
@@ -16966,11 +17230,629 @@ function toast(message, options = {}) {
16966
17230
  };
16967
17231
  }
16968
17232
 
17233
+ // src/ui/walkthrough-annotations.ts
17234
+ var EYE = "M247.31,124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57,61.26,162.88,48,128,48S61.43,61.26,36.34,86.35C17.51,105.18,9,124,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208s66.57-13.26,91.66-38.34c18.83-18.83,27.3-37.61,27.65-38.4A8,8,0,0,0,247.31,124.76ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.47,133.47,0,0,1,25,128,133.33,133.33,0,0,1,48.07,97.25C70.33,75.19,97.22,64,128,64s57.67,11.19,79.93,33.25A133.46,133.46,0,0,1,231.05,128C223.84,141.46,192.43,192,128,192Zm0-112a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Z";
17235
+ var EYE_OFF = "M53.92,34.62A8,8,0,1,0,42.08,45.38L61.32,66.55C25,88.84,9.38,123.2,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208a127.11,127.11,0,0,0,52.07-10.83l22,24.21a8,8,0,1,0,11.84-10.76Zm47.33,75.84,41.67,45.85a32,32,0,0,1-41.67-45.85ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.16,133.16,0,0,1,25,128c4.69-8.79,19.66-33.39,47.35-49.38l18,19.75a48,48,0,0,0,63.66,70l14.73,16.2A112,112,0,0,1,128,192Zm6-95.43a8,8,0,0,1,3-15.72,48.16,48.16,0,0,1,38.77,42.64,8,8,0,0,1-7.22,8.71,6.39,6.39,0,0,1-.75,0,8,8,0,0,1-8-7.26A32.09,32.09,0,0,0,134,96.57Zm113.28,34.69c-.42.94-10.55,23.37-33.36,43.8a8,8,0,1,1-10.67-11.92A132.77,132.77,0,0,0,231.05,128a133.15,133.15,0,0,0-23.12-30.77C185.67,75.19,158.78,64,128,64a118.37,118.37,0,0,0-19.36,1.57A8,8,0,1,1,106,49.79,134,134,0,0,1,128,48c34.88,0,66.57,13.26,91.66,38.35,18.83,18.83,27.3,37.62,27.65,38.41A8,8,0,0,1,247.31,131.26Z";
17236
+ var glyph = (d) => `<svg class="knobs-icon" viewBox="0 0 256 256" width="16" height="16" fill="currentColor" aria-hidden="true" focusable="false"><path d="${d}"/></svg>`;
17237
+ var ARROW = '<svg class="knobs-wt-anno-arrow" viewBox="0 0 28 28" width="28" height="28" aria-hidden="true"><path d="M3 3 L24 24 M24 24 L24 13 M24 24 L13 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
17238
+ var shapeOf = (a) => a.shape ?? (a.w !== undefined && a.h !== undefined ? "box" : "dot");
17239
+ var showsAt = (a, time) => !a.at || time === undefined || time >= a.at[0] && time <= a.at[1];
17240
+ function placeMark(a, rect, view = ALL) {
17241
+ const w = a.w ?? 0;
17242
+ const h = a.h ?? 0;
17243
+ const x = (a.x - view.u) / view.w;
17244
+ const y = (a.y - view.v) / view.h;
17245
+ const ww = w / view.w;
17246
+ const hh = h / view.h;
17247
+ if (x + ww < 0 || y + hh < 0 || x > 1 || y > 1)
17248
+ return null;
17249
+ return { x: rect.x + x * rect.w, y: rect.y + y * rect.h, w: ww * rect.w, h: hh * rect.h };
17250
+ }
17251
+ var ALL = { u: 0, v: 0, w: 1, h: 1 };
17252
+ var scopeOf = (el) => el.closest(".knobs-walkthrough") ?? el.closest(".knobs-wt-slot") ?? el;
17253
+ var annotationsOn = (scope) => scope.dataset.annotations !== "off";
17254
+ function setAnnotations(scope, on) {
17255
+ if (on)
17256
+ delete scope.dataset.annotations;
17257
+ else
17258
+ scope.dataset.annotations = "off";
17259
+ for (const b of Array.from(scope.querySelectorAll(".knobs-wt-anno-toggle")))
17260
+ syncToggle(b, on);
17261
+ }
17262
+ function syncToggle(b, on) {
17263
+ b.setAttribute("aria-pressed", String(on));
17264
+ b.title = on ? "Hide annotations (a)" : "Show annotations (a)";
17265
+ b.innerHTML = glyph(on ? EYE : EYE_OFF);
17266
+ }
17267
+ function annotationLayer(host, list, place, corner = host) {
17268
+ const doc = host.ownerDocument;
17269
+ const layer = doc.createElement("div");
17270
+ layer.className = "knobs-wt-anno";
17271
+ layer.setAttribute("aria-hidden", "true");
17272
+ host.append(layer);
17273
+ const toggle = doc.createElement("button");
17274
+ toggle.type = "button";
17275
+ toggle.className = "knobs-wt-anno-toggle";
17276
+ toggle.setAttribute("aria-label", "Annotations");
17277
+ syncToggle(toggle, annotationsOn(scopeOf(host)));
17278
+ toggle.addEventListener("click", (e) => {
17279
+ e.stopPropagation();
17280
+ const scope = scopeOf(host);
17281
+ setAnnotations(scope, !annotationsOn(scope));
17282
+ });
17283
+ corner.append(toggle);
17284
+ let marks = [];
17285
+ const make = (a) => {
17286
+ const m = doc.createElement("div");
17287
+ const shape = shapeOf(a);
17288
+ m.className = "knobs-wt-mark";
17289
+ m.dataset.shape = shape;
17290
+ m.dataset.hidden = "";
17291
+ if (shape === "arrow")
17292
+ m.innerHTML = ARROW;
17293
+ if (a.label) {
17294
+ const l = doc.createElement("span");
17295
+ l.className = "knobs-wt-mark-label";
17296
+ l.textContent = a.label;
17297
+ m.append(l);
17298
+ }
17299
+ layer.append(m);
17300
+ return m;
17301
+ };
17302
+ let settled = false;
17303
+ const update = () => {
17304
+ const p = place();
17305
+ if (marks.length !== p.rects.length) {
17306
+ for (const row of marks)
17307
+ for (const m of row)
17308
+ m.remove();
17309
+ marks = p.rects.map(() => list.map(make));
17310
+ }
17311
+ p.rects.forEach((rect, i) => {
17312
+ list.forEach((a, j) => {
17313
+ const m = marks[i][j];
17314
+ const box = placeMark(a, rect, p.view);
17315
+ const show = box !== null && showsAt(a, p.time);
17316
+ if (box) {
17317
+ m.style.transform = `translate(${box.x.toFixed(2)}px,${box.y.toFixed(2)}px)`;
17318
+ m.style.width = `${box.w.toFixed(2)}px`;
17319
+ m.style.height = `${box.h.toFixed(2)}px`;
17320
+ }
17321
+ m.toggleAttribute("data-hidden", !show);
17322
+ });
17323
+ });
17324
+ if (!settled) {
17325
+ settled = true;
17326
+ requestAnimationFrame(() => requestAnimationFrame(() => layer.dataset.settled = ""));
17327
+ }
17328
+ };
17329
+ const ro = new ResizeObserver(() => update());
17330
+ ro.observe(host);
17331
+ update();
17332
+ return {
17333
+ element: layer,
17334
+ update,
17335
+ dispose() {
17336
+ ro.disconnect();
17337
+ layer.remove();
17338
+ toggle.remove();
17339
+ }
17340
+ };
17341
+ }
17342
+
16969
17343
  // src/ui/walkthrough-blocks.ts
16970
17344
  init_motion();
16971
- init_icons();
17345
+
17346
+ // src/ui/history.ts
17347
+ init_motion();
17348
+ init_glide();
17349
+
17350
+ // src/ui/history-css.ts
17351
+ var historyCss = `
17352
+ :where(.knobs-history,.knobs-history *){box-sizing:border-box}
17353
+ .knobs-ui.knobs-history{--hy-gutter:128px;--hy-lane:24px;--hy-bar:16px;--hy-axis:20px;--hy-bandrow:0px;--hy-c:var(--_k-fg-3);
17354
+ --hy-fade:linear-gradient(to right,#000 calc(100% - 1.5em),transparent);
17355
+ position:relative;display:flex;flex-direction:column;width:100%;min-width:0;outline:none;color:var(--_k-fg);
17356
+ font:var(--_k-weight) var(--_k-type-body)/var(--_k-leading-body) var(--_k-font);font-variant-numeric:tabular-nums;font-feature-settings:"tnum";
17357
+ text-align:left;letter-spacing:normal;-webkit-font-smoothing:antialiased;user-select:none;-webkit-user-select:none}
17358
+ .knobs-history[data-bands]{--hy-bandrow:var(--_k-leading-small)}
17359
+ .knobs-history[data-narrow]{--hy-gutter:0px}
17360
+ .knobs-history [hidden]{display:none!important}
17361
+ :where(.knobs-ui) :is(.knobs-hy-ev,.knobs-hy-span,.knobs-hy-jump,.knobs-hy-more){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;
17362
+ background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
17363
+ .knobs-history [data-tone="good"]{--hy-c:var(--_k-ok)}
17364
+ .knobs-history [data-tone="warn"]{--hy-c:var(--_k-warn)}
17365
+ .knobs-history [data-tone="bad"]{--hy-c:var(--_k-danger)}
17366
+ .knobs-history [data-tone="info"]{--hy-c:var(--_k-info)}
17367
+ .knobs-history [data-tone="dim"]{--hy-c:var(--_k-fg-3)}
17368
+
17369
+ /* ——— the chart: lane labels beside the plot (above each lane where it's narrow) ——— */
17370
+ .knobs-hy-chart{position:relative;display:grid;grid-template-columns:var(--hy-gutter) minmax(0,1fr)}
17371
+ .knobs-hy-axis{position:relative;grid-column:2;grid-row:1;height:calc(var(--hy-bandrow) + var(--hy-axis))}
17372
+ :is(.knobs-hy-tick,.knobs-hy-now,.knobs-hy-blabel){position:absolute;white-space:nowrap;font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
17373
+ .knobs-hy-tick{bottom:2px;padding-left:4px}
17374
+ .knobs-hy-now{bottom:2px;color:var(--_k-accent-fg);font-weight:var(--_k-weight-label)}
17375
+ .knobs-hy-blabel{top:0;padding-left:4px;overflow:hidden;color:var(--_k-fg-2)}
17376
+ .knobs-hy-label{grid-column:1;align-self:center;min-width:0;margin-right:var(--_k-space-title);overflow:hidden;white-space:nowrap;
17377
+ font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
17378
+ .knobs-history[data-narrow] .knobs-hy-label{grid-column:2;align-self:end;margin:0}
17379
+ :is(.knobs-hy-label,.knobs-hy-blabel)[data-overflow]{-webkit-mask-image:var(--hy-fade);mask-image:var(--hy-fade)}
17380
+ .knobs-hy-track{position:relative;grid-column:2;height:var(--hy-lane)}
17381
+ .knobs-hy-layer{position:absolute;inset:0;grid-column:2;grid-row:1/-1;pointer-events:none}
17382
+
17383
+ /* Bands and month lines behind the lanes; today, the selection and the pointer's line over them. */
17384
+ .knobs-hy-band{position:absolute;top:0;bottom:0;background:color-mix(in oklch,var(--_k-fg) 3.5%,transparent);box-shadow:inset 1px 0 0 var(--_k-border-subtle)}
17385
+ :is(.knobs-hy-grid,.knobs-hy-today,.knobs-hy-hover,.knobs-hy-sel){position:absolute;left:0;top:calc(var(--hy-bandrow) + var(--hy-axis));bottom:0;width:1px;margin-left:-.5px}
17386
+ .knobs-hy-grid{background:var(--_k-border-subtle)}
17387
+ .knobs-hy-today{top:calc(var(--hy-bandrow) + var(--hy-axis) - 4px);background:var(--_k-accent)}
17388
+ .knobs-hy-hover{background:color-mix(in oklch,var(--_k-fg) 32%,transparent);opacity:0;transition:opacity calc(var(--_k-hover-out) * 1ms)}
17389
+ .knobs-hy-hover[data-on]{opacity:1;transition-duration:calc(var(--_k-hover-in) * 1ms)}
17390
+ .knobs-hy-sel{background:var(--_k-fg-2);opacity:0}
17391
+ .knobs-hy-sel[data-on]{opacity:.6}
17392
+
17393
+ /* Spans: a soft bar in their state's tone, inferred ones hatched; the tag at the end. */
17394
+ .knobs-hy-span{position:absolute;top:calc((var(--hy-lane) - var(--hy-bar)) / 2);height:var(--hy-bar);border-radius:var(--_k-radius-sm);cursor:pointer;
17395
+ background:color-mix(in oklch,var(--hy-c) 24%,transparent);transition:background calc(var(--_k-hover-out) * 1ms),box-shadow calc(var(--_k-hover-out) * 1ms)}
17396
+ .knobs-hy-span[data-inferred]{background:repeating-linear-gradient(135deg,color-mix(in oklch,var(--hy-c) 34%,transparent) 0 2px,color-mix(in oklch,var(--hy-c) 9%,transparent) 2px 6px)}
17397
+ .knobs-hy-span[data-hover]{background-color:color-mix(in oklch,var(--hy-c) 34%,transparent);transition-duration:calc(var(--_k-hover-in) * 1ms)}
17398
+ .knobs-hy-span[data-selected]{box-shadow:inset 0 0 0 1px var(--hy-c)}
17399
+ .knobs-hy-span[data-clip-start]{border-top-left-radius:0;border-bottom-left-radius:0;-webkit-mask-image:linear-gradient(to right,transparent,#000 16px);mask-image:linear-gradient(to right,transparent,#000 16px)}
17400
+ .knobs-hy-span[data-clip-end]{border-top-right-radius:0;border-bottom-right-radius:0;-webkit-mask-image:linear-gradient(to left,transparent,#000 16px);mask-image:linear-gradient(to left,transparent,#000 16px)}
17401
+ .knobs-hy-tag{position:absolute;top:0;right:6px;font-size:var(--_k-type-small);line-height:var(--hy-bar);white-space:nowrap;pointer-events:none;
17402
+ color:color-mix(in oklch,var(--hy-c) 55%,var(--_k-fg))}
17403
+ .knobs-hy-tag[data-place="after"]{right:auto;left:calc(100% + 6px)}
17404
+ .knobs-hy-tag[data-place="none"]{visibility:hidden}
17405
+
17406
+ /* Event marks (their kind named in the tooltip): a dot for a commit, a diamond for a release, a cross for a deletion, a square for a
17407
+ channel's switch, a triangle for a finding; inferred ones hollow. */
17408
+ .knobs-hy-ev{position:absolute;top:0;width:16px;height:var(--hy-lane);margin-left:-8px;cursor:pointer;--hy-c:var(--_k-fg-2);--hy-s:9px}
17409
+ .knobs-hy-ev::before{content:"";position:absolute;left:50%;top:50%;width:var(--hy-s);height:var(--hy-s);margin:calc(var(--hy-s) / -2) 0 0 calc(var(--hy-s) / -2);
17410
+ border-radius:50%;background:var(--hy-c);transition:scale calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy)}
17411
+ .knobs-hy-ev[data-range]{width:calc(var(--hy-w) + 16px)}
17412
+ .knobs-hy-ev[data-range]::before{left:calc(8px - var(--hy-s) / 2);right:calc(8px - var(--hy-s) / 2);width:auto;margin-left:0;border-radius:var(--_k-round)}
17413
+ .knobs-hy-ev[data-type="release"]{--hy-s:8px}
17414
+ .knobs-hy-ev[data-type="release"]::before{border-radius:1.5px;rotate:45deg}
17415
+ .knobs-hy-ev[data-type="channel"]{--hy-s:8px}
17416
+ .knobs-hy-ev[data-type="channel"]::before{border-radius:2px}
17417
+ .knobs-hy-ev[data-type="audit"]{--hy-s:10px}
17418
+ .knobs-hy-ev[data-type="audit"]::before{border-radius:0;clip-path:polygon(50% 4%,100% 92%,0 92%)}
17419
+ .knobs-hy-ev[data-type="deletion"]::before{border-radius:0;rotate:45deg;
17420
+ background:linear-gradient(var(--hy-c),var(--hy-c)) center/100% 1.75px no-repeat,linear-gradient(var(--hy-c),var(--hy-c)) center/1.75px 100% no-repeat}
17421
+ .knobs-hy-ev[data-inferred]:not([data-type="audit"],[data-type="deletion"])::before{background:transparent;box-shadow:inset 0 0 0 1.5px var(--hy-c)}
17422
+ .knobs-hy-ev[data-inferred]:is([data-type="audit"],[data-type="deletion"])::before{opacity:.5}
17423
+ /* Held-back marks (commits, by default) come up while the pointer is on their lane, and stay with
17424
+ "Show commits" or while selected. */
17425
+ .knobs-hy-ev[data-quiet]{opacity:0;transition:opacity calc(var(--_k-hover-out) * 1ms)}
17426
+ .knobs-hy-track:hover .knobs-hy-ev[data-quiet]{opacity:.6;transition-duration:calc(var(--_k-motion-snappy-ms) * 1ms)}
17427
+ :is(.knobs-history[data-all] .knobs-hy-ev[data-quiet],.knobs-hy-ev[data-quiet]:is([data-selected],[data-hover])){opacity:1}
17428
+ .knobs-hy-ev[data-hover]::before{scale:1.25}
17429
+ .knobs-hy-ev[data-selected]::before{scale:1.3}
17430
+ .knobs-hy-ev[data-selected]::after{content:"";position:absolute;left:50%;top:50%;width:calc(var(--hy-s) + 9px);height:calc(var(--hy-s) + 9px);
17431
+ margin:calc((var(--hy-s) + 9px) / -2) 0 0 calc((var(--hy-s) + 9px) / -2);border-radius:50%;box-shadow:0 0 0 1.5px var(--hy-c)}
17432
+ .knobs-hy-ev[data-range][data-selected]::after{left:3px;right:3px;width:auto;margin-left:0;border-radius:var(--_k-round)}
17433
+ .knobs-history:focus-visible .knobs-hy-ev[data-selected]::after{box-shadow:0 0 0 2px var(--_k-focus)}
17434
+ .knobs-history:focus-visible .knobs-hy-span[data-selected]{box-shadow:inset 0 0 0 2px var(--_k-focus)}
17435
+
17436
+ /* ——— under the chart: "Show commits", and a legend only when inferred and verified both appear ——— */
17437
+ .knobs-hy-foot{display:flex;flex-wrap:wrap;align-items:center;gap:var(--_k-space-inside) var(--_k-space-title);margin-top:var(--_k-space-sibling);padding-left:var(--hy-gutter);
17438
+ font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
17439
+ .knobs-hy-legend{display:flex;flex-wrap:wrap;gap:var(--_k-space-inside) var(--_k-space-title)}
17440
+ .knobs-ui .knobs-hy-more{height:20px;margin-left:-6px;padding:0 6px;border-radius:var(--_k-radius-sm);color:var(--_k-fg-3);cursor:pointer;
17441
+ transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
17442
+ .knobs-ui .knobs-hy-more:is(:hover,:focus-visible){background:var(--_k-fill);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
17443
+ .knobs-hy-key{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
17444
+ .knobs-hy-swatch{flex:none;width:14px;height:8px;border-radius:2px;background:color-mix(in oklch,var(--hy-c) 34%,transparent)}
17445
+ .knobs-hy-swatch[data-inferred]{background:repeating-linear-gradient(135deg,color-mix(in oklch,var(--hy-c) 60%,transparent) 0 1.5px,transparent 1.5px 4px)}
17446
+
17447
+ /* ——— the detail: one area under the chart, its height on a spring ——— */
17448
+ .knobs-hy-detail{position:relative;overflow:hidden;user-select:text;-webkit-user-select:text}
17449
+ .knobs-hy-detail[data-moving]{-webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 16px),transparent);mask-image:linear-gradient(to bottom,#000 calc(100% - 16px),transparent)}
17450
+ .knobs-hy-dc{padding:var(--_k-space-title) 0 0 var(--hy-gutter);transition:opacity calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy)}
17451
+ .knobs-hy-dc:is([data-enter],[data-leave]){opacity:0}
17452
+ .knobs-hy-dc[data-leave]{position:absolute;left:0;right:0;top:0;transition-duration:calc(var(--_k-motion-snappy-ms) * .8ms)}
17453
+ .knobs-hy-dmeta{display:flex;flex-wrap:wrap;gap:0 var(--_k-space-sibling);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
17454
+ .knobs-hy-dmeta>[data-tone]{color:var(--hy-c)}
17455
+ .knobs-hy-dtitle{margin-top:var(--_k-space-inside);font-size:var(--_k-type-body);line-height:var(--_k-leading-body);font-weight:var(--_k-weight-title);color:var(--_k-fg);text-wrap:balance}
17456
+ .knobs-hy-dtext{margin:var(--_k-space-inside) 0 0;max-width:68ch;font-size:var(--_k-type-body);line-height:var(--_k-leading-body);color:var(--_k-fg-2);text-wrap:pretty}
17457
+ .knobs-hy-dtext code{font:calc(1em - 1px) var(--_k-mono);padding:1px 4px;border-radius:var(--_k-radius-sm);background:var(--_k-fill)}
17458
+ .knobs-hy-dfoot{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:var(--_k-space-sibling)}
17459
+ .knobs-hy-dfoot:empty{display:none}
17460
+ .knobs-hy-evidence{display:inline-flex;align-items:center;height:20px;padding:0 6px;border-radius:var(--_k-radius-sm);background:var(--_k-fill);color:var(--_k-fg-2);
17461
+ font:var(--_k-type-small)/1 var(--_k-mono);text-decoration:none;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
17462
+ a.knobs-hy-evidence:hover{background:var(--_k-fill-hover);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
17463
+ .knobs-ui .knobs-hy-jump{display:inline-flex;align-items:center;gap:4px;height:20px;padding:0 6px 0 8px;border-radius:var(--_k-radius-sm);color:var(--_k-fg-2);cursor:pointer;
17464
+ font-size:var(--_k-type-small);transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
17465
+ .knobs-ui .knobs-hy-jump:is(:hover,:focus-visible){background:var(--_k-fill);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
17466
+ .knobs-hy-jump svg{width:12px;height:12px}
17467
+
17468
+ /* ——— the pointer's tooltip: title, date, one line ——— */
17469
+ .knobs-hy-tip{position:absolute;left:0;top:0;max-width:280px;padding:6px 8px;border-radius:var(--_k-radius-md);background:var(--_k-bg-overlay);
17470
+ box-shadow:var(--_k-shadow-overlay),0 0 0 1px var(--_k-border);pointer-events:none;opacity:0;transition:opacity calc(var(--_k-hover-out) * 1ms)}
17471
+ .knobs-hy-tip[data-on]{opacity:1;transition-duration:calc(var(--_k-hover-in) * 1ms)}
17472
+ .knobs-hy-tt{font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight-label);color:var(--_k-fg);text-wrap:balance}
17473
+ :is(.knobs-hy-td,.knobs-hy-tl){font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
17474
+ .knobs-hy-tl{margin-top:2px;color:var(--_k-fg-2);text-wrap:pretty}
17475
+ .knobs-hy-tl:empty{display:none}
17476
+
17477
+ @media(prefers-reduced-motion:reduce){
17478
+ .knobs-history :where(*,::before,::after){transition-property:opacity,background,color,box-shadow!important}
17479
+ .knobs-hy-dc{transition-duration:calc(var(--_k-hover-out) * 1ms)!important}
17480
+ }
17481
+ `;
17482
+
17483
+ // src/ui/history-model.ts
17484
+ var MS_DAY = 86400000;
17485
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
17486
+ var dayOf = (y, m, d) => Math.floor(Date.UTC(y, m, d) / MS_DAY);
17487
+ var parts = (day) => {
17488
+ const t = new Date(day * MS_DAY);
17489
+ return { y: t.getUTCFullYear(), m: t.getUTCMonth(), d: t.getUTCDate() };
17490
+ };
17491
+ var todayDay = (now = Date.now()) => {
17492
+ const t = new Date(now);
17493
+ return dayOf(t.getFullYear(), t.getMonth(), t.getDate());
17494
+ };
17495
+ var monthIndex = (name) => MONTHS.findIndex((m) => m.toLowerCase() === name.slice(0, 3).toLowerCase());
17496
+ function parseDay(text, year) {
17497
+ const s = text.trim();
17498
+ const iso = /^(\d{4})-(\d{2})-(\d{2})/.exec(s);
17499
+ if (iso) {
17500
+ const y = Number(iso[1]);
17501
+ const day = dayOf(y, Number(iso[2]) - 1, Number(iso[3]));
17502
+ return { from: day, to: day, year: y };
17503
+ }
17504
+ const m = /^([A-Za-z]{3,9})\.?\s+(\d{1,2})(?:\s*[–—-]\s*(?:([A-Za-z]{3,9})\.?\s+)?(\d{1,2}))?(?:,?\s+(\d{4}))?$/.exec(s);
17505
+ if (!m)
17506
+ return null;
17507
+ const m1 = monthIndex(m[1]);
17508
+ const m2 = m[3] ? monthIndex(m[3]) : m1;
17509
+ if (m1 < 0 || m2 < 0)
17510
+ return null;
17511
+ const written = m[5] ? Number(m[5]) : undefined;
17512
+ const y = written ?? year;
17513
+ if (y === undefined)
17514
+ return null;
17515
+ const from = dayOf(y, m1, Number(m[2]));
17516
+ let to = m[4] ? dayOf(y, m2, Number(m[4])) : from;
17517
+ if (to < from)
17518
+ to = dayOf(y + 1, m2, Number(m[4]));
17519
+ return { from, to, ...written !== undefined ? { year: written } : {} };
17520
+ }
17521
+ function formatDay(day) {
17522
+ const { y, m, d } = parts(day);
17523
+ return `${MONTHS[m]} ${d}, ${y}`;
17524
+ }
17525
+ function formatRange(from, to, open = false) {
17526
+ if (open)
17527
+ return `${formatDay(from)} – today`;
17528
+ if (to <= from)
17529
+ return formatDay(from);
17530
+ const a = parts(from);
17531
+ const b = parts(to);
17532
+ if (a.y === b.y && a.m === b.m)
17533
+ return `${MONTHS[a.m]} ${a.d}–${b.d}, ${a.y}`;
17534
+ if (a.y === b.y)
17535
+ return `${MONTHS[a.m]} ${a.d} – ${MONTHS[b.m]} ${b.d}, ${a.y}`;
17536
+ return `${formatDay(from)} – ${formatDay(to)}`;
17537
+ }
17538
+ var formatDays = (n) => `${n} ${n === 1 ? "day" : "days"}`;
17539
+ function guessTone(state) {
17540
+ const s = state.toLowerCase();
17541
+ if (/broken|bad|fail|missing|deleted|down/.test(s))
17542
+ return "bad";
17543
+ if (/warn|redirect|partial|beta|next|pre/.test(s))
17544
+ return "warn";
17545
+ if (/work|ok|good|fixed|live|green/.test(s))
17546
+ return "good";
17547
+ return "dim";
17548
+ }
17549
+ var bandLabel = (heading) => heading.replace(/^\s*\d+\s*[·.:)\-–]\s*/, "").replace(/\s*\([^)]*\)\s*$/, "").trim();
17550
+ function fromGroups(groups, today) {
17551
+ const events = [];
17552
+ const bands = [];
17553
+ const all = groups.flatMap((g) => g.events ?? g.rows ?? []);
17554
+ let year = all.map((e) => parseDay(e.date, 0)?.year).find((y) => y !== undefined) ?? parts(today).y;
17555
+ for (const g of groups) {
17556
+ let lo = Infinity;
17557
+ let hi = -Infinity;
17558
+ for (const e of g.events ?? g.rows ?? []) {
17559
+ const d = parseDay(e.date, year);
17560
+ if (!d)
17561
+ continue;
17562
+ if (d.year !== undefined)
17563
+ year = d.year;
17564
+ lo = Math.min(lo, d.from);
17565
+ hi = Math.max(hi, d.to);
17566
+ events.push({
17567
+ ...e,
17568
+ date: iso(d.from),
17569
+ ...d.to > d.from ? { to: iso(d.to) } : {},
17570
+ lane: e.lane ?? "code"
17571
+ });
17572
+ }
17573
+ const label = g.heading ? bandLabel(g.heading) : "";
17574
+ if (label && lo <= hi)
17575
+ bands.push({ from: iso(lo), to: iso(hi), label });
17576
+ }
17577
+ return { events, bands };
17578
+ }
17579
+ function iso(day) {
17580
+ const { y, m, d } = parts(day);
17581
+ return `${y}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
17582
+ }
17583
+ var startOfMonth = (day) => {
17584
+ const { y, m } = parts(day);
17585
+ return dayOf(y, m, 1);
17586
+ };
17587
+ var TYPE_TONE = { deletion: "bad" };
17588
+ function normalizeHistory(data, now = todayDay()) {
17589
+ const today = data.axis?.today && parseDay(data.axis.today, parts(now).y)?.from || now;
17590
+ const converted = data.groups?.length ? fromGroups(data.groups, today) : { events: [], bands: [] };
17591
+ const rawEvents = [...data.events ?? [], ...converted.events];
17592
+ const rawBands = [...data.bands ?? [], ...converted.bands];
17593
+ const year = parts(today).y;
17594
+ const day = (s) => s ? parseDay(s, year) ?? null : null;
17595
+ const states = new Map;
17596
+ const stateOf = (key) => {
17597
+ const known = states.get(key);
17598
+ if (known)
17599
+ return known;
17600
+ const def = data.states?.[key];
17601
+ const s = { key, label: def?.label ?? key, tone: def?.tone ?? guessTone(key) };
17602
+ states.set(key, s);
17603
+ return s;
17604
+ };
17605
+ const items = new Map;
17606
+ const laneOrder = [];
17607
+ const lanes = new Map;
17608
+ const lane = (id, label = id === "code" ? "Code" : id) => {
17609
+ let l = lanes.get(id);
17610
+ if (!l) {
17611
+ l = { id, label, spans: [], events: [] };
17612
+ lanes.set(id, l);
17613
+ laneOrder.push(id);
17614
+ }
17615
+ return l;
17616
+ };
17617
+ if (rawEvents.some((e) => !e.lane || e.lane === "code") && !data.lanes?.some((l) => l.id === "code"))
17618
+ lane("code");
17619
+ let inferred = false;
17620
+ for (const l of data.lanes ?? []) {
17621
+ const into = lane(l.id, l.label);
17622
+ (l.spans ?? []).forEach((s, i) => {
17623
+ const a = day(s.from);
17624
+ if (!a)
17625
+ return;
17626
+ const open = !s.to;
17627
+ const b = open ? null : day(s.to);
17628
+ const to = open ? today : b?.to ?? a.to;
17629
+ const st = stateOf(s.state);
17630
+ const days = Math.max(0, to - a.from);
17631
+ const own = s.label ?? data.states?.[s.state]?.tag ?? "{d}d";
17632
+ const tags = own === "" ? [] : [...new Set([own, "{d}d"])].map((t) => t.replace(/\{d\}/g, String(days)));
17633
+ const id = s.id ?? `${l.id}/${i}`;
17634
+ const item = {
17635
+ id,
17636
+ kind: "span",
17637
+ lane: l.id,
17638
+ from: a.from,
17639
+ to,
17640
+ open,
17641
+ title: s.title ?? `${l.label} · ${st.label}`,
17642
+ ...s.detail ? { detail: s.detail } : {},
17643
+ tone: st.tone,
17644
+ inferred: s.provenance === "inferred",
17645
+ ...s.provenance ? { provenance: s.provenance } : {},
17646
+ ...s.source ? { source: s.source } : {},
17647
+ evidence: s.evidence ?? [],
17648
+ ...s.step ? { step: s.step } : {},
17649
+ state: s.state,
17650
+ tags
17651
+ };
17652
+ if (item.inferred)
17653
+ inferred = true;
17654
+ into.spans.push(item);
17655
+ items.set(id, item);
17656
+ });
17657
+ }
17658
+ const types = [];
17659
+ const events = [];
17660
+ rawEvents.forEach((e, index) => {
17661
+ const a = day(e.date);
17662
+ if (!a)
17663
+ return;
17664
+ const to = Math.max(a.to, day(e.to)?.to ?? a.to);
17665
+ const into = lane(e.lane ?? "code");
17666
+ const id = e.id ?? `e${index}`;
17667
+ const item = {
17668
+ id,
17669
+ kind: "event",
17670
+ lane: into.id,
17671
+ from: a.from,
17672
+ to,
17673
+ open: false,
17674
+ title: e.title,
17675
+ ...e.detail ? { detail: e.detail } : {},
17676
+ tone: e.tone ?? TYPE_TONE[e.type ?? ""] ?? "dim",
17677
+ ...e.type ? { type: e.type } : {},
17678
+ inferred: e.provenance === "inferred",
17679
+ ...e.provenance ? { provenance: e.provenance } : {},
17680
+ ...e.source ? { source: e.source } : {},
17681
+ evidence: e.evidence ?? [],
17682
+ ...e.step ? { step: e.step } : {},
17683
+ tags: []
17684
+ };
17685
+ if (item.inferred)
17686
+ inferred = true;
17687
+ if (e.type && !types.includes(e.type))
17688
+ types.push(e.type);
17689
+ into.events.push(item);
17690
+ items.set(id, item);
17691
+ events.push({ item, index });
17692
+ });
17693
+ const bands = [];
17694
+ for (const b of rawBands) {
17695
+ const a = day(b.from);
17696
+ if (!a)
17697
+ continue;
17698
+ const z = b.to ? day(b.to) : null;
17699
+ bands.push({ from: a.from, to: z ? z.to : today, label: b.label });
17700
+ }
17701
+ bands.sort((x, y) => x.from - y.from);
17702
+ const all = [...items.values()].flatMap((i) => [i.from, i.to]);
17703
+ for (const b of bands)
17704
+ all.push(b.from, b.to);
17705
+ const hasOpen = [...items.values()].some((i) => i.open);
17706
+ const lo = all.length ? Math.min(...all) : today - 90;
17707
+ let hi = all.length ? Math.max(...all) : today;
17708
+ if (hasOpen || data.axis?.today)
17709
+ hi = Math.max(hi, today);
17710
+ const from = day(data.axis?.from)?.from ?? startOfMonth(lo);
17711
+ const to = day(data.axis?.to)?.to ?? hi + Math.max(6, Math.round((hi - from) * 0.05));
17712
+ const laneIndex = (id) => laneOrder.indexOf(id);
17713
+ const order = eventOrder(events.map((e) => ({ ...e, laneIndex: laneIndex(e.item.lane) })));
17714
+ for (const l of lanes.values())
17715
+ l.events.sort((a, b) => a.from - b.from);
17716
+ return {
17717
+ from,
17718
+ to: Math.max(to, from + 1),
17719
+ today,
17720
+ lanes: laneOrder.map((id) => lanes.get(id)),
17721
+ events: order,
17722
+ bands,
17723
+ states: [...states.values()],
17724
+ types,
17725
+ inferred,
17726
+ verified: [...items.values()].some((i) => i.provenance === "verified"),
17727
+ items
17728
+ };
17729
+ }
17730
+ var DEFAULT_MARKS = ["release", "deletion"];
17731
+ function quietEvents(events, marks) {
17732
+ if (marks === "all")
17733
+ return new Set;
17734
+ const quiet = events.filter((e) => e.type && !marks.includes(e.type));
17735
+ return quiet.length === events.length ? new Set : new Set(quiet.map((e) => e.id));
17736
+ }
17737
+ var PLURALS = {
17738
+ commit: "commits",
17739
+ release: "releases",
17740
+ deletion: "deletions",
17741
+ channel: "channel switches",
17742
+ audit: "findings"
17743
+ };
17744
+ function quietLabel(events, quiet) {
17745
+ const types = [];
17746
+ for (const e of events)
17747
+ if (quiet.has(e.id) && e.type && !types.includes(e.type))
17748
+ types.push(e.type);
17749
+ const names = types.map((t) => PLURALS[t] ?? `${t}s`);
17750
+ return names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}` : names[0] ?? "";
17751
+ }
17752
+ function eventOrder(events) {
17753
+ return [...events].sort((a, b) => a.item.from - b.item.from || a.laneIndex - b.laneIndex || a.index - b.index).map((e) => e.item);
17754
+ }
17755
+ function stepEvent(order, current, dir, at) {
17756
+ if (!order.length)
17757
+ return null;
17758
+ const i = current === null ? -1 : order.findIndex((e) => e.id === current);
17759
+ if (i < 0 && at !== undefined) {
17760
+ const next = dir > 0 ? order.find((e) => e.from >= at) : [...order].reverse().find((e) => e.from <= at);
17761
+ return next ?? (dir > 0 ? order[order.length - 1] : order[0]);
17762
+ }
17763
+ if (i < 0)
17764
+ return dir > 0 ? order[0] : order[order.length - 1];
17765
+ return order[Math.max(0, Math.min(order.length - 1, i + dir))];
17766
+ }
17767
+ function dayScale(from, to, width) {
17768
+ const span = Math.max(1, to - from);
17769
+ return {
17770
+ x: (day) => (day - from) / span * width,
17771
+ day: (x) => from + x / Math.max(1, width) * span,
17772
+ f: (day) => (day - from) / span
17773
+ };
17774
+ }
17775
+ var STEPS = [1, 2, 3, 6, 12];
17776
+ function monthTicks(from, to, width, minGap = 44) {
17777
+ const perMonth = width / Math.max(1, to - from) * 30.44;
17778
+ const step = STEPS.find((s) => s * perMonth >= minGap) ?? 12;
17779
+ const ticks = [];
17780
+ const { y } = parts(from);
17781
+ let { m } = parts(from);
17782
+ let d = dayOf(y, m, 1);
17783
+ if (d < from) {
17784
+ m++;
17785
+ d = dayOf(y, m, 1);
17786
+ }
17787
+ while (d <= to) {
17788
+ const p = parts(d);
17789
+ if (p.m % step === 0)
17790
+ ticks.push({ day: d, label: p.m === 0 ? String(p.y) : MONTHS[p.m] });
17791
+ m++;
17792
+ d = dayOf(y, m, 1);
17793
+ }
17794
+ return ticks;
17795
+ }
17796
+ function fitLabels(labels, width, reserved = [], gap = 8) {
17797
+ let end = -Infinity;
17798
+ return labels.map(({ x, w }) => {
17799
+ const x1 = x + w;
17800
+ if (x < end + gap || x1 > width)
17801
+ return false;
17802
+ if (reserved.some((r) => x < r.x1 + gap && x1 > r.x0 - gap))
17803
+ return false;
17804
+ end = x1;
17805
+ return true;
17806
+ });
17807
+ }
17808
+ function clipSpan(from, to, axisFrom, axisTo) {
17809
+ if (to < axisFrom || from > axisTo)
17810
+ return null;
17811
+ return {
17812
+ from: Math.max(from, axisFrom),
17813
+ to: Math.min(to, axisTo),
17814
+ clipStart: from < axisFrom,
17815
+ clipEnd: to > axisTo
17816
+ };
17817
+ }
17818
+ function fitTag(widths, spanWidth, roomAfter, pad = 6) {
17819
+ for (let i = 0;i < widths.length; i++)
17820
+ if (widths[i] + pad * 2 <= spanWidth)
17821
+ return { index: i, place: "inside" };
17822
+ for (let i = 0;i < widths.length; i++)
17823
+ if (widths[i] + pad <= roomAfter)
17824
+ return { index: i, place: "after" };
17825
+ return null;
17826
+ }
17827
+ function bandRoom(bands, x) {
17828
+ return bands.map((b, i) => {
17829
+ const next = bands.slice(i + 1).find((n) => n.from > b.from);
17830
+ const end = Math.min(b.to, next ? next.from : Infinity);
17831
+ return Math.max(0, x(end) - x(b.from));
17832
+ });
17833
+ }
17834
+ function nearestEvent(events, day, tolerance) {
17835
+ let best = null;
17836
+ let gap = tolerance;
17837
+ for (const e of events) {
17838
+ const d = day < e.from ? e.from - day : day > e.to ? day - e.to : 0;
17839
+ if (d <= gap) {
17840
+ best = e;
17841
+ gap = d;
17842
+ }
17843
+ }
17844
+ return best;
17845
+ }
17846
+ function firstSentence(text) {
17847
+ if (!text)
17848
+ return "";
17849
+ const plain = text.replace(/`([^`]*)`/g, "$1").replace(/\*\*?([^*]+)\*\*?/g, "$1").trim();
17850
+ const m = /^.*?[.!?](?=\s|$)/.exec(plain);
17851
+ return (m ? m[0] : plain).trim();
17852
+ }
17853
+
17854
+ // src/ui/history.ts
16972
17855
  init_look();
16973
- init_selection_pill();
16974
17856
 
16975
17857
  // src/ui/walkthrough-model.ts
16976
17858
  var DEFAULT_VERDICTS = ["ship", "change", "unsure"];
@@ -17036,7 +17918,14 @@ function fillMedia(m, base) {
17036
17918
  case "image":
17037
17919
  return { ...m, src: resolveSrc(m.src, base) };
17038
17920
  case "history":
17039
- return { kind: "history", groups: fillHistory(m) };
17921
+ if ("data" in m)
17922
+ return m;
17923
+ return {
17924
+ kind: "history",
17925
+ data: historyData(m),
17926
+ ...m.label ? { label: m.label } : {},
17927
+ ...m.marks ? { marks: m.marks } : {}
17928
+ };
17040
17929
  case "stack":
17041
17930
  return {
17042
17931
  kind: "stack",
@@ -17049,17 +17938,26 @@ function fillMedia(m, base) {
17049
17938
  return m;
17050
17939
  }
17051
17940
  }
17052
- function fillHistory(m) {
17941
+ function historyData(m) {
17942
+ const events = m.events ?? [];
17943
+ if (m.lanes || m.bands || m.states || m.axis)
17944
+ return {
17945
+ ...m.axis ? { axis: m.axis } : {},
17946
+ ...m.lanes ? { lanes: m.lanes } : {},
17947
+ ...events.length ? { events } : {},
17948
+ ...m.bands ? { bands: m.bands } : {},
17949
+ ...m.states ? { states: m.states } : {}
17950
+ };
17053
17951
  const groups = [];
17054
- if (m.events?.length)
17055
- groups.push({ events: m.events });
17952
+ if (events.length)
17953
+ groups.push({ events });
17056
17954
  for (const g of m.groups ?? [])
17057
17955
  groups.push({
17058
17956
  ...g.heading ? { heading: g.heading } : {},
17059
17957
  ...g.summary ? { summary: g.summary } : {},
17060
17958
  events: g.events ?? g.rows ?? []
17061
17959
  });
17062
- return groups;
17960
+ return { groups };
17063
17961
  }
17064
17962
  function parseBranch(branch) {
17065
17963
  if (!branch?.trim())
@@ -17259,7 +18157,7 @@ function normalizeWalkthrough(doc) {
17259
18157
  ...step.question ? { question: step.question } : {},
17260
18158
  media: list.map((m) => fillMedia(m, doc.base)),
17261
18159
  columns: Math.max(1, Math.min(3, Math.round(step.columns ?? 1))),
17262
- ...step.more?.trim() && list.length > 1 ? { more: step.more.trim() } : {},
18160
+ ...step.more?.trim() ? { more: step.more.trim() } : {},
17263
18161
  options,
17264
18162
  ask
17265
18163
  };
@@ -17274,6 +18172,7 @@ function normalizeWalkthrough(doc) {
17274
18172
  ...typeof s.kicker === "string" && s.kicker.trim() ? { kicker: s.kicker.trim() } : {},
17275
18173
  ...s.status ? { status: s.status } : {},
17276
18174
  ...s.summary ? { summary: s.summary } : {},
18175
+ ...s.more?.trim() ? { more: s.more.trim() } : {},
17277
18176
  steps: own
17278
18177
  };
17279
18178
  });
@@ -17307,6 +18206,7 @@ function normalizeWalkthrough(doc) {
17307
18206
  id: doc.id,
17308
18207
  title: doc.title,
17309
18208
  ...doc.summary ? { summary: doc.summary } : {},
18209
+ ...doc.more?.trim() ? { more: doc.more.trim() } : {},
17310
18210
  meta,
17311
18211
  feed,
17312
18212
  verdicts: ((doc.verdicts?.length) ? doc.verdicts : DEFAULT_VERDICTS).map((v) => ({
@@ -17314,9 +18214,40 @@ function normalizeWalkthrough(doc) {
17314
18214
  label: label(v)
17315
18215
  })),
17316
18216
  sections: withKickers(sections, doc.sections, doc.kickers),
17317
- steps
18217
+ steps,
18218
+ prs: knownPRs(doc, sections),
18219
+ ...repoOf(doc) ? { repo: repoOf(doc) } : {}
17318
18220
  };
17319
18221
  }
18222
+ var prNumber = (href) => /\/(?:pull|merge_requests)\/(\d+)(?:[/?#]|$)/.exec(href)?.[1] ?? null;
18223
+ var repoOf = (doc) => doc.repo?.trim().replace(/^https?:\/\/github\.com\//, "").replace(/\/+$/, "") || undefined;
18224
+ function knownPRs(doc, sections) {
18225
+ const out = {};
18226
+ const repo = repoOf(doc);
18227
+ for (const s of sections) {
18228
+ const n = s.link && prNumber(s.link.href);
18229
+ if (n)
18230
+ out[n] = { href: s.link.href.replace(/(\/pull\/\d+).*$/, "$1") };
18231
+ }
18232
+ for (const [k, pr] of Object.entries(doc.prs ?? {})) {
18233
+ const n = k.replace(/^#/, "");
18234
+ if (!/^\d+$/.test(n))
18235
+ continue;
18236
+ out[n] = { ...out[n], ...pr };
18237
+ }
18238
+ if (repo) {
18239
+ for (const n of Object.keys(out))
18240
+ if (!out[n].href)
18241
+ out[n] = { ...out[n], href: `https://github.com/${repo}/pull/${n}` };
18242
+ }
18243
+ return out;
18244
+ }
18245
+ function findPR(prs, n, repo) {
18246
+ const known = prs[n];
18247
+ if (known)
18248
+ return known;
18249
+ return repo ? { href: `https://github.com/${repo}/pull/${n}` } : null;
18250
+ }
17320
18251
  var isPullRequest = (href) => /\/(?:pull|merge_requests)\/\d+(?:[/?#]|$)/.test(href);
17321
18252
  function withKickers(sections, source, kickers = "pr") {
17322
18253
  if (kickers === false)
@@ -17645,11 +18576,18 @@ function inlineMarkdown(text) {
17645
18576
  return s.replace(/\u0000(\d+)\u0000/g, (_, i) => `<code>${escapeHtml(codes[Number(i)])}</code>`);
17646
18577
  }
17647
18578
 
17648
- // src/ui/walkthrough-blocks.ts
17649
- var GLYPH = {
17650
- go: icon("caretRight", { small: true }),
17651
- caret: icon("caretRight", { small: true })
18579
+ // src/ui/history.ts
18580
+ var HEIGHT2 = springMotion(SPRINGS.default);
18581
+ var GO = svg('<path d="M5 5h6v6M11 5l-6 6"/>');
18582
+ var TYPE_NAMES = {
18583
+ commit: "Commit",
18584
+ release: "Release",
18585
+ deletion: "Deletion",
18586
+ channel: "Channel switch",
18587
+ audit: "Finding"
17652
18588
  };
18589
+ var typeName = (t) => TYPE_NAMES[t] ?? t.charAt(0).toUpperCase() + t.slice(1);
18590
+ var REACH = 8;
17653
18591
  var el = (doc, tag, cls) => {
17654
18592
  const n = doc.createElement(tag);
17655
18593
  if (cls)
@@ -17661,112 +18599,625 @@ var text = (doc, tag, cls, value) => {
17661
18599
  n.textContent = value;
17662
18600
  return n;
17663
18601
  };
18602
+ function injectHistoryStyles(doc) {
18603
+ if (doc.querySelector("style[data-knobs-history]"))
18604
+ return;
18605
+ const style = doc.createElement("style");
18606
+ style.dataset.knobsHistory = "";
18607
+ style.textContent = aliased(historyCss);
18608
+ doc.head.append(style);
18609
+ }
18610
+ var when = (it) => it.kind === "span" ? `${formatRange(it.from, it.to, it.open)} · ${formatDays(it.to - it.from)}` : formatRange(it.from, it.to);
18611
+ function history(container, options) {
18612
+ const doc = container.ownerDocument;
18613
+ const win = doc.defaultView;
18614
+ const root = el(doc, "div", "knobs-history");
18615
+ applyLook2(root, options);
18616
+ injectHistoryStyles(doc);
18617
+ root.tabIndex = 0;
18618
+ root.setAttribute("role", "group");
18619
+ root.setAttribute("aria-label", options.label ?? "History");
18620
+ root.setAttribute("aria-roledescription", "timeline");
18621
+ const chart = el(doc, "div", "knobs-hy-chart");
18622
+ const foot = el(doc, "div", "knobs-hy-foot");
18623
+ const detail = el(doc, "div", "knobs-hy-detail");
18624
+ detail.setAttribute("aria-live", "polite");
18625
+ const tip = el(doc, "div", "knobs-hy-tip");
18626
+ tip.setAttribute("aria-hidden", "true");
18627
+ const tipTitle = el(doc, "div", "knobs-hy-tt");
18628
+ const tipDate = el(doc, "div", "knobs-hy-td");
18629
+ const tipLine = el(doc, "div", "knobs-hy-tl");
18630
+ tip.append(tipTitle, tipDate, tipLine);
18631
+ root.append(chart, foot, detail, tip);
18632
+ container.append(root);
18633
+ let model = normalizeHistory(options.data);
18634
+ let selected = options.selected && model.items.has(options.selected) ? options.selected : null;
18635
+ let disposed = false;
18636
+ let quiet = quietEvents(model.events, options.marks ?? DEFAULT_MARKS);
18637
+ let showAll = false;
18638
+ const walked = () => showAll ? model.events : model.events.filter((e) => !quiet.has(e.id));
18639
+ let nodes = new Map;
18640
+ let tracks = new Map;
18641
+ let over = null;
18642
+ let hoverLine = null;
18643
+ let selLine = null;
18644
+ let scale = dayScale(model.from, model.to, 1);
18645
+ let drawnWidth = -1;
18646
+ let hovered = null;
18647
+ let tipKey = "";
18648
+ const fontOf = (n) => {
18649
+ const c = win.getComputedStyle(n);
18650
+ return `${c.fontStyle} ${c.fontWeight} ${c.fontSize} ${c.fontFamily}`;
18651
+ };
18652
+ let measureCtx;
18653
+ const measure = (s, font) => {
18654
+ if (measureCtx === undefined)
18655
+ measureCtx = doc.createElement("canvas").getContext("2d");
18656
+ if (!measureCtx)
18657
+ return s.length * 6.4;
18658
+ measureCtx.font = font;
18659
+ return measureCtx.measureText(s).width;
18660
+ };
18661
+ function drawFoot() {
18662
+ foot.textContent = "";
18663
+ root.toggleAttribute("data-all", showAll);
18664
+ if (quiet.size) {
18665
+ const t = el(doc, "button", "knobs-hy-more");
18666
+ t.type = "button";
18667
+ t.setAttribute("aria-pressed", String(showAll));
18668
+ t.textContent = `${showAll ? "Hide" : "Show"} ${quietLabel(model.events, quiet)}`;
18669
+ t.addEventListener("click", () => {
18670
+ showAll = !showAll;
18671
+ if (!showAll && selected && quiet.has(selected))
18672
+ select(null, true, true);
18673
+ drawFoot();
18674
+ });
18675
+ foot.append(t);
18676
+ }
18677
+ if (model.inferred && model.verified) {
18678
+ const legend = el(doc, "span", "knobs-hy-legend");
18679
+ for (const [label, inferred] of [
18680
+ ["Verified", false],
18681
+ ["Inferred", true]
18682
+ ]) {
18683
+ const k = el(doc, "span", "knobs-hy-key");
18684
+ const sw = el(doc, "span", "knobs-hy-swatch");
18685
+ sw.dataset.tone = "dim";
18686
+ if (inferred)
18687
+ sw.dataset.inferred = "";
18688
+ k.append(sw, text(doc, "span", "", label));
18689
+ legend.append(k);
18690
+ }
18691
+ foot.append(legend);
18692
+ }
18693
+ foot.hidden = !foot.childElementCount;
18694
+ }
18695
+ function draw() {
18696
+ const width = root.clientWidth;
18697
+ drawnWidth = width;
18698
+ const narrow = width > 0 && width < (options.narrow ?? 560);
18699
+ root.toggleAttribute("data-narrow", narrow);
18700
+ root.toggleAttribute("data-bands", model.bands.length > 0);
18701
+ hideHover();
18702
+ chart.textContent = "";
18703
+ nodes = new Map;
18704
+ tracks = new Map;
18705
+ let gutter = 0;
18706
+ if (!narrow) {
18707
+ const probe = el(doc, "div", "knobs-hy-label");
18708
+ chart.append(probe);
18709
+ const font = fontOf(probe);
18710
+ const gap = parseFloat(win.getComputedStyle(probe).marginRight) || 0;
18711
+ probe.remove();
18712
+ const longest = Math.max(0, ...model.lanes.map((l) => measure(l.label, font)));
18713
+ gutter = Math.round(Math.min(Math.max(longest + gap + 2, 64), Math.max(96, width * 0.3), 200));
18714
+ }
18715
+ root.style.setProperty("--hy-gutter", `${gutter}px`);
18716
+ const plotW = Math.max(1, width - gutter);
18717
+ scale = dayScale(model.from, model.to, plotW);
18718
+ const pct = (d) => `${(scale.f(d) * 100).toFixed(4)}%`;
18719
+ const rows = ["auto"];
18720
+ for (let i = 0;i < model.lanes.length; i++)
18721
+ rows.push(...narrow ? ["auto", "var(--hy-lane)"] : ["var(--hy-lane)"]);
18722
+ chart.style.gridTemplateRows = rows.join(" ");
18723
+ const behind = el(doc, "div", "knobs-hy-layer");
18724
+ behind.setAttribute("aria-hidden", "true");
18725
+ for (const b of model.bands) {
18726
+ const c = clipSpan(b.from, b.to, model.from, model.to);
18727
+ if (!c)
18728
+ continue;
18729
+ const band = el(doc, "div", "knobs-hy-band");
18730
+ band.style.left = pct(c.from);
18731
+ band.style.width = pct(model.from + (c.to - c.from));
18732
+ behind.append(band);
18733
+ }
18734
+ const ticks = monthTicks(model.from, model.to, plotW);
18735
+ for (const t of ticks) {
18736
+ const line = el(doc, "div", "knobs-hy-grid");
18737
+ line.style.left = pct(t.day);
18738
+ behind.append(line);
18739
+ }
18740
+ chart.append(behind);
18741
+ const axis = el(doc, "div", "knobs-hy-axis");
18742
+ axis.setAttribute("aria-hidden", "true");
18743
+ const tickEls = ticks.map((t) => {
18744
+ const n = text(doc, "span", "knobs-hy-tick", t.label);
18745
+ n.style.left = pct(t.day);
18746
+ axis.append(n);
18747
+ return n;
18748
+ });
18749
+ const showToday = model.today >= model.from && model.today <= model.to;
18750
+ const now = showToday ? text(doc, "span", "knobs-hy-now", "Today") : null;
18751
+ if (now)
18752
+ axis.append(now);
18753
+ const room = bandRoom(model.bands, scale.x);
18754
+ const bandEls = model.bands.map((b, i) => {
18755
+ const n = text(doc, "span", "knobs-hy-blabel", b.label);
18756
+ n.style.left = pct(Math.max(b.from, model.from));
18757
+ n.style.maxWidth = `${Math.max(0, room[i])}px`;
18758
+ axis.append(n);
18759
+ return n;
18760
+ });
18761
+ chart.append(axis);
18762
+ model.lanes.forEach((lane, i) => {
18763
+ const row = narrow ? 2 + i * 2 : 2 + i;
18764
+ const label = text(doc, "div", "knobs-hy-label", lane.label);
18765
+ label.style.gridRow = String(row);
18766
+ label.title = lane.label;
18767
+ const track = el(doc, "div", "knobs-hy-track");
18768
+ track.style.gridRow = String(narrow ? row + 1 : row);
18769
+ track.dataset.lane = lane.id;
18770
+ tracks.set(lane.id, track);
18771
+ for (const s of lane.spans) {
18772
+ const c = clipSpan(s.from, s.to, model.from, model.to);
18773
+ if (!c)
18774
+ continue;
18775
+ const b = el(doc, "button", "knobs-hy-span");
18776
+ b.type = "button";
18777
+ b.tabIndex = -1;
18778
+ b.dataset.id = s.id;
18779
+ b.dataset.tone = s.tone;
18780
+ if (s.inferred)
18781
+ b.dataset.inferred = "";
18782
+ if (c.clipStart)
18783
+ b.dataset.clipStart = "";
18784
+ if (c.clipEnd)
18785
+ b.dataset.clipEnd = "";
18786
+ b.style.left = pct(c.from);
18787
+ b.style.width = pct(model.from + (c.to - c.from));
18788
+ b.setAttribute("aria-label", `${s.title}, ${when(s)}`);
18789
+ if (s.tags.length)
18790
+ b.append(text(doc, "span", "knobs-hy-tag", s.tags[0]));
18791
+ track.append(b);
18792
+ nodes.set(s.id, b);
18793
+ }
18794
+ for (const e of lane.events) {
18795
+ if (e.to < model.from || e.from > model.to)
18796
+ continue;
18797
+ const b = el(doc, "button", "knobs-hy-ev");
18798
+ b.type = "button";
18799
+ b.tabIndex = -1;
18800
+ b.dataset.id = e.id;
18801
+ b.dataset.tone = e.tone;
18802
+ if (e.type)
18803
+ b.dataset.type = e.type;
18804
+ if (e.inferred)
18805
+ b.dataset.inferred = "";
18806
+ if (quiet.has(e.id))
18807
+ b.dataset.quiet = "";
18808
+ b.style.left = pct(Math.max(e.from, model.from));
18809
+ if (e.to > e.from) {
18810
+ b.dataset.range = "";
18811
+ b.style.setProperty("--hy-w", `${scale.x(model.from + (e.to - e.from)).toFixed(2)}px`);
18812
+ }
18813
+ b.setAttribute("aria-label", `${when(e)}: ${e.title}`);
18814
+ track.append(b);
18815
+ nodes.set(e.id, b);
18816
+ }
18817
+ chart.append(label, track);
18818
+ });
18819
+ over = el(doc, "div", "knobs-hy-layer");
18820
+ over.setAttribute("aria-hidden", "true");
18821
+ if (showToday) {
18822
+ const today = el(doc, "div", "knobs-hy-today");
18823
+ today.style.left = pct(model.today);
18824
+ over.append(today);
18825
+ }
18826
+ selLine = el(doc, "div", "knobs-hy-sel");
18827
+ hoverLine = el(doc, "div", "knobs-hy-hover");
18828
+ over.append(selLine, hoverLine);
18829
+ chart.append(over);
18830
+ const reserved = [];
18831
+ if (now) {
18832
+ const w = now.offsetWidth;
18833
+ const x0 = Math.max(0, Math.min(plotW - w, scale.x(model.today) - w / 2));
18834
+ now.style.left = `${x0}px`;
18835
+ reserved.push({ x0, x1: x0 + w });
18836
+ }
18837
+ const fits = fitLabels(ticks.map((t, i) => ({ x: scale.x(t.day), w: tickEls[i].offsetWidth })), plotW, reserved);
18838
+ tickEls.forEach((n, i) => n.hidden = !fits[i]);
18839
+ for (const n of bandEls)
18840
+ n.toggleAttribute("data-overflow", n.scrollWidth > n.clientWidth + 1);
18841
+ for (const n of chart.querySelectorAll(".knobs-hy-label"))
18842
+ n.toggleAttribute("data-overflow", n.scrollWidth > n.clientWidth + 1);
18843
+ for (const lane of model.lanes) {
18844
+ const spans = lane.spans.map((s) => ({ s, c: clipSpan(s.from, s.to, model.from, model.to) })).filter((x) => x.c).sort((a, b) => a.c.from - b.c.from);
18845
+ spans.forEach(({ s, c }, i) => {
18846
+ const b = nodes.get(s.id);
18847
+ const tag = b?.querySelector(".knobs-hy-tag");
18848
+ if (!b || !tag)
18849
+ return;
18850
+ const font = fontOf(tag);
18851
+ const widths = s.tags.map((t) => measure(t, font));
18852
+ const next = spans[i + 1]?.c;
18853
+ const spanW = scale.x(c.to) - scale.x(c.from);
18854
+ const after = (next ? scale.x(next.from) : plotW) - scale.x(c.to);
18855
+ const fit = fitTag(widths, spanW, after);
18856
+ tag.textContent = fit ? s.tags[fit.index] : s.tags[0] ?? "";
18857
+ tag.dataset.place = fit ? fit.place : "none";
18858
+ });
18859
+ }
18860
+ paintSelection();
18861
+ }
18862
+ const h = new Spring(0, HEIGHT2.omega, HEIGHT2.zeta, 0.25);
18863
+ let goal = 0;
18864
+ let content = null;
18865
+ const loop = frameLoop(win, (dt) => {
18866
+ h.step(dt);
18867
+ const moving = !h.resting;
18868
+ detail.style.height = moving ? `${Math.max(0, h.x).toFixed(2)}px` : goal > 0 ? "" : "0px";
18869
+ detail.toggleAttribute("data-moving", moving);
18870
+ return moving;
18871
+ });
18872
+ function buildDetail(it) {
18873
+ const c = el(doc, "div", "knobs-hy-dc");
18874
+ const meta = el(doc, "div", "knobs-hy-dmeta");
18875
+ meta.append(text(doc, "time", "", when(it)));
18876
+ const lane = model.lanes.find((l) => l.id === it.lane);
18877
+ if (lane && (model.lanes.length > 1 || lane.id !== "code"))
18878
+ meta.append(text(doc, "span", "", lane.label));
18879
+ if (it.type)
18880
+ meta.append(text(doc, "span", "", typeName(it.type)));
18881
+ if (it.provenance || it.source) {
18882
+ const p = text(doc, "span", "", [it.provenance, it.source].filter(Boolean).join(" · "));
18883
+ if (it.inferred) {
18884
+ p.dataset.tone = "warn";
18885
+ p.title = "Worked out from behaviour, not seen in the record";
18886
+ }
18887
+ meta.append(p);
18888
+ }
18889
+ c.append(meta, text(doc, "div", "knobs-hy-dtitle", it.title));
18890
+ if (it.detail) {
18891
+ const p = el(doc, "p", "knobs-hy-dtext");
18892
+ p.innerHTML = inlineMarkdown(it.detail);
18893
+ c.append(p);
18894
+ }
18895
+ const foot = el(doc, "div", "knobs-hy-dfoot");
18896
+ for (const e of it.evidence) {
18897
+ const tag = text(doc, e.href ? "a" : "span", "knobs-hy-evidence", e.label);
18898
+ if (tag instanceof HTMLAnchorElement && e.href) {
18899
+ tag.href = e.href;
18900
+ tag.target = "_blank";
18901
+ tag.rel = "noreferrer";
18902
+ }
18903
+ foot.append(tag);
18904
+ }
18905
+ if (it.step && options.onJump) {
18906
+ const go = el(doc, "button", "knobs-hy-jump");
18907
+ go.type = "button";
18908
+ go.append(text(doc, "span", "", "Go to step"));
18909
+ go.insertAdjacentHTML("beforeend", GO);
18910
+ const step = it.step;
18911
+ go.addEventListener("click", () => options.onJump?.(step));
18912
+ foot.append(go);
18913
+ }
18914
+ c.append(foot);
18915
+ return c;
18916
+ }
18917
+ function showDetail(it, animate) {
18918
+ const from = detail.offsetHeight;
18919
+ const old = content;
18920
+ const next = it ? buildDetail(it) : null;
18921
+ if (old) {
18922
+ if (animate) {
18923
+ old.dataset.leave = "";
18924
+ const drop = () => old.remove();
18925
+ old.addEventListener("transitionend", drop, { once: true });
18926
+ win.setTimeout(drop, 400);
18927
+ } else
18928
+ old.remove();
18929
+ }
18930
+ content = next;
18931
+ if (next) {
18932
+ detail.append(next);
18933
+ if (animate) {
18934
+ next.dataset.enter = "";
18935
+ next.offsetHeight;
18936
+ delete next.dataset.enter;
18937
+ }
18938
+ }
18939
+ goal = next ? next.offsetHeight : 0;
18940
+ if (!animate || reducedMotion(win)) {
18941
+ loop.stop();
18942
+ h.snap(goal);
18943
+ detail.style.height = goal > 0 ? "" : "0px";
18944
+ detail.removeAttribute("data-moving");
18945
+ return;
18946
+ }
18947
+ if (h.resting)
18948
+ h.snap(from);
18949
+ h.set(goal);
18950
+ loop.wake();
18951
+ }
18952
+ function paintSelection() {
18953
+ for (const [id, n] of nodes)
18954
+ n.toggleAttribute("data-selected", id === selected);
18955
+ const it = selected ? model.items.get(selected) : undefined;
18956
+ if (selLine) {
18957
+ const on = it?.kind === "event";
18958
+ selLine.toggleAttribute("data-on", on);
18959
+ if (on)
18960
+ selLine.style.left = `${(scale.f(it.from) * 100).toFixed(4)}%`;
18961
+ }
18962
+ }
18963
+ function select(id, animate, notify) {
18964
+ const next = id && model.items.has(id) ? id : null;
18965
+ if (next === selected)
18966
+ return;
18967
+ selected = next;
18968
+ paintSelection();
18969
+ showDetail(next ? model.items.get(next) : null, animate);
18970
+ if (notify)
18971
+ options.onSelect?.(next);
18972
+ }
18973
+ function itemAt(e) {
18974
+ if (!over)
18975
+ return null;
18976
+ const r = over.getBoundingClientRect();
18977
+ if (!r.width)
18978
+ return null;
18979
+ const x = Math.max(0, Math.min(r.width, e.clientX - r.left));
18980
+ const target = e.target instanceof Element ? e.target : null;
18981
+ const hit = target?.closest("[data-id]");
18982
+ if (hit && chart.contains(hit))
18983
+ return { it: model.items.get(hit.dataset.id) ?? null, x };
18984
+ const laneId = target?.closest(".knobs-hy-track")?.dataset.lane;
18985
+ const lane = laneId ? model.lanes.find((l) => l.id === laneId) : undefined;
18986
+ if (lane) {
18987
+ const reach = REACH / r.width * (model.to - model.from);
18988
+ const near = nearestEvent(lane.events, scale.day(x), reach);
18989
+ if (near)
18990
+ return { it: near, x };
18991
+ }
18992
+ return { it: null, x };
18993
+ }
18994
+ function hideHover() {
18995
+ hoverLine?.removeAttribute("data-on");
18996
+ tip.removeAttribute("data-on");
18997
+ hovered?.removeAttribute("data-hover");
18998
+ hovered = null;
18999
+ tipKey = "";
19000
+ }
19001
+ function hover(e) {
19002
+ const at = itemAt(e);
19003
+ if (!at || !over || !hoverLine)
19004
+ return hideHover();
19005
+ const { it, x } = at;
19006
+ const lineX = it?.kind === "event" ? scale.x(it.from) : x;
19007
+ hoverLine.style.left = `${lineX.toFixed(2)}px`;
19008
+ hoverLine.setAttribute("data-on", "");
19009
+ const node = it ? nodes.get(it.id) ?? null : null;
19010
+ if (node !== hovered) {
19011
+ hovered?.removeAttribute("data-hover");
19012
+ node?.setAttribute("data-hover", "");
19013
+ hovered = node;
19014
+ }
19015
+ const key = it ? it.id : `d${Math.round(scale.day(x))}`;
19016
+ if (key !== tipKey) {
19017
+ tipKey = key;
19018
+ tipTitle.textContent = it ? it.title : formatDay(Math.round(scale.day(x)));
19019
+ tipDate.textContent = it ? [when(it), it.type ? typeName(it.type) : ""].filter(Boolean).join(" · ") : "";
19020
+ tipDate.hidden = !it;
19021
+ tipLine.textContent = it ? firstSentence(it.detail) : "";
19022
+ }
19023
+ const rr = root.getBoundingClientRect();
19024
+ const or = over.getBoundingClientRect();
19025
+ const laneId = it?.lane ?? (e.target instanceof Element ? e.target.closest(".knobs-hy-track")?.dataset.lane : undefined);
19026
+ const lr = laneId && tracks.get(laneId)?.getBoundingClientRect() || or;
19027
+ const tw = tip.offsetWidth;
19028
+ const th = tip.offsetHeight;
19029
+ const left = Math.max(0, Math.min(rr.width - tw, or.left - rr.left + lineX - tw / 2));
19030
+ let top = lr.top - rr.top - th - 6;
19031
+ if (top + rr.top < 0)
19032
+ top = lr.bottom - rr.top + 6;
19033
+ tip.style.transform = `translate3d(${Math.round(left)}px,${Math.round(top)}px,0)`;
19034
+ tip.setAttribute("data-on", "");
19035
+ }
19036
+ chart.addEventListener("pointermove", (e) => {
19037
+ if (e.pointerType === "touch")
19038
+ return;
19039
+ hover(e);
19040
+ });
19041
+ chart.addEventListener("pointerleave", hideHover);
19042
+ chart.addEventListener("mousedown", (e) => {
19043
+ if (!(e.target instanceof Element) || !e.target.closest("button"))
19044
+ return;
19045
+ e.preventDefault();
19046
+ root.focus({ preventScroll: true });
19047
+ });
19048
+ chart.addEventListener("click", (e) => {
19049
+ const at = itemAt(e);
19050
+ const id = at?.it?.id ?? null;
19051
+ select(id !== null && id === selected ? null : id, true, true);
19052
+ });
19053
+ root.addEventListener("keydown", (e) => {
19054
+ const onChart = e.target instanceof Node && chart.contains(e.target);
19055
+ if (e.target !== root && !onChart || e.metaKey || e.ctrlKey || e.altKey)
19056
+ return;
19057
+ let next;
19058
+ const order = walked();
19059
+ const at = selected ? model.items.get(selected)?.from : undefined;
19060
+ if (e.key === "ArrowRight")
19061
+ next = stepEvent(order, selected, 1, at);
19062
+ else if (e.key === "ArrowLeft")
19063
+ next = stepEvent(order, selected, -1, at);
19064
+ else if (e.key === "Home")
19065
+ next = order[0] ?? null;
19066
+ else if (e.key === "End")
19067
+ next = order[order.length - 1] ?? null;
19068
+ else if (e.key === "Escape" && selected)
19069
+ next = null;
19070
+ else if (e.key === "Enter" && selected) {
19071
+ const step = model.items.get(selected)?.step;
19072
+ if (!step || !options.onJump)
19073
+ return;
19074
+ options.onJump(step);
19075
+ e.preventDefault();
19076
+ e.stopPropagation();
19077
+ return;
19078
+ } else
19079
+ return;
19080
+ e.preventDefault();
19081
+ e.stopPropagation();
19082
+ hideHover();
19083
+ select(next?.id ?? null, true, true);
19084
+ });
19085
+ let raf = 0;
19086
+ const ro = new ResizeObserver(() => {
19087
+ if (raf || disposed)
19088
+ return;
19089
+ raf = win.requestAnimationFrame(() => {
19090
+ raf = 0;
19091
+ if (!disposed && root.clientWidth !== drawnWidth)
19092
+ draw();
19093
+ });
19094
+ });
19095
+ ro.observe(root);
19096
+ const restyle = () => draw();
19097
+ root.addEventListener("knobs-restyle", restyle);
19098
+ drawFoot();
19099
+ draw();
19100
+ detail.style.height = "0px";
19101
+ if (selected)
19102
+ showDetail(model.items.get(selected), false);
19103
+ const dispose = () => {
19104
+ if (disposed)
19105
+ return;
19106
+ disposed = true;
19107
+ ro.disconnect();
19108
+ if (raf)
19109
+ win.cancelAnimationFrame(raf);
19110
+ loop.stop();
19111
+ root.removeEventListener("knobs-restyle", restyle);
19112
+ root.remove();
19113
+ };
19114
+ return {
19115
+ element: root,
19116
+ get model() {
19117
+ return model;
19118
+ },
19119
+ get selected() {
19120
+ return selected;
19121
+ },
19122
+ select(id) {
19123
+ select(id, true, false);
19124
+ },
19125
+ set(data) {
19126
+ model = normalizeHistory(data);
19127
+ const keep = selected && model.items.has(selected) ? selected : null;
19128
+ selected = keep;
19129
+ quiet = quietEvents(model.events, options.marks ?? DEFAULT_MARKS);
19130
+ drawFoot();
19131
+ draw();
19132
+ showDetail(keep ? model.items.get(keep) : null, false);
19133
+ },
19134
+ focus() {
19135
+ root.focus();
19136
+ },
19137
+ dispose,
19138
+ destroy: dispose
19139
+ };
19140
+ }
19141
+
19142
+ // src/ui/walkthrough-blocks.ts
19143
+ init_icons();
19144
+ init_look();
19145
+ init_selection_pill();
19146
+ var GLYPH = {
19147
+ caret: icon("caretRight", { small: true })
19148
+ };
19149
+ var el2 = (doc, tag, cls) => {
19150
+ const n = doc.createElement(tag);
19151
+ if (cls)
19152
+ n.className = cls;
19153
+ return n;
19154
+ };
19155
+ var text2 = (doc, tag, cls, value) => {
19156
+ const n = el2(doc, tag, cls);
19157
+ n.textContent = value;
19158
+ return n;
19159
+ };
17664
19160
  var prose = (doc, tag, cls, value) => {
17665
- const n = el(doc, tag, cls);
19161
+ const n = el2(doc, tag, cls);
17666
19162
  n.innerHTML = inlineMarkdown(value);
17667
19163
  return n;
17668
19164
  };
17669
19165
  var link = (doc, cls, label, href) => {
17670
19166
  if (!href)
17671
- return text(doc, "span", cls, label);
17672
- const a = text(doc, "a", cls, label);
19167
+ return text2(doc, "span", cls, label);
19168
+ const a = text2(doc, "a", cls, label);
17673
19169
  a.href = href;
17674
19170
  a.target = "_blank";
17675
19171
  a.rel = "noreferrer";
17676
19172
  return a;
17677
19173
  };
17678
- function stepButton(doc, ref, steps) {
19174
+ function rowLink(row, ref, steps) {
17679
19175
  const s = ref && steps?.find(ref);
17680
19176
  if (!s || !steps)
17681
- return null;
17682
- const b = el(doc, "button", "knobs-wt-steplink");
17683
- b.innerHTML = GLYPH.go;
17684
- b.type = "button";
17685
- b.title = `Go to “${s.title}”`;
17686
- b.setAttribute("aria-label", b.title);
17687
- b.addEventListener("click", (e) => {
17688
- e.stopPropagation();
19177
+ return;
19178
+ const doc = row.ownerDocument;
19179
+ row.dataset.go = "";
19180
+ row.tabIndex = 0;
19181
+ row.title = `Go to “${plainMarkdown(s.title)}”`;
19182
+ row.addEventListener("click", (e) => {
19183
+ if (e.target?.closest("a,button"))
19184
+ return;
19185
+ if (doc.getSelection()?.toString())
19186
+ return;
19187
+ steps.go(s.key);
19188
+ });
19189
+ row.addEventListener("keydown", (e) => {
19190
+ if (e.target !== row || e.key !== "Enter" && e.key !== " ")
19191
+ return;
19192
+ e.preventDefault();
17689
19193
  steps.go(s.key);
17690
19194
  });
17691
- return b;
17692
19195
  }
17693
- var tags = (doc, list) => {
17694
- if (!list?.length)
17695
- return null;
17696
- const row = el(doc, "div", "knobs-wt-tags");
17697
- for (const e of list)
17698
- row.append(link(doc, "knobs-wt-tag", e.label, e.href));
17699
- return row;
17700
- };
17701
19196
  function mountHistory(host, m, steps) {
17702
- const doc = host.ownerDocument;
17703
- const root = el(doc, "div", "knobs-wt-history");
17704
- const longest = Math.max(6, ...m.groups.flatMap((g) => g.events.flatMap((e) => [e.date.length, e.age?.length ?? 0])));
17705
- root.style.setProperty("--kw-hw", `${Math.min(16, longest + 1)}ch`);
17706
- for (const g of m.groups) {
17707
- const group = el(doc, "section", "knobs-wt-hgroup");
17708
- if (g.heading || g.summary) {
17709
- const head = el(doc, "header", "knobs-wt-hhead");
17710
- if (g.heading)
17711
- head.append(text(doc, "h4", "knobs-wt-hheading", g.heading));
17712
- if (g.summary)
17713
- head.append(text(doc, "span", "knobs-wt-hsum", g.summary));
17714
- group.append(head);
17715
- }
17716
- const list = el(doc, "ol", "knobs-wt-hlist");
17717
- for (const e of g.events)
17718
- list.append(historyEvent(doc, e, steps));
17719
- group.append(list);
17720
- root.append(group);
17721
- }
17722
- host.append(root);
17723
- return { dispose: () => root.remove(), context: () => ({ kind: "history" }) };
17724
- }
17725
- function historyEvent(doc, e, steps) {
17726
- const li = el(doc, "li", "knobs-wt-hev");
17727
- if (e.tone)
17728
- li.dataset.tone = e.tone;
17729
- if (e.type)
17730
- li.dataset.type = e.type;
17731
- if (e.provenance)
17732
- li.dataset.provenance = e.provenance;
17733
- const when = el(doc, "div", "knobs-wt-hwhen");
17734
- when.append(text(doc, "time", "knobs-wt-hdate", e.date));
17735
- if (e.age)
17736
- when.append(text(doc, "span", "knobs-wt-hage", e.age));
17737
- const spine = el(doc, "span", "knobs-wt-hspine");
17738
- spine.setAttribute("aria-hidden", "true");
17739
- spine.append(el(doc, "span", "knobs-wt-hdot"));
17740
- const main = el(doc, "div", "knobs-wt-hmain");
17741
- const title = el(doc, "div", "knobs-wt-htitle");
17742
- title.append(text(doc, "span", "knobs-wt-htext", e.title));
17743
- if (e.provenance || e.source) {
17744
- const p = text(doc, "span", "knobs-wt-prov", [e.provenance, e.source].filter(Boolean).join(" · "));
17745
- if (e.provenance === "inferred")
17746
- p.title = "Inferred from behaviour, not seen in the record";
17747
- else if (e.provenance === "verified")
17748
- p.title = "Seen in the record";
17749
- title.append(p);
17750
- }
17751
- const go = stepButton(doc, e.step, steps);
17752
- if (go)
17753
- title.append(go);
17754
- main.append(title);
17755
- if (e.detail)
17756
- main.append(prose(doc, "p", "knobs-wt-hdetail", e.detail));
17757
- const t = tags(doc, e.evidence);
17758
- if (t)
17759
- main.append(t);
17760
- li.append(when, spine, main);
17761
- return li;
19197
+ const h = history(host, {
19198
+ data: m.data,
19199
+ ...m.label ? { label: m.label } : {},
19200
+ ...m.marks ? { marks: m.marks } : {},
19201
+ ...steps ? {
19202
+ onJump: (ref) => {
19203
+ const s = steps.find(ref);
19204
+ if (s)
19205
+ steps.go(s.key);
19206
+ }
19207
+ } : {}
19208
+ });
19209
+ return {
19210
+ dispose: () => h.dispose(),
19211
+ context: () => ({ kind: "history" })
19212
+ };
17762
19213
  }
17763
19214
  function mountStack(host, m, steps) {
17764
19215
  const doc = host.ownerDocument;
17765
19216
  const layout = stackLayout(m);
17766
- const root = el(doc, "div", "knobs-wt-stack");
19217
+ const root = el2(doc, "div", "knobs-wt-stack");
17767
19218
  root.toggleAttribute("data-flat", layout.flat);
17768
19219
  const lane = (kind) => {
17769
- const s = el(doc, "span", "knobs-wt-lane");
19220
+ const s = el2(doc, "span", "knobs-wt-lane");
17770
19221
  s.dataset.rail = kind;
17771
19222
  return s;
17772
19223
  };
@@ -17777,19 +19228,19 @@ function mountStack(host, m, steps) {
17777
19228
  };
17778
19229
  const from = layout.trunk ? 0 : 1;
17779
19230
  if (layout.trunk && layout.rows.length) {
17780
- const row = el(doc, "div", "knobs-wt-srow");
19231
+ const row = el2(doc, "div", "knobs-wt-srow");
17781
19232
  row.dataset.trunk = "";
17782
- const lanes = el(doc, "span", "knobs-wt-lanes");
19233
+ const lanes = el2(doc, "span", "knobs-wt-lanes");
17783
19234
  lanes.setAttribute("aria-hidden", "true");
17784
19235
  lanes.append(node(true));
17785
- row.append(lanes, text(doc, "span", "knobs-wt-trunk", layout.trunk));
19236
+ row.append(lanes, text2(doc, "span", "knobs-wt-trunk", layout.trunk));
17786
19237
  root.append(row);
17787
19238
  }
17788
19239
  for (const r of layout.rows) {
17789
19240
  const it = r.item;
17790
- const row = el(doc, "div", "knobs-wt-srow");
19241
+ const row = el2(doc, "div", "knobs-wt-srow");
17791
19242
  if (!layout.flat) {
17792
- const lanes = el(doc, "span", "knobs-wt-lanes");
19243
+ const lanes = el2(doc, "span", "knobs-wt-lanes");
17793
19244
  lanes.setAttribute("aria-hidden", "true");
17794
19245
  for (let k = from;k < r.depth; k++)
17795
19246
  lanes.append(lane(r.rails[k] ? "pass" : "none"));
@@ -17798,24 +19249,21 @@ function mountStack(host, m, steps) {
17798
19249
  lanes.append(node(r.children > 0));
17799
19250
  row.append(lanes);
17800
19251
  }
17801
- const main = el(doc, "div", "knobs-wt-smain");
17802
- const line = el(doc, "div", "knobs-wt-sline");
19252
+ const main = el2(doc, "div", "knobs-wt-smain");
19253
+ const line = el2(doc, "div", "knobs-wt-sline");
17803
19254
  if (it.order)
17804
- line.append(text(doc, "span", "knobs-wt-sord", it.order));
19255
+ line.append(text2(doc, "span", "knobs-wt-sord", it.order));
17805
19256
  line.append(link(doc, "knobs-wt-spr", it.pr, it.href));
17806
19257
  const target = it.step ? steps?.find(it.step) : null;
17807
19258
  if (target && steps) {
17808
- const title = text(doc, "button", "knobs-wt-stitle", it.title);
19259
+ const title = text2(doc, "button", "knobs-wt-stitle", it.title);
17809
19260
  title.type = "button";
17810
- title.title = `Go to “${target.title}”`;
19261
+ title.title = `Go to “${plainMarkdown(target.title)}”`;
17811
19262
  title.addEventListener("click", (e) => {
17812
19263
  e.stopPropagation();
17813
19264
  steps.go(target.key);
17814
19265
  });
17815
19266
  line.append(title);
17816
- const go = stepButton(doc, it.step, steps);
17817
- if (go)
17818
- line.append(go);
17819
19267
  row.dataset.go = "";
17820
19268
  row.addEventListener("click", (e) => {
17821
19269
  const t = e.target;
@@ -17826,14 +19274,14 @@ function mountStack(host, m, steps) {
17826
19274
  steps.go(target.key);
17827
19275
  });
17828
19276
  } else
17829
- line.append(text(doc, "span", "knobs-wt-stitle", it.title));
19277
+ line.append(text2(doc, "span", "knobs-wt-stitle", it.title));
17830
19278
  main.append(line);
17831
19279
  if (it.summary)
17832
19280
  main.append(prose(doc, "p", "knobs-wt-ssummary", it.summary));
17833
- const meta = el(doc, "div", "knobs-wt-smeta");
19281
+ const meta = el2(doc, "div", "knobs-wt-smeta");
17834
19282
  if (it.head) {
17835
19283
  const branch = r.offBase ? `${it.head} → ${r.offBase}` : it.head;
17836
- meta.append(text(doc, "span", "knobs-wt-sbranch", branch));
19284
+ meta.append(text2(doc, "span", "knobs-wt-sbranch", branch));
17837
19285
  }
17838
19286
  if (it.delta !== undefined)
17839
19287
  meta.append(delta(doc, it.delta));
@@ -17841,7 +19289,7 @@ function mountStack(host, m, steps) {
17841
19289
  main.append(meta);
17842
19290
  row.append(main);
17843
19291
  if (it.status) {
17844
- const chip = text(doc, "span", "knobs-wt-chip knobs-wt-sstatus", it.status);
19292
+ const chip = text2(doc, "span", "knobs-wt-chip knobs-wt-sstatus", it.status);
17845
19293
  chip.dataset.tone = it.tone ?? "dim";
17846
19294
  row.append(chip);
17847
19295
  }
@@ -17851,40 +19299,38 @@ function mountStack(host, m, steps) {
17851
19299
  return { dispose: () => root.remove(), context: () => ({ kind: "stack" }) };
17852
19300
  }
17853
19301
  function delta(doc, d) {
17854
- const s = el(doc, "span", "knobs-wt-delta");
19302
+ const s = el2(doc, "span", "knobs-wt-delta");
17855
19303
  if (typeof d === "string") {
17856
19304
  s.textContent = d;
17857
19305
  return s;
17858
19306
  }
17859
19307
  const n = (v) => v.toLocaleString("en-US");
17860
- s.append(text(doc, "span", "knobs-wt-add", `+${n(d.add)}`));
17861
- s.append(text(doc, "span", "knobs-wt-del", `−${n(d.del)}`));
19308
+ s.append(text2(doc, "span", "knobs-wt-add", `+${n(d.add)}`));
19309
+ s.append(text2(doc, "span", "knobs-wt-del", `−${n(d.del)}`));
17862
19310
  s.setAttribute("aria-label", `${d.add} added, ${d.del} removed`);
17863
19311
  return s;
17864
19312
  }
17865
- function cellContent(doc, into, cell, steps) {
19313
+ function cellContent(doc, into, cell) {
17866
19314
  let content;
17867
19315
  if (cell.badge) {
17868
19316
  content = link(doc, "knobs-wt-chip", cell.text, cell.href);
17869
19317
  content.dataset.tone = cell.badge;
17870
19318
  } else {
17871
- const inner = cell.code ? text(doc, "code", "", cell.text) : null;
19319
+ const inner = cell.code ? text2(doc, "code", "", cell.text) : null;
17872
19320
  if (cell.href) {
17873
19321
  content = link(doc, "knobs-wt-clink", inner ? "" : cell.text, cell.href);
17874
19322
  if (inner)
17875
19323
  content.append(inner);
17876
19324
  } else
17877
- content = inner ?? text(doc, "span", "", cell.text);
19325
+ content = inner ?? text2(doc, "span", "", cell.text);
17878
19326
  if (cell.tone) {
17879
19327
  content.classList.add("knobs-wt-ctone");
17880
19328
  content.dataset.tone = cell.tone;
17881
19329
  }
17882
19330
  }
17883
19331
  into.append(content);
17884
- const go = stepButton(doc, cell.step, steps);
17885
- if (go)
17886
- into.append(go);
17887
19332
  }
19333
+ var rowStep = (r, columns) => columns.map((c) => r[c.key]?.step).find((step) => step !== undefined);
17888
19334
  var cellAttrs = (td, c) => {
17889
19335
  td.dataset.align = c.align;
17890
19336
  if (c.mono)
@@ -17893,83 +19339,85 @@ var cellAttrs = (td, c) => {
17893
19339
  function mountTable(host, m, steps) {
17894
19340
  const doc = host.ownerDocument;
17895
19341
  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");
19342
+ const box = el2(doc, "div", "knobs-wt-tablebox");
19343
+ const scroller = el2(doc, "div", "knobs-wt-table");
19344
+ const table = el2(doc, "table");
17899
19345
  if (m.caption)
17900
- table.append(text(doc, "caption", "", m.caption));
17901
- const head = el(doc, "thead");
17902
- const hr = el(doc, "tr");
19346
+ table.append(text2(doc, "caption", "", m.caption));
19347
+ const head = el2(doc, "thead");
19348
+ const hr = el2(doc, "tr");
17903
19349
  for (const c of m.columns) {
17904
- const th = text(doc, "th", "", c.label);
19350
+ const th = text2(doc, "th", "", c.label);
17905
19351
  th.scope = "col";
17906
19352
  th.dataset.align = c.align;
17907
19353
  hr.append(th);
17908
19354
  }
17909
19355
  head.append(hr);
17910
- const body = el(doc, "tbody");
19356
+ const body = el2(doc, "tbody");
17911
19357
  for (const r of m.rows) {
17912
- const tr = el(doc, "tr");
19358
+ const tr = el2(doc, "tr");
17913
19359
  for (const c of m.columns) {
17914
- const td = el(doc, "td");
19360
+ const td = el2(doc, "td");
17915
19361
  cellAttrs(td, c);
17916
19362
  const cell = r[c.key];
17917
19363
  if (cell) {
17918
19364
  if (cell.text.length > 40 && !c.mono && !cell.code)
17919
19365
  td.dataset.long = "";
17920
- cellContent(doc, td, cell, steps);
19366
+ cellContent(doc, td, cell);
17921
19367
  }
17922
19368
  tr.append(td);
17923
19369
  }
19370
+ rowLink(tr, rowStep(r, m.columns), steps);
17924
19371
  body.append(tr);
17925
19372
  }
17926
19373
  table.append(head, body);
17927
19374
  scroller.append(table);
17928
- const stack = el(doc, "div", "knobs-wt-tstack");
19375
+ const stack = el2(doc, "div", "knobs-wt-tstack");
17929
19376
  stack.setAttribute("role", "list");
17930
19377
  if (m.caption)
17931
19378
  stack.setAttribute("aria-label", m.caption);
17932
19379
  const primary = m.columns.find((c) => c.key === m.primary) ?? m.columns[0];
17933
19380
  for (const r of m.rows) {
17934
- const item = el(doc, "div", "knobs-wt-trow");
19381
+ const item = el2(doc, "div", "knobs-wt-trow");
17935
19382
  item.setAttribute("role", "listitem");
17936
- const title = el(doc, "div", "knobs-wt-ttitle");
19383
+ const title = el2(doc, "div", "knobs-wt-ttitle");
17937
19384
  if (primary) {
17938
19385
  cellAttrs(title, primary);
17939
19386
  const cell = r[primary.key];
17940
19387
  if (cell)
17941
- cellContent(doc, title, cell, steps);
19388
+ cellContent(doc, title, cell);
17942
19389
  }
17943
- const line = el(doc, "div", "knobs-wt-tline");
19390
+ const line = el2(doc, "div", "knobs-wt-tline");
17944
19391
  line.append(title);
17945
- const badges = el(doc, "div", "knobs-wt-tbadges");
19392
+ const badges = el2(doc, "div", "knobs-wt-tbadges");
17946
19393
  for (const c of m.columns) {
17947
19394
  const cell = r[c.key];
17948
19395
  if (c !== primary && cell?.badge)
17949
- cellContent(doc, badges, cell, steps);
19396
+ cellContent(doc, badges, cell);
17950
19397
  }
17951
19398
  if (badges.childNodes.length)
17952
19399
  line.append(badges);
17953
19400
  item.append(line);
17954
- const terms = el(doc, "dl", "knobs-wt-tterms");
19401
+ const terms = el2(doc, "dl", "knobs-wt-tterms");
17955
19402
  for (const c of m.columns) {
17956
19403
  if (c === primary)
17957
19404
  continue;
17958
19405
  const cell = r[c.key];
17959
19406
  if (!cell || cell.text === "" || cell.badge)
17960
19407
  continue;
17961
- const dd = el(doc, "dd");
19408
+ const dd = el2(doc, "dd");
17962
19409
  cellAttrs(dd, c);
17963
19410
  dd.removeAttribute("data-align");
17964
- cellContent(doc, dd, cell, steps);
17965
- terms.append(text(doc, "dt", "", c.label), dd);
19411
+ cellContent(doc, dd, cell);
19412
+ terms.append(text2(doc, "dt", "", c.label), dd);
17966
19413
  }
17967
19414
  if (terms.childNodes.length)
17968
19415
  item.append(terms);
19416
+ rowLink(item, rowStep(r, m.columns), steps);
17969
19417
  stack.append(item);
17970
19418
  }
17971
19419
  if (m.caption)
17972
- stack.append(text(doc, "p", "knobs-wt-tcaption", m.caption));
19420
+ stack.append(text2(doc, "p", "knobs-wt-tcaption", m.caption));
17973
19421
  box.append(scroller, stack);
17974
19422
  host.append(box);
17975
19423
  let natural = 0;
@@ -18023,16 +19471,16 @@ function mountTable(host, m, steps) {
18023
19471
  }
18024
19472
  function chapterPills(host, chapters, io) {
18025
19473
  const doc = host.ownerDocument;
18026
- const list = el(doc, "div", "knobs-wt-chapters");
19474
+ const list = el2(doc, "div", "knobs-wt-chapters");
18027
19475
  list.setAttribute("role", "group");
18028
19476
  list.setAttribute("aria-label", "Chapters");
18029
19477
  let at = -2;
18030
19478
  const rows = chapters.map((c, i) => {
18031
- const b = el(doc, "button", "knobs-wt-chapter");
19479
+ const b = el2(doc, "button", "knobs-wt-chapter");
18032
19480
  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)));
19481
+ b.append(text2(doc, "span", "knobs-wt-chapter-n", String(i + 1)));
19482
+ b.append(text2(doc, "span", "knobs-wt-chapter-label", c.label));
19483
+ b.append(text2(doc, "span", "knobs-wt-chapter-time", clock(c.start)));
18036
19484
  b.addEventListener("click", () => {
18037
19485
  io.onEvent?.({ what: "seek", time: Math.round(c.start * 100) / 100, detail: c.label });
18038
19486
  io.seek(c.start);
@@ -18070,15 +19518,13 @@ function disclosure(host, options) {
18070
19518
  const doc = host.ownerDocument;
18071
19519
  const win = doc.defaultView;
18072
19520
  let open = options.open === true;
18073
- const root = el(doc, "div", "knobs-wt-disc");
18074
- const toggle = el(doc, "button", "knobs-wt-disc-toggle");
19521
+ const root = el2(doc, "div", "knobs-wt-disc");
19522
+ const toggle = el2(doc, "button", "knobs-wt-disc-toggle");
18075
19523
  toggle.type = "button";
18076
19524
  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");
19525
+ toggle.append(text2(doc, "span", "", options.label));
19526
+ const viewport = el2(doc, "div", "knobs-wt-disc-body");
19527
+ const content = el2(doc, "div", "knobs-wt-disc-content");
18082
19528
  content.append(options.content);
18083
19529
  viewport.append(content);
18084
19530
  root.append(toggle, viewport);
@@ -18090,7 +19536,9 @@ function disclosure(host, options) {
18090
19536
  const k = full > 0 ? Math.max(0, Math.min(1, h.x / full)) : open ? 1 : 0;
18091
19537
  viewport.style.height = !moving && open ? "" : `${Math.max(0, h.x).toFixed(2)}px`;
18092
19538
  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);
19539
+ const o = open ? Math.min(1, k * 1.25) : Math.max(0, k * 1.5 - 0.5);
19540
+ content.style.opacity = o.toFixed(3);
19541
+ content.style.filter = o >= 1 ? "" : `blur(${((1 - o) * 3).toFixed(2)}px)`;
18094
19542
  };
18095
19543
  const loop = frameLoop(win, (dt) => {
18096
19544
  h.step(dt);
@@ -18126,6 +19574,7 @@ function disclosure(host, options) {
18126
19574
  h.snap(open ? full : 0);
18127
19575
  viewport.style.height = open ? "" : "0px";
18128
19576
  content.style.opacity = open ? "1" : "0";
19577
+ content.style.filter = "";
18129
19578
  return;
18130
19579
  }
18131
19580
  if (h.resting)
@@ -18142,10 +19591,79 @@ function disclosure(host, options) {
18142
19591
  toggle.addEventListener("click", () => api.toggle());
18143
19592
  return api;
18144
19593
  }
19594
+ var PR_PATHS = {
19595
+ pull: "M104,64A32,32,0,1,0,64,95v66a32,32,0,1,0,16,0V95A32.06,32.06,0,0,0,104,64ZM56,64A16,16,0,1,1,72,80,16,16,0,0,1,56,64ZM88,192a16,16,0,1,1-16-16A16,16,0,0,1,88,192Zm120-31V110.63a23.85,23.85,0,0,0-7-17L163.31,56H192a8,8,0,0,0,0-16H144a8,8,0,0,0-8,8V96a8,8,0,0,0,16,0V67.31L189.66,105a8,8,0,0,1,2.34,5.66V161a32,32,0,1,0,16,0Zm-8,47a16,16,0,1,1,16-16A16,16,0,0,1,200,208Z",
19596
+ merge: "M208,112a32.05,32.05,0,0,0-30.69,23l-42.21-6a8,8,0,0,1-4.95-2.71L94.43,84.55A32,32,0,1,0,72,87v82a32,32,0,1,0,16,0V101.63l30,35a24,24,0,0,0,14.83,8.14l44,6.28A32,32,0,1,0,208,112ZM64,56A16,16,0,1,1,80,72,16,16,0,0,1,64,56ZM96,200a16,16,0,1,1-16-16A16,16,0,0,1,96,200Zm112-40a16,16,0,1,1,16-16A16,16,0,0,1,208,160Z"
19597
+ };
19598
+ var prGlyph = (merged) => `<svg class="knobs-icon" viewBox="0 0 256 256" width="12" height="12" fill="currentColor" aria-hidden="true" focusable="false"><path d="${merged ? PR_PATHS.merge : PR_PATHS.pull}"/></svg>`;
19599
+ var PR_STATE = { merged: "Merged", open: "Open", draft: "Draft", closed: "Closed" };
19600
+ function prChip(doc, n, pr) {
19601
+ const chip = el2(doc, pr.href ? "a" : "span", "knobs-wt-pr");
19602
+ if (pr.href) {
19603
+ chip.setAttribute("href", pr.href);
19604
+ chip.setAttribute("target", "_blank");
19605
+ chip.setAttribute("rel", "noreferrer");
19606
+ }
19607
+ if (pr.state)
19608
+ chip.dataset.state = pr.state;
19609
+ chip.innerHTML = prGlyph(pr.state === "merged");
19610
+ chip.append(text2(doc, "span", "", `#${n}`));
19611
+ const tip = [`#${n}`, pr.title, pr.state ? PR_STATE[pr.state] : ""].filter(Boolean).join(" · ");
19612
+ chip.title = tip;
19613
+ chip.setAttribute("aria-label", `Pull request ${tip}`);
19614
+ return chip;
19615
+ }
19616
+ var PR_URL = /^https?:\/\/[^/]+\/.+\/(?:pull|merge_requests)\/(\d+)\/?(?:[?#].*)?$/;
19617
+ var PR_REF = /(^|[^\w&/#])#(\d{3,7})\b/g;
19618
+ function linkPRs(root, find) {
19619
+ const doc = root.ownerDocument;
19620
+ for (const a of Array.from(root.querySelectorAll("a[href]"))) {
19621
+ if (a.classList.contains("knobs-wt-pr") || a.closest(".knobs-wt-slot,button"))
19622
+ continue;
19623
+ const n = PR_URL.exec(a.getAttribute("href") ?? "")?.[1];
19624
+ const words = a.textContent?.trim() ?? "";
19625
+ if (!n || !(words === `#${n}` || words === n || words === a.getAttribute("href")))
19626
+ continue;
19627
+ a.replaceWith(prChip(doc, n, { href: a.href, ...find(n) }));
19628
+ }
19629
+ const walker = doc.createTreeWalker(root, 5);
19630
+ const hits = [];
19631
+ for (let t = walker.nextNode();t; t = walker.nextNode()) {
19632
+ if (t.nodeType !== 3)
19633
+ continue;
19634
+ const p = t.parentElement;
19635
+ if (!p || p.closest("a,code,pre,button,.knobs-wt-slot,.knobs-wt-pr"))
19636
+ continue;
19637
+ PR_REF.lastIndex = 0;
19638
+ if (PR_REF.test(t.nodeValue ?? ""))
19639
+ hits.push(t);
19640
+ }
19641
+ for (const t of hits) {
19642
+ const value = t.nodeValue ?? "";
19643
+ const frag = doc.createDocumentFragment();
19644
+ let last = 0;
19645
+ let changed = false;
19646
+ PR_REF.lastIndex = 0;
19647
+ for (let m = PR_REF.exec(value);m; m = PR_REF.exec(value)) {
19648
+ const pr = find(m[2]);
19649
+ if (!pr)
19650
+ continue;
19651
+ const at = m.index + m[1].length;
19652
+ frag.append(value.slice(last, at), prChip(doc, m[2], pr));
19653
+ last = at + m[2].length + 1;
19654
+ changed = true;
19655
+ }
19656
+ if (!changed)
19657
+ continue;
19658
+ frag.append(value.slice(last));
19659
+ t.replaceWith(frag);
19660
+ }
19661
+ }
18145
19662
 
18146
19663
  // src/ui/walkthrough-media.ts
18147
19664
  init_compare_model();
18148
19665
  init_glide();
19666
+ init_motion();
18149
19667
  init_icons();
18150
19668
  init_look();
18151
19669
 
@@ -18158,8 +19676,9 @@ var quick = springCss(FADE);
18158
19676
  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
19677
  var walkthroughCss = `
18160
19678
  :where(.knobs-walkthrough,.knobs-walkthrough *,.knobs-wt-slot,.knobs-wt-slot *){box-sizing:border-box}
19679
+ .knobs-ui:is(.knobs-walkthrough,.knobs-wt-slot){--k-glass:0;--k-glow:0;--kw-quick:${quick.easing};--kw-quick-ms:${quick.duration}ms}
18161
19680
  .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));
19681
+ --kw-measure:var(--k-walkthrough-measure,68ch);--kw-width:var(--k-walkthrough-width,960px);--kw-rail-w:220px;--kw-rail-gap:calc(var(--_k-space-group) + var(--_k-space-column) / 1.5);--kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));
18163
19682
  --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
19683
  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
19684
  letter-spacing:normal;-webkit-font-smoothing:antialiased}
@@ -18172,44 +19691,57 @@ var walkthroughCss = `
18172
19691
  :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
19692
  background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
18174
19693
 
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}
19694
+ /* ——— layout: the contents rail sticky at the left edge (right if asked), and the column centred
19695
+ in the room beside it, up to --kw-width: media take the column, prose stops at the measure.
19696
+ One column where it's narrow ——— */
19697
+ .knobs-wt-layout{display:grid;grid-template-columns:var(--kw-rail-w) minmax(0,1fr);gap:var(--kw-rail-gap);align-items:start}
19698
+ .knobs-walkthrough[data-rail="right"] .knobs-wt-layout{grid-template-columns:minmax(0,1fr) var(--kw-rail-w)}
18178
19699
  .knobs-walkthrough[data-rail="right"] .knobs-wt-main{grid-column:1;grid-row:1}
18179
19700
  .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}
19701
+ .knobs-walkthrough[data-rail="none"] .knobs-wt-layout{grid-template-columns:minmax(0,1fr)}
19702
+ .knobs-wt-main{justify-self:center;width:100%;max-width:var(--kw-width);display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0;padding-bottom:12px}
18183
19703
  /* The intro, the updates and each section: a section's room apart, the hairline in the middle. */
18184
19704
  .knobs-wt-flow{display:flex;flex-direction:column;gap:calc(var(--_k-space-section) / 2);min-width:0}
18185
19705
  .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)}
19706
+ @container knobs-wt (max-width:960px){.knobs-walkthrough[data-rail] .knobs-wt-layout{grid-template-columns:minmax(0,1fr);gap:var(--_k-space-title)}
18187
19707
  .knobs-walkthrough[data-rail] .knobs-wt-layout>*{grid-column:auto;grid-row:auto}}
18188
19708
 
18189
19709
  /* ——— one line of facts: small, faint, joined by ·, its trailing edge fading ——— */
18190
19710
  .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
19711
  .knobs-wt-metaline>*{flex:none}
18192
19712
  .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)}
19713
+ .knobs-walkthrough .knobs-wt-metaline a:not(.knobs-wt-pr){display:inline-flex;align-items:baseline;color:inherit;text-decoration:none;transition:color calc(var(--_k-hover-out) * 1ms)}
19714
+ .knobs-walkthrough .knobs-wt-metaline a:not(.knobs-wt-pr):hover{color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18195
19715
  .knobs-wt-metaline svg{align-self:center;width:12px;height:12px;margin-left:2px}
19716
+ /* A pull request: a small chip, its state's icon and number in a quiet tint of the state (open
19717
+ the success hue, merged the violet series, draft and closed neutral), its title on hover. */
19718
+ .knobs-ui .knobs-wt-pr{--kw-pr:var(--_k-fg-2);display:inline-flex;align-items:center;gap:3px;height:18px;padding:0 5px 0 4px;margin:0 1px;border-radius:calc(var(--_k-radius) / 2);vertical-align:1px;
19719
+ background:color-mix(in oklch,var(--kw-pr) 12%,transparent);color:var(--kw-pr);font:var(--_k-weight-label) var(--_k-type-small)/1 var(--_k-font);font-variant-numeric:tabular-nums;white-space:nowrap;text-decoration:none;
19720
+ transition:background calc(var(--_k-hover-out) * 1ms)}
19721
+ .knobs-ui .knobs-wt-pr[data-state="open"]{--kw-pr:color-mix(in oklch,var(--_k-ok) 60%,var(--_k-fg-2))}
19722
+ .knobs-ui .knobs-wt-pr[data-state="merged"]{--kw-pr:color-mix(in oklch,var(--_k-series-5) 60%,var(--_k-fg-2))}
19723
+ .knobs-ui .knobs-wt-pr:is([data-state="draft"],[data-state="closed"]){--kw-pr:var(--_k-fg-3)}
19724
+ .knobs-ui a.knobs-wt-pr:is(:hover,:focus-visible){background:color-mix(in oklch,var(--kw-pr) 22%,transparent);transition-duration:calc(var(--_k-hover-in) * 1ms)}
19725
+ .knobs-wt-pr svg{flex:none}
19726
+ .knobs-wt-metaline .knobs-wt-pr{align-self:center;margin:0}
18196
19727
  .knobs-wt-kicker-lead{color:var(--_k-fg);font-weight:var(--kw-strong)}
18197
19728
  .knobs-wt-metaline [data-status="on"]{color:var(--_k-fg)}
18198
19729
  .knobs-wt-metaline [data-status="warn"]{color:var(--_k-warn)}
18199
19730
 
18200
19731
  /* ——— the page's head ——— */
18201
19732
  .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}
19733
+ .knobs-wt-title{font-size:var(--_k-type-heading)!important;line-height:var(--_k-leading-heading);font-weight:var(--kw-strong)!important;text-wrap:balance}
18203
19734
  .knobs-wt-summary{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
18204
19735
  .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
19736
  .knobs-wt-meta>span{display:inline-flex;gap:var(--_k-space-inside);white-space:nowrap}
18206
19737
  .knobs-wt-meta b{font-weight:var(--_k-weight);color:var(--_k-fg-2);font-family:var(--_k-mono)}
18207
19738
 
18208
- /* ——— a section: a PR or topic. Its kicker leads the meta line; its title outranks the page's ——— */
19739
+ /* ——— a section: a PR or topic. Its kicker leads the meta line, its title under it (16/24, under
19740
+ the page's 20/28), both on the column's edge ——— */
18209
19741
  .knobs-wt-section{display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0}
18210
19742
  .knobs-wt-sintro{display:flex;flex-direction:column;gap:var(--_k-space-title);min-width:0}
18211
19743
  .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}
19744
+ .knobs-wt-heading{font-size:var(--_k-type-title)!important;line-height:var(--_k-leading-title);font-weight:var(--kw-strong)!important;text-wrap:balance}
18213
19745
  .knobs-wt-ssum{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
18214
19746
  .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
19747
  --kw-tone:var(--_k-fg-2);color:var(--kw-tone);background:color-mix(in srgb,var(--kw-tone) 14%,transparent)}
@@ -18240,7 +19772,6 @@ var walkthroughCss = `
18240
19772
  .knobs-wt-feed{display:flex;flex-direction:column;gap:var(--_k-space-title);min-width:0;scroll-margin-top:16px}
18241
19773
  .knobs-wt-feed-head{display:flex;align-items:baseline;gap:var(--_k-space-sibling)}
18242
19774
  .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
19775
  .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
19776
  .knobs-wt-fi{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0}
18246
19777
  .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)}
@@ -18251,10 +19782,6 @@ var walkthroughCss = `
18251
19782
  :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
19783
  transition:color calc(var(--_k-hover-out) * 1ms)}
18253
19784
  .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
19785
 
18259
19786
  /* ——— a disclosure: a small faint toggle with a caret, no fill. The body's height springs (in
18260
19787
  script) and its content fades in place, top-anchored, under a soft bottom mask ——— */
@@ -18265,11 +19792,12 @@ var walkthroughCss = `
18265
19792
  .knobs-ui .knobs-wt-disc-toggle:focus-visible{box-shadow:0 0 0 2px var(--_k-focus)}
18266
19793
  .knobs-wt-disc-toggle svg{flex:none;width:12px;height:12px;transition:rotate var(--kw-state-ms) var(--kw-state)}
18267
19794
  .knobs-wt-disc[data-open]>.knobs-wt-disc-toggle svg{rotate:90deg}
18268
- .knobs-wt-disc-count{color:var(--_k-fg-3)}
18269
19795
  .knobs-wt-disc-body{overflow:hidden}
18270
19796
  .knobs-wt-disc[data-open]>.knobs-wt-disc-body:not([data-moving]){overflow:visible}
18271
19797
  .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
19798
  .knobs-wt-disc-content{padding-top:var(--_k-space-inside)}
19799
+ /* "More" under a caption or summary: the rest of its words, in the same voice. */
19800
+ .knobs-wt-more-text{display:flex;flex-direction:column;gap:var(--_k-space-sibling);max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
18273
19801
  /* A trail sits on the toggle's label edge: faint lines, the time trailing. */
18274
19802
  .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
19803
  .knobs-wt-trail li{display:flex;align-items:baseline;gap:var(--_k-space-sibling);min-width:0;white-space:nowrap}
@@ -18387,32 +19915,28 @@ var walkthroughCss = `
18387
19915
  .knobs-wt-ocap{color:var(--_k-fg-2);max-width:var(--kw-measure)}
18388
19916
 
18389
19917
  /* ——— 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)}
19918
+ .knobs-wt-rail{position:sticky;top:var(--_k-pad-page);display:flex;flex-direction:column;gap:var(--_k-space-title);max-height:calc(100vh - var(--_k-pad-page) * 2);min-height:0;font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18391
19919
  .knobs-wt-progress{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
18392
19920
  .knobs-wt-count{display:flex;align-items:baseline;justify-content:space-between;color:var(--_k-fg-2)}
18393
19921
  .knobs-wt-count b{color:var(--_k-fg);font-weight:var(--kw-strong)}
18394
19922
  .knobs-wt-bar{position:relative;height:4px;border-radius:2px;overflow:hidden;background:var(--_k-fill)}
18395
19923
  .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
19924
  .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. */
19925
+ /* The contents: every row on one text edge and in one colour, title rows (32 high, the control
19926
+ radius) and the steps under them (20 high, half of it), bled out so their text sits on the
19927
+ rail's edge; the selection pill slides to the current step, and base rows never brighten. */
18400
19928
  .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
19929
  .knobs-wt-toc::-webkit-scrollbar{display:none}
18402
19930
  .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;
19931
+ .knobs-ui .knobs-wt-toc-row{position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:var(--_k-space-sibling);align-items:center;flex:none;width:100%;height:32px;
19932
+ 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(--_k-weight);text-align:left;white-space:nowrap;cursor:pointer;
18405
19933
  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}
19934
+ .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)}
18409
19935
  .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
19936
  .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)}
19937
+ .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)}
18414
19938
  .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)}
19939
+ .knobs-pick-copy .knobs-wt-toc-end{color:var(--_k-fg-2)}
18416
19940
  /* A step's state: an open ring, a filled dot once answered, nothing to answer a faint ring. */
18417
19941
  .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
19942
  transition:background var(--kw-snappy-ms) var(--kw-snappy),box-shadow var(--kw-snappy-ms) var(--kw-snappy)}
@@ -18427,10 +19951,12 @@ var walkthroughCss = `
18427
19951
  .knobs-ui .knobs-wt-btn[data-primary]:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_k-fg) 16%,transparent)}
18428
19952
  .knobs-wt-keys{color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18429
19953
  .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){
19954
+ /* Narrow: the contents go; with review on, the rail becomes a bar along the top with the
19955
+ progress and the export. */
19956
+ @container knobs-wt (max-width:960px){
18432
19957
  .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
19958
  .knobs-wt-progress{flex:1}.knobs-wt-toc,.knobs-wt-keys{display:none}
19959
+ .knobs-walkthrough:not([data-review]) .knobs-wt-rail{display:none}
18434
19960
  .knobs-wt-step,.knobs-wt-shead,.knobs-wt-feed{scroll-margin-top:60px}
18435
19961
  }
18436
19962
 
@@ -18440,9 +19966,18 @@ var walkthroughCss = `
18440
19966
  well for hairlines, so nothing draws a fill and a ring of its own there ——— */
18441
19967
  .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
19968
  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)}
19969
+ --kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));--kw-frame-r:var(--_k-radius-panel)}
19970
+ .knobs-surface .knobs-wt-slot{--kw-frame-r:calc(var(--_k-radius-panel) - 4px)}
19971
+ /* A slot's label: sentence case as written, small and faint, never a heading. */
19972
+ .knobs-wt-slot-label{display:block;min-width:0;overflow:hidden;white-space:nowrap;font:var(--_k-weight) var(--_k-type-small)/var(--_k-leading-small) var(--_k-font);letter-spacing:normal;text-transform:none;color:var(--_k-fg-3);${fade2}}
18445
19973
  .knobs-wt-body{min-width:0}
19974
+ /* Media mounting after the first paint, and a picture still decoding, come into focus: a fade with
19975
+ a small blur on the quick spring, from the top; at rest, nothing. */
19976
+ .knobs-wt-body,.knobs-wt-frame>img{transition:opacity var(--kw-quick-ms) var(--kw-quick),filter var(--kw-quick-ms) var(--kw-quick)}
19977
+ .knobs-wt-body[data-entering],.knobs-wt-frame[data-loading]>img{opacity:0;filter:blur(3px);transition:none}
19978
+ /* One frame for every slot that has one (a picture, a video, a code block, a compare's stage, a
19979
+ slot still loading): the panel radius and a quiet fill, no ring; concentric on a card. */
19980
+ .knobs-wt-slot .knobs-cmp-stage{border-radius:var(--kw-frame-r)}
18446
19981
  /* A compare is exactly as tall as its pictures at this width, so there's no letterbox: the two
18447
19982
  sides edge to edge (side) or one (overlaid), plus its mode bar, the timeline and their two gaps
18448
19983
  (92 px), and the frame strip with its gap (158 px). */
@@ -18450,13 +19985,16 @@ var walkthroughCss = `
18450
19985
  .knobs-wt-fit[data-overlay]{height:calc(100cqw / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
18451
19986
  .knobs-wt-fit[data-strip]{--kw-chrome:158px}
18452
19987
  .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)}
19988
+ .knobs-wt-frame{position:relative;width:100%;overflow:hidden;border-radius:var(--kw-frame-r);background:var(--_k-fill);aspect-ratio:var(--kw-aspect,16/9)}
18454
19989
  .knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
18455
19990
  .knobs-wt-frame[data-natural]{aspect-ratio:auto;background:transparent}
18456
19991
  .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);
19992
+ /* Diff, code and terminal: one plain container (no glass): a flat inset fill, a hairline ring, the
19993
+ control radius and room inside, so a diff's buttons and the code under them can breathe. */
19994
+ .knobs-wt-slot[data-kind="diff"]>.knobs-wt-body{padding:var(--_k-space-sibling) var(--_k-pad-card) var(--_k-pad-card);border-radius:var(--_k-radius);background:var(--_k-bg-inset);box-shadow:inset 0 0 0 1px var(--_k-border-subtle)}
19995
+ .knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:var(--_k-radius);background:var(--_k-bg-inset);box-shadow:inset 0 0 0 1px var(--_k-border-subtle);color:var(--_k-fg);
18458
19996
  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)}
19997
+ .knobs-surface :is(.knobs-wt-code,.knobs-wt-slot[data-kind="diff"]>.knobs-wt-body){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
19998
  .knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
18461
19999
  .knobs-wt-prompt{color:var(--_k-fg-3);user-select:none;-webkit-user-select:none}
18462
20000
  .knobs-wt-cmd{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
@@ -18464,8 +20002,8 @@ var walkthroughCss = `
18464
20002
  .knobs-wt-metric{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
18465
20003
  .knobs-wt-mlabel{color:var(--_k-fg-2)}
18466
20004
  .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)}
20005
+ .knobs-wt-mfrom{color:var(--_k-fg-3)}
20006
+ .knobs-wt-mto{font-size:var(--_k-type-title);line-height:var(--_k-leading-title);font-weight:var(--_k-weight-label)}
18469
20007
  .knobs-wt-mof{color:var(--_k-fg-3)}
18470
20008
  .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
20009
  .knobs-wt-mdelta[data-tone="good"]{--kw-tone:var(--kw-good)}
@@ -18480,51 +20018,40 @@ var walkthroughCss = `
18480
20018
  .knobs-wt-callout[data-tone="warn"]{--kw-tone:var(--_k-warn)}
18481
20019
  .knobs-wt-callout[data-tone="bad"]{--kw-tone:var(--_k-danger)}
18482
20020
  .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)}
20021
+ .knobs-wt-hold{border-radius:var(--kw-frame-r);background:var(--_k-fill)}
18484
20022
  .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
20023
 
20024
+ /* Annotations: a calm ring round a box, a dot or an arrow at a point, a small label beside it;
20025
+ each fades in and out with a small blur on the quick spring as its time comes and goes, and a
20026
+ page's switch (the eye, or a) hides them all. Placed in px by script, 1:1 with the picture;
20027
+ last in their box, so they sit over the picture (and under a compare's grip and labels). */
20028
+ .knobs-wt-anno{position:absolute;inset:0;pointer-events:none;overflow:hidden;transition:opacity var(--kw-quick-ms) var(--kw-quick),filter var(--kw-quick-ms) var(--kw-quick)}
20029
+ .knobs-wt-anno:not([data-settled]),.knobs-wt-anno:not([data-settled]) *{transition:none!important}
20030
+ [data-annotations="off"] .knobs-wt-anno,.knobs-wt-anno[data-moving]{opacity:0;filter:blur(3px)}
20031
+ .knobs-wt-mark{--kw-mark:var(--_k-accent);position:absolute;left:0;top:0;color:var(--kw-mark);transition:opacity var(--kw-quick-ms) var(--kw-quick),filter var(--kw-quick-ms) var(--kw-quick)}
20032
+ .knobs-wt-mark[data-hidden]{opacity:0;filter:blur(3px)}
20033
+ .knobs-wt-mark[data-shape="box"]{border-radius:6px;box-shadow:0 0 0 1.5px var(--kw-mark),0 0 0 4px color-mix(in srgb,var(--_k-bg) 45%,transparent)}
20034
+ .knobs-wt-mark[data-shape="dot"]::before{content:"";position:absolute;left:-6px;top:-6px;width:12px;height:12px;border-radius:50%;background:var(--kw-mark);box-shadow:0 0 0 3px color-mix(in srgb,var(--kw-mark) 30%,transparent)}
20035
+ .knobs-wt-anno-arrow{position:absolute;right:0;bottom:0;filter:drop-shadow(0 0 2px var(--_k-bg))}
20036
+ .knobs-wt-mark-label{position:absolute;left:0;bottom:calc(100% + 6px);max-width:240px;padding:2px 6px;border-radius:calc(var(--_k-radius) / 2);background:var(--_k-bg-overlay);color:var(--_k-fg);
20037
+ font:var(--_k-weight-label) var(--_k-type-small)/var(--_k-leading-small) var(--_k-font);white-space:nowrap;overflow:hidden;box-shadow:var(--_k-shadow-overlay);${fade2}}
20038
+ .knobs-wt-mark:is([data-shape="dot"],[data-shape="arrow"]) .knobs-wt-mark-label{left:12px;bottom:auto;top:10px}
20039
+ .knobs-wt-mark[data-shape="arrow"] .knobs-wt-mark-label{left:auto;right:30px;top:auto;bottom:30px}
20040
+ .knobs-ui .knobs-wt-anno-toggle{all:revert;box-sizing:border-box;position:absolute;top:8px;right:8px;display:grid;place-items:center;width:28px;height:28px;padding:0;border:0;margin:0;
20041
+ border-radius:var(--_k-radius);background:color-mix(in srgb,var(--_k-bg) 72%,transparent);color:var(--_k-fg-2);cursor:pointer;opacity:.6;outline:none;
20042
+ transition:opacity calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
20043
+ .knobs-ui .knobs-wt-anno-toggle:is(:hover,:focus-visible){opacity:1;color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
20044
+ .knobs-ui .knobs-wt-anno-toggle:focus-visible{box-shadow:inset 0 0 0 1px var(--_k-focus)}
20045
+ .knobs-cmp-stage>.knobs-wt-anno-toggle{top:auto;bottom:8px}
20046
+
18486
20047
  /* ——— data blocks: history, stack, table, chapters. On the step's card, so no card of their own:
18487
20048
  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)}
20049
+ :where(.knobs-wt-stack,.knobs-wt-tablebox,.knobs-wt-chapters){--kw-line:var(--_k-leading-body);--kw-rail:var(--_k-border-strong)}
18489
20050
  :where(.knobs-wt-slot) [data-tone="good"]{--kw-tone:var(--kw-good)}
18490
20051
  :where(.knobs-wt-slot) [data-tone="warn"]{--kw-tone:var(--_k-warn)}
18491
20052
  :where(.knobs-wt-slot) [data-tone="bad"]{--kw-tone:var(--_k-danger)}
18492
20053
  :where(.knobs-wt-slot) [data-tone="info"]{--kw-tone:var(--kw-info)}
18493
20054
  :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
20055
 
18529
20056
  /* A stack: the trunk at the top, each PR hanging off the branch it merges into, rails drawn as a
18530
20057
  git graph (solid, so where two meet they don't darken). A PR with a step goes there on a click. */
@@ -18569,6 +20096,12 @@ var walkthroughCss = `
18569
20096
  /* Too narrow for its columns, each row becomes a block: its primary cell titling the rest as term
18570
20097
  and value, the blocks parted by hairlines. Scrolling sideways is the last resort (stack: never). */
18571
20098
  .knobs-wt-tablebox{min-width:0}
20099
+ /* A row with a step goes there: the whole row, a wash under the pointer, no arrow, so rows with and
20100
+ without one line up. */
20101
+ .knobs-wt-tablebox [data-go]{cursor:pointer;outline:none}
20102
+ .knobs-wt-tablebox tr[data-go]>td,.knobs-wt-trow[data-go]{transition:background calc(var(--_k-hover-out) * 1ms)}
20103
+ .knobs-wt-tablebox tr[data-go]:is(:hover,:focus-visible)>td,.knobs-wt-trow[data-go]:is(:hover,:focus-visible){background:var(--_ku-wash);transition-duration:calc(var(--_k-hover-in) * 1ms)}
20104
+ .knobs-wt-tablebox [data-go]:focus-visible{box-shadow:inset 0 0 0 1px var(--_k-focus)}
18572
20105
  .knobs-wt-tablebox[data-measure] table{width:min-content}
18573
20106
  .knobs-wt-tstack{display:none;flex-direction:column;min-width:0}
18574
20107
  .knobs-wt-tablebox[data-stacked]>.knobs-wt-table{display:none}
@@ -18592,7 +20125,7 @@ var walkthroughCss = `
18592
20125
  .knobs-wt-table{max-height:520px;overflow:auto;overscroll-behavior-x:contain;outline:none;scrollbar-width:thin;scrollbar-color:transparent transparent;
18593
20126
  transition:scrollbar-color calc(var(--_k-hover-out) * 1ms)}
18594
20127
  .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)}
20128
+ .knobs-wt-table:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus);border-radius:var(--kw-frame-r)}
18596
20129
  @supports (animation-timeline:scroll()){
18597
20130
  .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
20131
  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%;
@@ -18740,6 +20273,8 @@ async function mountCompare(host, m, look, alive, onEvent) {
18740
20273
  fit.style.setProperty("--kw-aspect", String(aspect));
18741
20274
  let lastZoom = "all";
18742
20275
  let lastLoop = "off";
20276
+ let marks = null;
20277
+ let marking = null;
18743
20278
  const c = compare(fit, {
18744
20279
  ...look,
18745
20280
  before: side(before, "Before"),
@@ -18752,6 +20287,14 @@ async function mountCompare(host, m, look, alive, onEvent) {
18752
20287
  onMode: (next) => {
18753
20288
  fit.toggleAttribute("data-overlay", next !== "side");
18754
20289
  onEvent?.({ what: "mode", detail: next });
20290
+ const layer = marks?.element;
20291
+ if (layer) {
20292
+ layer.dataset.moving = "";
20293
+ setTimeout(() => {
20294
+ delete layer.dataset.moving;
20295
+ marks?.update();
20296
+ }, MARK_GLIDE);
20297
+ }
18755
20298
  },
18756
20299
  onView: (view) => {
18757
20300
  const z = view.zoom;
@@ -18762,8 +20305,23 @@ async function mountCompare(host, m, look, alive, onEvent) {
18762
20305
  const loopKey = l ? `${formatTime(l.start)}–${formatTime(l.end)}` : "off";
18763
20306
  if (loopKey !== lastLoop)
18764
20307
  onEvent?.({ what: "loop", detail: lastLoop = loopKey });
20308
+ marking?.kick(MARK_GLIDE);
18765
20309
  }
18766
20310
  });
20311
+ const stage = fit.querySelector(".knobs-cmp-stage");
20312
+ if (m.annotations?.length && stage) {
20313
+ const shape = { w: aspect ?? DEFAULT_ASPECT, h: 1 };
20314
+ marks = annotationLayer(stage, m.annotations, () => {
20315
+ const l = compareLayout({ x: 0, y: 0, w: stage.clientWidth, h: stage.clientHeight }, shape, shape);
20316
+ return {
20317
+ rects: c.mode === "side" ? [l.side.before, l.side.after] : [l.overlay.after],
20318
+ view: c.zoom,
20319
+ time: c.time
20320
+ };
20321
+ });
20322
+ const layer = marks;
20323
+ marking = follow(doc.defaultView, () => c.playing, () => layer.update());
20324
+ }
18767
20325
  const chapters = m.chapters?.length ? m.chapters : null;
18768
20326
  if (chapters)
18769
20327
  c.timeline?.update({
@@ -18780,10 +20338,14 @@ async function mountCompare(host, m, look, alive, onEvent) {
18780
20338
  const unlisten = c.timeline?.listen({
18781
20339
  onSeek: (time, source) => {
18782
20340
  pills?.sync();
20341
+ marking?.kick();
18783
20342
  if (source !== "play")
18784
20343
  onEvent?.({ what: "seek", time: round2(time) });
18785
20344
  },
18786
- onPlay: (on) => onEvent?.({ what: on ? "play" : "pause", time: round2(c.time) })
20345
+ onPlay: (on) => {
20346
+ marking?.kick();
20347
+ onEvent?.({ what: on ? "play" : "pause", time: round2(c.time) });
20348
+ }
18787
20349
  });
18788
20350
  requestAnimationFrame(() => requestAnimationFrame(() => fit.dataset.settled = ""));
18789
20351
  return {
@@ -18810,6 +20372,8 @@ async function mountCompare(host, m, look, alive, onEvent) {
18810
20372
  };
18811
20373
  },
18812
20374
  dispose: () => {
20375
+ marking?.stop();
20376
+ marks?.dispose();
18813
20377
  unlisten?.();
18814
20378
  pills?.dispose();
18815
20379
  c.dispose();
@@ -18818,6 +20382,7 @@ async function mountCompare(host, m, look, alive, onEvent) {
18818
20382
  };
18819
20383
  }
18820
20384
  var round2 = (v) => Math.round(v * 100) / 100;
20385
+ var MARK_GLIDE = 450;
18821
20386
  var zoomedIn = (z) => z.w < 0.999 || z.h < 0.999;
18822
20387
  function mountVideo(host, m, onEvent) {
18823
20388
  const doc = host.ownerDocument;
@@ -18865,6 +20430,16 @@ function mountVideo(host, m, onEvent) {
18865
20430
  if (pills)
18866
20431
  for (const type of ["timeupdate", "seeking", "seeked"])
18867
20432
  v.addEventListener(type, sync);
20433
+ const marks = m.annotations?.length ? annotationLayer(frame, m.annotations, () => ({
20434
+ rects: [
20435
+ contain({ w: frame.clientWidth, h: frame.clientHeight }, { w: v.videoWidth, h: v.videoHeight })
20436
+ ],
20437
+ time: v.currentTime
20438
+ })) : null;
20439
+ const marking = marks ? follow(doc.defaultView, () => !v.paused, () => marks.update()) : null;
20440
+ if (marking)
20441
+ for (const type of ["play", "seeked", "loadedmetadata", "timeupdate"])
20442
+ v.addEventListener(type, () => marking.kick());
18868
20443
  return {
18869
20444
  pause: () => v.pause(),
18870
20445
  seekBy: (d) => {
@@ -18878,6 +20453,8 @@ function mountVideo(host, m, onEvent) {
18878
20453
  ...pills?.current ? { chapter: pills.current.label } : {}
18879
20454
  }),
18880
20455
  dispose: () => {
20456
+ marking?.stop();
20457
+ marks?.dispose();
18881
20458
  pills?.dispose();
18882
20459
  v.pause();
18883
20460
  v.removeAttribute("src");
@@ -18895,9 +20472,52 @@ function mountImage(host, m) {
18895
20472
  img.decoding = "async";
18896
20473
  img.alt = m.alt ?? m.label ?? "";
18897
20474
  img.src = m.src;
20475
+ if (!img.complete) {
20476
+ frame.dataset.loading = "";
20477
+ img.addEventListener("load", () => delete frame.dataset.loading, { once: true });
20478
+ img.addEventListener("error", () => delete frame.dataset.loading, { once: true });
20479
+ }
18898
20480
  frame.append(img);
18899
20481
  host.append(frame);
18900
- return { dispose: () => frame.remove() };
20482
+ const marks = m.annotations?.length ? annotationLayer(frame, m.annotations, () => ({
20483
+ rects: [{ x: 0, y: 0, w: frame.clientWidth, h: frame.clientHeight }]
20484
+ })) : null;
20485
+ if (marks)
20486
+ img.addEventListener("load", () => marks.update(), { once: true });
20487
+ return {
20488
+ dispose: () => {
20489
+ marks?.dispose();
20490
+ frame.remove();
20491
+ }
20492
+ };
20493
+ }
20494
+ function contain(box, natural) {
20495
+ if (!(natural.w > 0 && natural.h > 0))
20496
+ return { x: 0, y: 0, w: box.w, h: box.h };
20497
+ const k = Math.min(box.w / natural.w, box.h / natural.h);
20498
+ const w = natural.w * k;
20499
+ const h = natural.h * k;
20500
+ return { x: (box.w - w) / 2, y: (box.h - h) / 2, w, h };
20501
+ }
20502
+ function follow(win, on, fn) {
20503
+ let raf = 0;
20504
+ let until = 0;
20505
+ const tick = () => {
20506
+ fn();
20507
+ raf = on() || performance.now() < until ? win.requestAnimationFrame(tick) : 0;
20508
+ };
20509
+ return {
20510
+ kick(ms = 0) {
20511
+ until = Math.max(until, performance.now() + ms);
20512
+ if (!raf)
20513
+ raf = win.requestAnimationFrame(tick);
20514
+ },
20515
+ stop() {
20516
+ if (raf)
20517
+ win.cancelAnimationFrame(raf);
20518
+ raf = 0;
20519
+ }
20520
+ };
18901
20521
  }
18902
20522
  async function mountDiff(host, m, look, alive, onEvent) {
18903
20523
  const lib = await Promise.resolve().then(() => (init_diff(), {}));
@@ -19051,6 +20671,14 @@ function mediaSlot(container, options) {
19051
20671
  let pending = false;
19052
20672
  let disposed = false;
19053
20673
  let generation = 0;
20674
+ const born = performance.now();
20675
+ const enter = () => {
20676
+ const win = doc.defaultView;
20677
+ if (!win || performance.now() - born < 120 || reducedMotion(win))
20678
+ return;
20679
+ host.dataset.entering = "";
20680
+ win.requestAnimationFrame(() => win.requestAnimationFrame(() => delete host.dataset.entering));
20681
+ };
19054
20682
  const hold = (px) => {
19055
20683
  host.style.minHeight = px === null ? "" : `${px}px`;
19056
20684
  host.classList.toggle("knobs-wt-hold", px !== null);
@@ -19070,6 +20698,7 @@ function mediaSlot(container, options) {
19070
20698
  if (!alive())
19071
20699
  return r.dispose();
19072
20700
  mounted = r;
20701
+ enter();
19073
20702
  if (filled)
19074
20703
  r.fill?.(true);
19075
20704
  if (r.ready)
@@ -19162,14 +20791,14 @@ function mediaSlot(container, options) {
19162
20791
  var CHECK3 = icon("check", { small: true });
19163
20792
 
19164
20793
  // src/ui/walkthrough.ts
19165
- var el2 = (doc, tag, cls) => {
20794
+ var el3 = (doc, tag, cls) => {
19166
20795
  const n = doc.createElement(tag);
19167
20796
  if (cls)
19168
20797
  n.className = cls;
19169
20798
  return n;
19170
20799
  };
19171
20800
  var prose2 = (doc, tag, cls, text) => {
19172
- const n = el2(doc, tag, cls);
20801
+ const n = el3(doc, tag, cls);
19173
20802
  n.innerHTML = inlineMarkdown(text);
19174
20803
  return n;
19175
20804
  };
@@ -19206,37 +20835,46 @@ function tasteTest(container, options) {
19206
20835
  ...o.media !== undefined ? { media: fillMedia(o.media) } : {},
19207
20836
  suggested: o.suggested === true
19208
20837
  });
19209
- let picked = options.picked ?? null;
19210
- const root = el2(doc, "div", "knobs-wt-options");
19211
- root.setAttribute("role", "radiogroup");
19212
- root.setAttribute("aria-label", "Options");
20838
+ const pickable = options.pickable !== false;
20839
+ let picked = pickable ? options.picked ?? null : null;
20840
+ const root = el3(doc, "div", "knobs-wt-options");
20841
+ if (pickable) {
20842
+ root.setAttribute("role", "radiogroup");
20843
+ root.setAttribute("aria-label", "Options");
20844
+ } else
20845
+ root.dataset.static = "";
19213
20846
  root.style.setProperty("--kw-n", String(Math.min(list.length, 3)));
19214
20847
  if (!container.closest(".knobs-ui"))
19215
20848
  applyLook2(root, options);
19216
20849
  const slots = [];
19217
20850
  const tiles = list.map((o, i) => {
19218
- const tile = el2(doc, "div", "knobs-wt-option");
19219
- const pick = el2(doc, "button", "knobs-wt-pick");
20851
+ const tile = el3(doc, "div", "knobs-wt-option");
20852
+ if (!pickable) {
20853
+ const name = el3(doc, "span", "knobs-wt-slot-label");
20854
+ name.textContent = o.label;
20855
+ tile.append(name);
20856
+ }
20857
+ const pick = el3(doc, "button", "knobs-wt-pick");
19220
20858
  pick.type = "button";
19221
20859
  pick.setAttribute("role", "radio");
19222
20860
  if (i < 9)
19223
20861
  pick.setAttribute("aria-keyshortcuts", String(i + 1));
19224
- const letter = el2(doc, "span", "knobs-wt-letter");
20862
+ const letter = el3(doc, "span", "knobs-wt-letter");
19225
20863
  letter.textContent = o.letter;
19226
- const name = el2(doc, "span", "knobs-wt-olabel");
20864
+ const name = el3(doc, "span", "knobs-wt-olabel");
19227
20865
  name.textContent = o.label;
19228
20866
  pick.append(letter, name);
19229
20867
  if (o.suggested) {
19230
- const s = el2(doc, "span", "knobs-wt-suggested");
20868
+ const s = el3(doc, "span", "knobs-wt-suggested");
19231
20869
  s.textContent = "Suggested";
19232
20870
  pick.append(s);
19233
20871
  }
19234
20872
  if (i < 9) {
19235
- const key = el2(doc, "span", "knobs-wt-key");
20873
+ const key = el3(doc, "span", "knobs-wt-key");
19236
20874
  key.textContent = String(i + 1);
19237
20875
  pick.append(key);
19238
20876
  }
19239
- const check = el2(doc, "span", "knobs-wt-check");
20877
+ const check = el3(doc, "span", "knobs-wt-check");
19240
20878
  check.innerHTML = CHECK3;
19241
20879
  pick.append(check);
19242
20880
  pick.addEventListener("click", () => {
@@ -19244,7 +20882,8 @@ function tasteTest(container, options) {
19244
20882
  api.pick(next);
19245
20883
  options.onPick?.(next);
19246
20884
  });
19247
- tile.append(pick);
20885
+ if (pickable)
20886
+ tile.append(pick);
19248
20887
  if (o.media)
19249
20888
  slots.push(mediaSlot(tile, {
19250
20889
  ...look,
@@ -19301,7 +20940,7 @@ function walkthrough(container, options) {
19301
20940
  injectWalkthroughStyles(doc);
19302
20941
  const look = lookOf2(options);
19303
20942
  const storage = options.storage === undefined ? browserStorage() : options.storage;
19304
- const root = el2(doc, "div", "knobs-walkthrough");
20943
+ const root = el3(doc, "div", "knobs-walkthrough");
19305
20944
  applyLook2(root, options);
19306
20945
  container.append(root);
19307
20946
  let page = normalizeWalkthrough(options.doc);
@@ -19327,7 +20966,8 @@ function walkthrough(container, options) {
19327
20966
  let tocRows = [];
19328
20967
  let discs = [];
19329
20968
  let full = null;
19330
- const noteMode = options.noteBar ?? "collapsed";
20969
+ const review = options.review === true;
20970
+ const noteMode = review ? options.noteBar ?? "collapsed" : "hidden";
19331
20971
  let reserve = noteBarReserve(52, DOCK_INSET, DOCK_GAP);
19332
20972
  const viewOf = (key) => views.find((v) => v.step.key === key);
19333
20973
  const feedback = () => collectFeedback(page, state);
@@ -19513,7 +21153,7 @@ function walkthrough(container, options) {
19513
21153
  return true;
19514
21154
  return false;
19515
21155
  };
19516
- const scrim = el2(doc, "div", "knobs-wt-scrim");
21156
+ const scrim = el3(doc, "div", "knobs-wt-scrim");
19517
21157
  scrim.addEventListener("click", () => setFull(false));
19518
21158
  const fullRect = () => ({
19519
21159
  x: DOCK_INSET,
@@ -19563,7 +21203,7 @@ function walkthrough(container, options) {
19563
21203
  }
19564
21204
  if (!full) {
19565
21205
  const r = v.card.getBoundingClientRect();
19566
- const hold = el2(doc, "div", "knobs-wt-hold-card");
21206
+ const hold = el3(doc, "div", "knobs-wt-hold-card");
19567
21207
  hold.style.height = `${r.height}px`;
19568
21208
  v.card.before(hold);
19569
21209
  v.card.dataset.full = "";
@@ -19713,7 +21353,7 @@ function walkthrough(container, options) {
19713
21353
  toast("Couldn't reach the microphone", { tone: "warn", look, doc });
19714
21354
  }
19715
21355
  };
19716
- const backPill = el2(doc, "button", "knobs-wt-back");
21356
+ const backPill = el3(doc, "button", "knobs-wt-back");
19717
21357
  backPill.type = "button";
19718
21358
  let backTo = null;
19719
21359
  let backTimer = 0;
@@ -19733,7 +21373,7 @@ function walkthrough(container, options) {
19733
21373
  return;
19734
21374
  backTo = key;
19735
21375
  backPill.innerHTML = ICON.back;
19736
- const words = el2(doc, "span", "knobs-wt-back-label");
21376
+ const words = el3(doc, "span", "knobs-wt-back-label");
19737
21377
  words.textContent = `Back to ${plainMarkdown(v.step.title)}`;
19738
21378
  backPill.append(words);
19739
21379
  backPill.dataset.on = "";
@@ -19741,16 +21381,16 @@ function walkthrough(container, options) {
19741
21381
  backTimer = win.setTimeout(hideBack, BACK_MS);
19742
21382
  };
19743
21383
  const bar = (() => {
19744
- const dock = el2(doc, "div", "knobs-wt-dock");
19745
- const box = el2(doc, "div", "knobs-wt-notebar");
19746
- const on = el2(doc, "div", "knobs-wt-on");
19747
- const row = el2(doc, "div", "knobs-wt-row");
19748
- const input = el2(doc, "textarea", "knobs-wt-input");
21384
+ const dock = el3(doc, "div", "knobs-wt-dock");
21385
+ const box = el3(doc, "div", "knobs-wt-notebar");
21386
+ const on = el3(doc, "div", "knobs-wt-on");
21387
+ const row = el3(doc, "div", "knobs-wt-row");
21388
+ const input = el3(doc, "textarea", "knobs-wt-input");
19749
21389
  input.rows = 1;
19750
21390
  input.setAttribute("aria-label", "Note");
19751
- const attach = el2(doc, "div", "knobs-wt-attach");
21391
+ const attach = el3(doc, "div", "knobs-wt-attach");
19752
21392
  const btn = (icon, label, run) => {
19753
- const b = el2(doc, "button", "knobs-wt-tool");
21393
+ const b = el3(doc, "button", "knobs-wt-tool");
19754
21394
  b.type = "button";
19755
21395
  b.innerHTML = icon;
19756
21396
  b.setAttribute("aria-label", label);
@@ -19761,17 +21401,17 @@ function walkthrough(container, options) {
19761
21401
  const fullBtn = btn(ICON.full, "Full window · f", () => setFull(!full));
19762
21402
  const penBtn = btn(ICON.pen, "Draw on the media", () => setInk(!ink.on));
19763
21403
  const micBtn = btn(ICON.mic, "Record a voice note", () => void toggleVoice());
19764
- const micTime = el2(doc, "span", "knobs-wt-rec");
21404
+ const micTime = el3(doc, "span", "knobs-wt-rec");
19765
21405
  micBtn.append(micTime);
19766
21406
  const sendBtn = btn(ICON.send, "Send · Enter", () => send());
19767
21407
  sendBtn.dataset.send = "";
19768
21408
  row.append(input, attach, fullBtn, penBtn, micBtn, sendBtn);
19769
21409
  box.append(on, row);
19770
- const launch = el2(doc, "button", "knobs-wt-launch");
21410
+ const launch = el3(doc, "button", "knobs-wt-launch");
19771
21411
  launch.type = "button";
19772
21412
  launch.innerHTML = ICON.pen;
19773
21413
  launch.append("Note");
19774
- const kbd = el2(doc, "kbd");
21414
+ const kbd = el3(doc, "kbd");
19775
21415
  kbd.textContent = "n";
19776
21416
  launch.append(kbd);
19777
21417
  launch.setAttribute("aria-label", "Write a note (n)");
@@ -19823,9 +21463,9 @@ function walkthrough(container, options) {
19823
21463
  }
19824
21464
  });
19825
21465
  const chip = (text, clear) => {
19826
- const c = el2(doc, "span", "knobs-wt-chip-a");
21466
+ const c = el3(doc, "span", "knobs-wt-chip-a");
19827
21467
  c.append(text);
19828
- const x = el2(doc, "button", "knobs-wt-x");
21468
+ const x = el3(doc, "button", "knobs-wt-x");
19829
21469
  x.type = "button";
19830
21470
  x.innerHTML = ICON.close;
19831
21471
  x.setAttribute("aria-label", `Remove ${text}`);
@@ -19974,9 +21614,9 @@ function walkthrough(container, options) {
19974
21614
  }
19975
21615
  };
19976
21616
  const renderNote = (v, n, entering = false) => {
19977
- const row = el2(doc, "div", "knobs-wt-n");
21617
+ const row = el3(doc, "div", "knobs-wt-n");
19978
21618
  row.dataset.id = n.id;
19979
- const meta = el2(doc, "span", "knobs-wt-n-meta");
21619
+ const meta = el3(doc, "span", "knobs-wt-n-meta");
19980
21620
  const m = n.context.media;
19981
21621
  meta.textContent = m?.time !== undefined ? formatTime(m.time) : m?.file ? m.file.split("/").pop() : "·";
19982
21622
  if (m?.time !== undefined) {
@@ -19992,11 +21632,11 @@ function walkthrough(container, options) {
19992
21632
  });
19993
21633
  }
19994
21634
  row.append(meta);
19995
- const body = el2(doc, "div", "knobs-wt-n-body");
21635
+ const body = el3(doc, "div", "knobs-wt-n-body");
19996
21636
  if (n.text)
19997
21637
  body.append(prose2(doc, "p", "knobs-wt-n-text", n.text));
19998
21638
  if (n.voice) {
19999
- const a = el2(doc, "audio", "knobs-wt-n-voice");
21639
+ const a = el3(doc, "audio", "knobs-wt-n-voice");
20000
21640
  a.controls = true;
20001
21641
  a.preload = "none";
20002
21642
  a.src = n.voice.url;
@@ -20016,7 +21656,7 @@ function walkthrough(container, options) {
20016
21656
  body.append(thumb);
20017
21657
  }
20018
21658
  row.append(body);
20019
- const x = el2(doc, "button", "knobs-wt-x");
21659
+ const x = el3(doc, "button", "knobs-wt-x");
20020
21660
  x.type = "button";
20021
21661
  x.innerHTML = ICON.close;
20022
21662
  x.setAttribute("aria-label", "Delete this note");
@@ -20056,15 +21696,22 @@ function walkthrough(container, options) {
20056
21696
  row.addEventListener("transitionend", done, { once: true });
20057
21697
  win.setTimeout(done, 600);
20058
21698
  };
20059
- const metaLine = (cls) => el2(doc, "div", `knobs-wt-metaline ${cls}`);
21699
+ const metaLine = (cls) => el3(doc, "div", `knobs-wt-metaline ${cls}`);
20060
21700
  const metaText = (line, text, cls) => {
20061
- const s = el2(doc, "span", cls);
21701
+ const s = el3(doc, "span", cls);
20062
21702
  s.textContent = text;
20063
21703
  line.append(s);
20064
21704
  return s;
20065
21705
  };
21706
+ const findPRIn = (n) => findPR(page.prs, n, page.repo);
20066
21707
  const metaLink = (line, text, href) => {
20067
- const a = el2(doc, "a");
21708
+ const n = prNumber(href);
21709
+ if (n) {
21710
+ const chip = prChip(doc, n, { href, ...findPRIn(n) });
21711
+ line.append(chip);
21712
+ return chip;
21713
+ }
21714
+ const a = el3(doc, "a");
20068
21715
  a.href = href;
20069
21716
  a.target = "_blank";
20070
21717
  a.rel = "noreferrer";
@@ -20074,58 +21721,58 @@ function walkthrough(container, options) {
20074
21721
  return a;
20075
21722
  };
20076
21723
  const feedItem = (f, now) => {
20077
- const li = el2(doc, "li", "knobs-wt-fi");
21724
+ const li = el3(doc, "li", "knobs-wt-fi");
20078
21725
  li.dataset.kind = f.kind;
20079
- const q = el2(doc, "div", "knobs-wt-fi-q");
21726
+ const q = el3(doc, "div", "knobs-wt-fi-q");
20080
21727
  const meta = metaLine("knobs-wt-fi-meta");
20081
21728
  if (f.at !== null) {
20082
21729
  const when = metaText(meta, ago(f.at, now));
20083
21730
  when.title = new Date(f.at).toLocaleString();
20084
- } else
20085
- metaText(meta, f.kind === "qa" ? "Answer" : "Update");
21731
+ }
20086
21732
  const step = f.step ? page.steps.find((s) => s.key === f.step) : undefined;
20087
21733
  if (step) {
20088
21734
  const section = page.sections.find((s) => s.id === step.section);
20089
- const b = el2(doc, "button", "knobs-wt-fi-step");
21735
+ const b = el3(doc, "button", "knobs-wt-fi-step");
20090
21736
  b.type = "button";
20091
21737
  b.textContent = section?.link?.label ?? plainMarkdown(step.title);
20092
21738
  b.title = `Go to “${plainMarkdown(step.title)}”`;
20093
21739
  b.addEventListener("click", () => go(step.key));
20094
21740
  meta.append(b);
20095
21741
  }
20096
- if (f.status) {
21742
+ if (f.status && review) {
20097
21743
  const id = f.status.toLowerCase();
20098
21744
  const shipped = /^(ship|shipped|merged|done)$/.test(id);
20099
21745
  const changes = /^(changes?|changes queued)$/.test(id);
20100
21746
  const s = metaText(meta, shipped ? "Shipped" : changes ? "Changes queued" : f.status);
20101
21747
  s.dataset.status = shipped ? "on" : changes ? "warn" : "";
20102
21748
  }
20103
- q.append(meta);
21749
+ if (meta.childNodes.length)
21750
+ q.append(meta);
20104
21751
  if (f.q)
20105
21752
  q.append(prose2(doc, "p", "knobs-wt-fi-ask", f.q));
20106
21753
  else
20107
21754
  q.append(prose2(doc, "p", "knobs-wt-fi-text", f.text));
20108
21755
  li.append(q);
20109
21756
  if (f.q || f.trail.length) {
20110
- const a = el2(doc, "div", "knobs-wt-fi-a");
21757
+ const a = el3(doc, "div", "knobs-wt-fi-a");
20111
21758
  if (f.q)
20112
21759
  a.append(prose2(doc, "div", "knobs-wt-fi-answer", f.text));
20113
21760
  if (f.trail.length) {
20114
- const list = el2(doc, "ol", "knobs-wt-trail");
21761
+ const list = el3(doc, "ol", "knobs-wt-trail");
20115
21762
  for (const line of f.trail) {
20116
21763
  const i = line.indexOf(" · ");
20117
- const row = el2(doc, "li");
20118
- const words = el2(doc, "span", "knobs-wt-trail-text");
21764
+ const row = el3(doc, "li");
21765
+ const words = el3(doc, "span", "knobs-wt-trail-text");
20119
21766
  words.textContent = i < 0 ? line : line.slice(i + 3);
20120
21767
  row.append(words);
20121
21768
  if (i >= 0) {
20122
- const at = el2(doc, "span", "knobs-wt-trail-at");
21769
+ const at = el3(doc, "span", "knobs-wt-trail-at");
20123
21770
  at.textContent = line.slice(0, i);
20124
21771
  row.append(at);
20125
21772
  }
20126
21773
  list.append(row);
20127
21774
  }
20128
- discs.push(disclosure(a, { label: "Context", count: f.trail.length, content: list }));
21775
+ discs.push(disclosure(a, { label: "Context", content: list }));
20129
21776
  }
20130
21777
  li.append(a);
20131
21778
  }
@@ -20150,25 +21797,28 @@ function walkthrough(container, options) {
20150
21797
  views = [];
20151
21798
  root.textContent = "";
20152
21799
  root.removeAttribute("data-settled");
20153
- root.dataset.rail = options.rail === false ? "none" : options.rail === "left" ? "left" : "right";
21800
+ root.dataset.rail = options.rail === false ? "none" : options.rail === "right" ? "right" : "left";
20154
21801
  root.toggleAttribute("data-cards", options.cards === true);
20155
- const layout = el2(doc, "div", "knobs-wt-layout");
20156
- const main = el2(doc, "div", "knobs-wt-main");
20157
- const flow = el2(doc, "div", "knobs-wt-flow");
20158
- const head = el2(doc, "header", "knobs-wt-head");
20159
- const h1 = el2(doc, "h1", "knobs-wt-title");
21802
+ root.toggleAttribute("data-review", review);
21803
+ const layout = el3(doc, "div", "knobs-wt-layout");
21804
+ const main = el3(doc, "div", "knobs-wt-main");
21805
+ const flow = el3(doc, "div", "knobs-wt-flow");
21806
+ const head = el3(doc, "header", "knobs-wt-head");
21807
+ const h1 = el3(doc, "h1", "knobs-wt-title");
20160
21808
  h1.textContent = page.title;
20161
21809
  head.append(h1);
20162
21810
  if (page.summary)
20163
21811
  head.append(prose2(doc, "p", "knobs-wt-summary", page.summary));
21812
+ if (page.more)
21813
+ head.append(more(page.more));
20164
21814
  if (page.meta.length) {
20165
- const meta = el2(doc, "div", "knobs-wt-meta");
21815
+ const meta = el3(doc, "div", "knobs-wt-meta");
20166
21816
  for (const m of page.meta) {
20167
- const s = el2(doc, "span");
21817
+ const s = el3(doc, "span");
20168
21818
  s.append(m.label);
20169
- const b = el2(doc, "b");
21819
+ const b = el3(doc, "b");
20170
21820
  if (m.href) {
20171
- const a = el2(doc, "a");
21821
+ const a = el3(doc, "a");
20172
21822
  a.href = m.href;
20173
21823
  a.target = "_blank";
20174
21824
  a.rel = "noreferrer";
@@ -20184,16 +21834,14 @@ function walkthrough(container, options) {
20184
21834
  flow.append(head);
20185
21835
  let feedEl = null;
20186
21836
  if (page.feed.length) {
20187
- feedEl = el2(doc, "section", "knobs-wt-feed");
21837
+ feedEl = el3(doc, "section", "knobs-wt-feed");
20188
21838
  feedEl.id = "wt-latest";
20189
21839
  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");
21840
+ const fh = el3(doc, "header", "knobs-wt-feed-head");
21841
+ const h = el3(doc, "h2", "knobs-wt-feed-title");
20192
21842
  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");
21843
+ fh.append(h);
21844
+ const list = el3(doc, "ol", "knobs-wt-feed-list");
20197
21845
  const now = Date.now();
20198
21846
  for (const f of page.feed)
20199
21847
  list.append(feedItem(f, now));
@@ -20203,64 +21851,66 @@ function walkthrough(container, options) {
20203
21851
  let rail = null;
20204
21852
  count = fill = tocEl = null;
20205
21853
  if (options.rail !== false) {
20206
- rail = el2(doc, "aside", "knobs-wt-rail");
20207
- rail.setAttribute("aria-label", "Progress");
20208
- const progress = el2(doc, "div", "knobs-wt-progress");
20209
- count = el2(doc, "div", "knobs-wt-count");
20210
- const meter = el2(doc, "div", "knobs-wt-bar");
20211
- fill = el2(doc, "div", "knobs-wt-fill");
20212
- meter.append(fill);
20213
- progress.append(count, meter);
20214
- tocEl = el2(doc, "nav", "knobs-wt-toc");
21854
+ rail = el3(doc, "aside", "knobs-wt-rail");
21855
+ rail.setAttribute("aria-label", "Contents");
21856
+ tocEl = el3(doc, "nav", "knobs-wt-toc");
20215
21857
  tocEl.setAttribute("aria-label", "Contents");
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
21858
  if (feedEl) {
20222
- const group = el2(doc, "div", "knobs-wt-toc-group");
21859
+ const group = el3(doc, "div", "knobs-wt-toc-group");
20223
21860
  const target = feedEl;
20224
- group.append(tocRow("Latest", "", String(page.feed.length), false, () => target.scrollIntoView({ block: "start", behavior: reduced() ? "auto" : "smooth" })));
21861
+ group.append(tocRow("Latest", false, () => target.scrollIntoView({ block: "start", behavior: reduced() ? "auto" : "smooth" })));
20225
21862
  tocEl.append(group);
20226
21863
  }
20227
- rail.append(progress, tocEl, actions(true));
20228
- if (options.keys !== false) {
20229
- const keys = el2(doc, "div", "knobs-wt-keys");
21864
+ if (review) {
21865
+ const progress = el3(doc, "div", "knobs-wt-progress");
21866
+ count = el3(doc, "div", "knobs-wt-count");
21867
+ const meter = el3(doc, "div", "knobs-wt-bar");
21868
+ fill = el3(doc, "div", "knobs-wt-fill");
21869
+ meter.append(fill);
21870
+ progress.append(count, meter);
21871
+ rail.append(progress);
21872
+ }
21873
+ rail.append(tocEl);
21874
+ if (review)
21875
+ rail.append(actions(true));
21876
+ if (review && options.keys !== false) {
21877
+ const keys = el3(doc, "div", "knobs-wt-keys");
20230
21878
  keys.innerHTML = "<kbd>j</kbd> <kbd>k</kbd> steps · <kbd>←</kbd> <kbd>→</kbd> seek · <kbd>f</kbd> full · <kbd>n</kbd> note" + (page.steps.some((s) => s.ask === "pick") ? " · <kbd>1</kbd>–<kbd>9</kbd> pick" : "");
20231
21879
  rail.append(keys);
20232
21880
  }
20233
21881
  }
20234
21882
  for (const section of page.sections) {
20235
- const sec = el2(doc, "section", "knobs-wt-section");
21883
+ const sec = el3(doc, "section", "knobs-wt-section");
20236
21884
  sec.id = `wt-${section.id}`;
20237
- const sh = el2(doc, "header", "knobs-wt-shead");
20238
- if (section.kicker || section.link || section.status) {
21885
+ const sh = el3(doc, "header", "knobs-wt-shead");
21886
+ const status = review ? section.status : undefined;
21887
+ if (section.kicker || section.link || status) {
20239
21888
  const meta = metaLine("knobs-wt-kicker");
20240
21889
  if (section.kicker)
20241
21890
  metaText(meta, section.kicker, "knobs-wt-kicker-lead");
20242
21891
  if (section.link)
20243
21892
  metaLink(meta, section.link.label, section.link.href);
20244
- if (section.status) {
20245
- const s = statusLook(section.status);
21893
+ if (status) {
21894
+ const s = statusLook(status);
20246
21895
  metaText(meta, s.label, "knobs-wt-status").dataset.tone = s.tone;
20247
21896
  }
20248
21897
  sh.append(meta);
20249
21898
  }
20250
- const h2 = el2(doc, "h2", "knobs-wt-heading");
21899
+ const h2 = el3(doc, "h2", "knobs-wt-heading");
20251
21900
  h2.innerHTML = inlineMarkdown(section.title);
20252
21901
  sh.append(h2);
20253
- const intro = el2(doc, "div", "knobs-wt-sintro");
21902
+ const intro = el3(doc, "div", "knobs-wt-sintro");
20254
21903
  intro.append(sh);
20255
21904
  if (section.summary)
20256
21905
  intro.append(prose2(doc, "p", "knobs-wt-ssum", section.summary));
21906
+ if (section.more)
21907
+ intro.append(more(section.more));
20257
21908
  sec.append(intro);
20258
21909
  let group = null;
20259
21910
  if (tocEl) {
20260
- group = el2(doc, "div", "knobs-wt-toc-group");
21911
+ group = el3(doc, "div", "knobs-wt-toc-group");
20261
21912
  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));
21913
+ group.append(tocRow(plainMarkdown(section.title), false, () => first ? go(first) : undefined, section.id));
20264
21914
  tocEl.append(group);
20265
21915
  }
20266
21916
  for (const step of section.steps) {
@@ -20270,6 +21920,7 @@ function walkthrough(container, options) {
20270
21920
  }
20271
21921
  flow.append(sec);
20272
21922
  }
21923
+ linkPRs(flow, findPRIn);
20273
21924
  main.append(flow);
20274
21925
  if (noteMode !== "hidden")
20275
21926
  main.append(bar.dock);
@@ -20298,28 +21949,35 @@ function walkthrough(container, options) {
20298
21949
  root.dataset.settled = "";
20299
21950
  }));
20300
21951
  };
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");
21952
+ const more = (text) => {
21953
+ const box = el3(doc, "div", "knobs-wt-more");
21954
+ const body = el3(doc, "div", "knobs-wt-more-text");
21955
+ for (const para of text.split(/\n\s*\n/))
21956
+ if (para.trim())
21957
+ body.append(prose2(doc, "p", "", para.trim()));
21958
+ discs.push(disclosure(box, { label: "More", content: body }));
21959
+ return box;
21960
+ };
21961
+ const tocRow = (label, sub, run, section) => {
21962
+ const b = el3(doc, "button", sub ? "knobs-wt-toc-row knobs-wt-toc-sub" : "knobs-wt-toc-row");
20303
21963
  b.type = "button";
20304
21964
  if (section)
20305
21965
  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");
21966
+ const t = el3(doc, "span", "knobs-wt-toc-label");
20309
21967
  t.textContent = label;
20310
- const e = el2(doc, "span", "knobs-wt-toc-end");
20311
- e.textContent = end;
20312
21968
  b.title = label;
20313
- b.append(ncol, t, e);
21969
+ b.append(t);
21970
+ if (review)
21971
+ b.append(el3(doc, "span", "knobs-wt-toc-end"));
20314
21972
  b.addEventListener("mousedown", (ev) => ev.preventDefault());
20315
21973
  b.addEventListener("click", run);
20316
21974
  tocRows.push(b);
20317
21975
  return b;
20318
21976
  };
20319
21977
  const actions = (rail) => {
20320
- const row = el2(doc, "div", "knobs-wt-actions");
21978
+ const row = el3(doc, "div", "knobs-wt-actions");
20321
21979
  const btn = (text, run, primary = false) => {
20322
- const b = el2(doc, "button", "knobs-wt-btn");
21980
+ const b = el3(doc, "button", "knobs-wt-btn");
20323
21981
  b.type = "button";
20324
21982
  b.textContent = text;
20325
21983
  if (primary)
@@ -20357,44 +22015,37 @@ function walkthrough(container, options) {
20357
22015
  }
20358
22016
  };
20359
22017
  const stepView = (step, group) => {
20360
- const card = el2(doc, "article", options.cards ? "knobs-wt-step knobs-surface" : "knobs-wt-step");
22018
+ const card = el3(doc, "article", options.cards ? "knobs-wt-step knobs-surface" : "knobs-wt-step");
20361
22019
  card.id = `wt-${step.key.replace("/", "--")}`;
20362
22020
  card.tabIndex = -1;
20363
22021
  card.dataset.key = step.key;
20364
22022
  card.setAttribute("aria-label", plainMarkdown(step.title));
20365
- const head = el2(doc, "header", "knobs-wt-step-head");
20366
- const h3 = el2(doc, "h3", "knobs-wt-stepTitle");
22023
+ const head = el3(doc, "header", "knobs-wt-step-head");
22024
+ const h3 = el3(doc, "h3", "knobs-wt-stepTitle");
20367
22025
  h3.innerHTML = inlineMarkdown(step.title);
20368
22026
  head.append(h3);
20369
22027
  if (step.caption)
20370
22028
  head.append(prose2(doc, "p", "knobs-wt-caption", step.caption));
22029
+ if (step.more)
22030
+ head.append(more(step.more));
20371
22031
  card.append(head);
20372
22032
  const slots = [];
20373
22033
  const onEvent = (e) => record({ ...e, step: step.key });
20374
22034
  let media = null;
20375
22035
  if (step.media.length) {
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);
22036
+ media = el3(doc, "div", "knobs-wt-media");
22037
+ media.style.setProperty("--kw-n", String(Math.min(step.media.length, step.columns)));
22038
+ for (const m of step.media)
22039
+ slots.push(mediaSlot(media, { ...look, media: m, onEvent, steps: stepLinks }));
20385
22040
  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
- }
20391
22041
  }
20392
22042
  let taste = null;
20393
- if (step.question)
22043
+ if (step.question && review)
20394
22044
  card.append(prose2(doc, "p", "knobs-wt-question", step.question));
20395
22045
  if (step.ask === "pick") {
20396
22046
  taste = tasteTest(card, {
20397
22047
  ...look,
22048
+ pickable: review,
20398
22049
  options: step.options,
20399
22050
  picked: state.steps[step.key]?.pick ?? null,
20400
22051
  onPick: (id) => pick(step.key, id),
@@ -20402,8 +22053,9 @@ function walkthrough(container, options) {
20402
22053
  });
20403
22054
  media ??= taste.element;
20404
22055
  }
20405
- const notes = el2(doc, "div", "knobs-wt-notes");
20406
- card.append(notes);
22056
+ const notes = el3(doc, "div", "knobs-wt-notes");
22057
+ if (review)
22058
+ card.append(notes);
20407
22059
  card.addEventListener("focusin", () => {
20408
22060
  if (scroll.to)
20409
22061
  return;
@@ -20413,9 +22065,11 @@ function walkthrough(container, options) {
20413
22065
  let toc = null;
20414
22066
  let glyph = null;
20415
22067
  if (group) {
20416
- toc = tocRow(plainMarkdown(step.title), "", "", true, () => go(step.key));
20417
- glyph = el2(doc, "span", "knobs-wt-toc-glyph");
20418
- toc.querySelector(".knobs-wt-toc-end").append(glyph);
22068
+ toc = tocRow(plainMarkdown(step.title), true, () => go(step.key));
22069
+ if (review) {
22070
+ glyph = el3(doc, "span", "knobs-wt-toc-glyph");
22071
+ toc.querySelector(".knobs-wt-toc-end").append(glyph);
22072
+ }
20419
22073
  group.append(toc);
20420
22074
  }
20421
22075
  return { step, card, media, notes, toc, glyph, taste, slots };
@@ -20487,13 +22141,17 @@ function walkthrough(container, options) {
20487
22141
  claim();
20488
22142
  return;
20489
22143
  }
22144
+ if (key === "a" && !e.shiftKey && root.querySelector(".knobs-wt-anno")) {
22145
+ claim();
22146
+ return setAnnotations(root, !annotationsOn(root));
22147
+ }
20490
22148
  if (key === "n" && noteMode !== "hidden") {
20491
22149
  claim();
20492
22150
  bar.setOpen(true, true);
20493
22151
  return;
20494
22152
  }
20495
22153
  const v = viewOf(current);
20496
- if (!v || e.shiftKey)
22154
+ if (!v || e.shiftKey || !review)
20497
22155
  return;
20498
22156
  const choice = digitChoice(v.step, state.steps[v.step.key], key);
20499
22157
  if (choice === undefined)
@@ -20585,7 +22243,7 @@ function walkthrough(container, options) {
20585
22243
  }
20586
22244
 
20587
22245
  // src/ui/solid.ts
20588
- var follow = (read, apply) => {
22246
+ var follow2 = (read, apply) => {
20589
22247
  const changes = propChanges(untrack(read));
20590
22248
  createEffect(() => {
20591
22249
  const changed = changes(read());
@@ -20633,7 +22291,7 @@ function Tree(props) {
20633
22291
  }
20634
22292
  }));
20635
22293
  untrack(() => props.onReady?.(t));
20636
- follow(() => ({ items: props.items, filter: props.filter, selection: props.selection }), (c) => {
22294
+ follow2(() => ({ items: props.items, filter: props.filter, selection: props.selection }), (c) => {
20637
22295
  if ("items" in c)
20638
22296
  t.update(c.items);
20639
22297
  if ("filter" in c)
@@ -20669,7 +22327,7 @@ function CodeDiff(props) {
20669
22327
  onViewed: props.onViewed && ((viewed, file) => props.onViewed?.(viewed, file))
20670
22328
  }));
20671
22329
  untrack(() => props.onReady?.(d));
20672
- follow(diffProps(props), (c) => {
22330
+ follow2(diffProps(props), (c) => {
20673
22331
  if (c.height)
20674
22332
  host.style.height = c.height === "auto" ? "" : "100%";
20675
22333
  d.update(c);
@@ -20692,7 +22350,7 @@ function DiffReview(props) {
20692
22350
  onViewed: (viewed, file) => props.onViewed?.(viewed, file)
20693
22351
  }));
20694
22352
  untrack(() => props.onReady?.(r));
20695
- follow(diffProps(props), (c) => {
22353
+ follow2(diffProps(props), (c) => {
20696
22354
  if (c.height)
20697
22355
  host.style.height = c.height === "auto" ? "" : "100%";
20698
22356
  r.update(c);
@@ -20714,7 +22372,7 @@ function Timeline(props) {
20714
22372
  onCross: (m) => props.onCross?.(m)
20715
22373
  }));
20716
22374
  untrack(() => props.onReady?.(t));
20717
- follow(() => ({
22375
+ follow2(() => ({
20718
22376
  duration: props.duration,
20719
22377
  fps: props.fps,
20720
22378
  units: props.units,
@@ -20743,7 +22401,7 @@ function Compare(props) {
20743
22401
  onView: (view) => props.onView?.(view)
20744
22402
  }));
20745
22403
  untrack(() => props.onReady?.(c));
20746
- follow(() => ({
22404
+ follow2(() => ({
20747
22405
  before: props.before,
20748
22406
  after: props.after,
20749
22407
  mode: props.mode,
@@ -20766,7 +22424,7 @@ function CompareStrip(props) {
20766
22424
  host.style.height = "100%";
20767
22425
  const s = untrack(() => compareStrip(host, { ...props, onPick: (time) => props.onPick?.(time) }));
20768
22426
  untrack(() => props.onReady?.(s));
20769
- follow(() => ({ before: props.before.src, after: props.after.src }), (changed) => s.update({
22427
+ follow2(() => ({ before: props.before.src, after: props.after.src }), (changed) => s.update({
20770
22428
  ..."before" in changed ? { before: props.before } : {},
20771
22429
  ..."after" in changed ? { after: props.after } : {}
20772
22430
  }));
@@ -20786,7 +22444,7 @@ function Bento(props) {
20786
22444
  onActive: (id) => props.onActive?.(id)
20787
22445
  }));
20788
22446
  untrack(() => props.onReady?.(b));
20789
- follow(() => ({ items: props.items, picks: props.picks }), (c) => {
22447
+ follow2(() => ({ items: props.items, picks: props.picks }), (c) => {
20790
22448
  if ("items" in c)
20791
22449
  b.update(c.items);
20792
22450
  if (c.picks)
@@ -20808,7 +22466,7 @@ function Activity(props) {
20808
22466
  onPlay: props.onPlay && ((id, event) => props.onPlay?.(id, event))
20809
22467
  }));
20810
22468
  untrack(() => props.onReady?.(a));
20811
- follow(() => ({ events: props.events, newest: props.newest, selected: props.selected }), (c) => {
22469
+ follow2(() => ({ events: props.events, newest: props.newest, selected: props.selected }), (c) => {
20812
22470
  if (c.events || c.newest)
20813
22471
  a.update({ events: c.events, newest: c.newest });
20814
22472
  if (c.selected !== undefined && c.selected !== a.selected)
@@ -20837,7 +22495,7 @@ function OptionPicker(props) {
20837
22495
  }
20838
22496
  }));
20839
22497
  untrack(() => props.onReady?.(p));
20840
- follow(() => ({ options: props.options, selected: props.selected, chosen: props.chosen }), (c) => {
22498
+ follow2(() => ({ options: props.options, selected: props.selected, chosen: props.chosen }), (c) => {
20841
22499
  if ("options" in c)
20842
22500
  p.set(c.options);
20843
22501
  if (c.selected != null)
@@ -20854,7 +22512,7 @@ function Walkthrough(props) {
20854
22512
  const host = document.createElement("div");
20855
22513
  const w = untrack(() => walkthrough(host, { ...props, onFeedback: (feedback) => props.onFeedback?.(feedback) }));
20856
22514
  untrack(() => props.onReady?.(w));
20857
- follow(() => ({ doc: props.doc }), (c) => c.doc && w.set(c.doc));
22515
+ follow2(() => ({ doc: props.doc }), (c) => c.doc && w.set(c.doc));
20858
22516
  onCleanup(() => w.dispose());
20859
22517
  return host;
20860
22518
  }
@@ -20864,7 +22522,7 @@ function TasteTest(props) {
20864
22522
  const host = document.createElement("div");
20865
22523
  const t = untrack(() => tasteTest(host, { ...props, onPick: (id) => props.onPick?.(id) }));
20866
22524
  untrack(() => props.onReady?.(t));
20867
- follow(() => ({ picked: props.picked }), (c) => ("picked" in c) && t.pick(c.picked ?? null));
22525
+ follow2(() => ({ picked: props.picked }), (c) => ("picked" in c) && t.pick(c.picked ?? null));
20868
22526
  onCleanup(() => t.dispose());
20869
22527
  return host;
20870
22528
  }