@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-react.js CHANGED
@@ -578,11 +578,11 @@ var compareCss = `
578
578
  .knobs-cmp-modes[hidden]{display:none}
579
579
  .knobs-ui .knobs-cmp-mode{display:inline-flex;align-items:center;height:100%;padding:0 8px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:pointer;white-space:nowrap;
580
580
  transition:color calc(var(--_k-hover-out) * 1ms)}
581
- .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible){color:var(--_k-fg)}
581
+ .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
582
582
  /* Toggles: icon buttons, the accent's wash when on. */
583
583
  .knobs-ui .knobs-cmp-toggle{display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:var(--_k-radius);color:var(--_k-fg-3);cursor:pointer;
584
584
  transition:color calc(var(--_k-hover-out) * 1ms),background calc(var(--_k-hover-out) * 1ms)}
585
- .knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash)}
585
+ .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)}
586
586
  .knobs-ui .knobs-cmp-toggle[aria-pressed="true"]{color:var(--_k-accent-fg);background:var(--_k-accent-soft)}
587
587
  .knobs-cmp-toggle svg{display:block}
588
588
  .knobs-cmp-fill{flex:1}
@@ -596,6 +596,8 @@ var compareCss = `
596
596
  .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}
597
597
  .knobs-compare[data-frame="flat"] .knobs-cmp-stage{border-radius:0}
598
598
  .knobs-compare[data-mode="wipe"] .knobs-cmp-stage{cursor:ew-resize}
599
+ /* Videos: a click on the stage plays and pauses. */
600
+ .knobs-compare[data-video]:not([data-mode="wipe"],[data-zoomed]) .knobs-cmp-stage{cursor:pointer}
599
601
  /* Unzoomed, a finger still scrolls the page up and down; zoomed, the stage takes every gesture. */
600
602
  .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:grab;touch-action:none}
601
603
  .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:grabbing!important}
@@ -605,6 +607,9 @@ var compareCss = `
605
607
  .knobs-cmp-canvas{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
606
608
  .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}
607
609
  .knobs-compare[data-renderer="gpu"] .knobs-cmp-media{opacity:0}
610
+ /* The media comes in with its first frames: a fade and a 4px blur on the default spring. */
611
+ .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)}
612
+ .knobs-compare:not([data-ready]) :is(.knobs-cmp-canvas,.knobs-cmp-media){opacity:0;filter:blur(4px)}
608
613
  /* Glass: what's behind shows through, blurred, on the knobs tokens. */
609
614
  .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);
610
615
  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)))}
@@ -613,11 +618,17 @@ var compareCss = `
613
618
  white-space:nowrap;pointer-events:none;will-change:transform}
614
619
  .knobs-cmp-label[data-side="before"]{color:var(--_k-fg-3)}
615
620
  .knobs-cmp-label[data-side="after"]{color:var(--_k-fg)}
616
- .knobs-cmp-mark svg{display:block;width:12px;height:12px}
617
- /* The wipe: a hairline down the line with a raised grip at the pointer's height; a wider strip
618
- takes the hand and the keys (a slider). It fades with the mode; wiping, line and grip brighten. */
619
- .knobs-cmp-handle{cursor:ew-resize;position:absolute;left:-12px;top:0;width:24px;height:0;opacity:0;pointer-events:none;outline:none;will-change:transform;transition:opacity .18s cubic-bezier(.4,0,1,1)}
620
- .knobs-cmp-handle[data-on]{opacity:1;pointer-events:auto;transition:opacity .22s cubic-bezier(.2,.8,.2,1)}
621
+ /* Fusing, the mark folds away and the label is its name. */
622
+ .knobs-cmp-mark{flex:none;display:flex;justify-content:flex-end;overflow:hidden}
623
+ .knobs-cmp-mark svg{display:block;flex:none;width:12px;height:12px}
624
+ /* The fuse's seam: a blurred strip over where the sides meet, soft at both sides. */
625
+ .knobs-cmp-seam{position:absolute;left:0;top:0;width:8px;height:0;opacity:0;pointer-events:none;will-change:transform,opacity;
626
+ -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)}
627
+ /* The wipe: a hairline down the line with a raised grip in its middle (at the pointer's height
628
+ while dragged); a wider strip takes the hand and the keys (a slider). It fades in with the
629
+ fuse; wiping, line and grip brighten. */
630
+ .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}
631
+ .knobs-cmp-handle[data-on]{pointer-events:auto}
621
632
  .knobs-cmp-handle::before{content:"";position:absolute;top:0;bottom:0;left:11.5px;width:1px;background:color-mix(in srgb,var(--_ku-cmp-line) 72%,transparent);box-shadow:0 0 0 .5px rgba(0,0,0,.28),0 0 6px rgba(0,0,0,.25)}
622
633
  .knobs-compare[data-dragging] .knobs-cmp-handle::before{background:var(--_ku-cmp-line)}
623
634
  .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);
@@ -637,7 +648,7 @@ var compareCss = `
637
648
  .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)}
638
649
  :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;
639
650
  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}
640
- :where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){color:var(--_k-fg);background:color-mix(in srgb,var(--_k-fg) 12%,transparent)}
651
+ :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)}
641
652
  .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}
642
653
  .knobs-cmp-note:empty{display:none}
643
654
  .knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
@@ -649,6 +660,31 @@ var compareCss = `
649
660
  @media(prefers-reduced-motion:reduce){:is(.knobs-compare,.knobs-cmp-strip) :where(*,::before,::after){transition-duration:.12s!important}}
650
661
  `;
651
662
 
663
+ // src/ui/player-css.ts
664
+ var playerCss = `
665
+ :where(.knobs-player,.knobs-player *){box-sizing:border-box}
666
+ .knobs-ui.knobs-player{--_kp-stage:var(--_k-compare-stage);display:flex;flex-direction:column;gap:6px;width:100%;outline:none;
667
+ font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
668
+ :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)}
669
+ .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}
670
+ .knobs-player[data-frame="flat"] .knobs-player-frame{border-radius:0}
671
+ .knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
672
+ 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)}
673
+ .knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}
674
+ .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;
675
+ opacity:0;filter:blur(4px);scale:.96;
676
+ 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);
677
+ box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim);
678
+ 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)}
679
+ .knobs-player-glyph svg{width:16px;height:16px}
680
+ .knobs-player-glyph[data-flash]{opacity:1;filter:none;scale:1}
681
+ .knobs-player-line{flex:none;min-height:0}
682
+ /* Hover in is immediate (0 ms), out eases. The timeline's own stylesheet is look.ts's; its hover
683
+ states are set here, after it, so the player's line and every timeline share them. */
684
+ .knobs-ui .knobs-tl-play:hover,.knobs-tl-edge:hover::after,.knobs-tl-rlabel:hover,.knobs-tl-track:hover .knobs-tl-hover,
685
+ .knobs-timeline[data-variant="line"] .knobs-tl-track:hover .knobs-tl-segs{transition-duration:calc(var(--_k-hover-in) * 1ms)}
686
+ `;
687
+
652
688
  // src/ui/overlay-css.ts
653
689
  var overlayCss = `
654
690
  .knobs-layers{position:fixed;inset:0;z-index:2147480005;pointer-events:none}
@@ -1513,7 +1549,7 @@ var init_schema = __esm(() => {
1513
1549
  type: "length",
1514
1550
  group: "Type",
1515
1551
  source: value("20px"),
1516
- doc: "Section headings (a PR's title, under its kicker), weight 500."
1552
+ doc: "A page's title, weight 500."
1517
1553
  },
1518
1554
  {
1519
1555
  name: "leading-heading",
@@ -1529,7 +1565,7 @@ var init_schema = __esm(() => {
1529
1565
  type: "length",
1530
1566
  group: "Type",
1531
1567
  source: value("16px"),
1532
- doc: "A page's title, weight 500."
1568
+ doc: "Section titles (under their kicker), weight 500."
1533
1569
  },
1534
1570
  {
1535
1571
  name: "leading-title",
@@ -1561,7 +1597,7 @@ var init_schema = __esm(() => {
1561
1597
  type: "length",
1562
1598
  group: "Type",
1563
1599
  source: value("12px"),
1564
- doc: "Labels over media, weight 500; mono inside body text."
1600
+ doc: "Mono inside body text: code and paths."
1565
1601
  },
1566
1602
  {
1567
1603
  name: "leading-label",
@@ -3004,8 +3040,6 @@ var css2 = `
3004
3040
  .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}
3005
3041
  .knobs-tl-track:hover .knobs-tl-hover{opacity:1}
3006
3042
  .knobs-timeline[data-scrubbing] .knobs-tl-hover{opacity:0}
3007
- @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
3008
- @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
3009
3043
  .knobs-tl-tag{position:absolute;left:0;bottom:2px;font-size:var(--_k-text-meta);line-height:16px;color:var(--_k-fg);font-variant-numeric:tabular-nums;font-feature-settings:"tnum";white-space:nowrap;pointer-events:none;opacity:0}
3010
3044
  .knobs-tl-tag[data-on]{opacity:1}
3011
3045
  /* The lane (variant: "lane"): one slim track of ticks under a compare or a video; markers are
@@ -3021,7 +3055,26 @@ var css2 = `
3021
3055
  .knobs-timeline[data-variant="lane"] .knobs-tl-head::before{top:0}
3022
3056
  .knobs-timeline[data-variant="lane"] .knobs-tl-head::after{content:none}
3023
3057
  .knobs-timeline[data-variant="lane"] .knobs-tl-tag{top:2px;bottom:auto}
3024
- `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + selectionPillCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
3058
+ /* The line (variant: "line"): a player's progress under its media. One 2px line, fg-3 with fg
3059
+ filling it, a segment per chapter; it thickens under the pointer and while scrubbing. The
3060
+ chapter and time under the pointer float over it; nothing else shows. */
3061
+ .knobs-ui.knobs-timeline[data-variant="line"]{height:auto;min-height:0;padding:0;gap:0}
3062
+ .knobs-timeline[data-variant="line"] .knobs-tl-transport{display:none}
3063
+ .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}
3064
+ .knobs-timeline[data-variant="line"] :is(.knobs-tl-ruler,.knobs-tl-lane,.knobs-tl-hover,.knobs-tl-head,.knobs-tl-rlabel){display:none}
3065
+ .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)}
3066
+ .knobs-tl-seg{position:absolute;left:0;top:0;height:100%;overflow:hidden;border-radius:1px;background:var(--_k-fg-3);will-change:transform}
3067
+ .knobs-tl-seg-fill{position:absolute;inset:0;background:var(--_k-fg);transform-origin:0 0;will-change:transform}
3068
+ .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}
3069
+ .knobs-timeline[data-variant="line"] .knobs-tl-range{top:3px;bottom:3px;border-radius:3px}
3070
+ .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)}
3071
+ .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);
3072
+ -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:inset 0 0 0 .5px var(--_k-border)}
3073
+ .knobs-tl-tag-label{color:var(--_k-fg-2)}
3074
+ .knobs-tl-tag-label:empty{display:none}
3075
+ @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
3076
+ @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
3077
+ `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + playerCss + selectionPillCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
3025
3078
  s.omega = m.omega;
3026
3079
  s.zeta = m.zeta;
3027
3080
  return s;
@@ -3144,19 +3197,19 @@ var init_diff_css = __esm(() => {
3144
3197
  @property --kd-fb{syntax:"<length>";inherits:false;initial-value:0px}
3145
3198
  :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))}
3146
3199
  :where(.knobs-diff,.knobs-diff *,.knobs-diff-files,.knobs-diff-files *){box-sizing:border-box}
3147
- .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);
3200
+ .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);
3148
3201
  --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);
3149
3202
  --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;
3150
3203
  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;
3151
3204
  letter-spacing:normal;word-spacing:normal;text-transform:none;-webkit-font-smoothing:antialiased}
3152
3205
  .knobs-ui.knobs-diff[data-height="auto"]{height:auto}
3153
- .knobs-diff::after{content:"";position:absolute;inset:0;z-index:3;border-radius:inherit;box-shadow:var(--_ku-pane-ring);pointer-events:none}
3206
+ .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}
3154
3207
  .knobs-diff [hidden],.knobs-diff-files [hidden]{display:none!important}
3155
3208
  .knobs-diff svg{display:block;flex:none}
3156
- .knobs-ui.knobs-diff[data-on="surface"]{--kd-x:0px;--kd-head:var(--_k-control-h);--kd-ctl:var(--_k-radius)}
3157
- /* The header's hairline shows only once the code has scrolled under it. */
3209
+ .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)}
3210
+ /* A hairline under the header, firmer once the code has scrolled under it. */
3158
3211
  .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);
3159
- box-shadow:inset 0 -1px transparent;transition:box-shadow .15s;user-select:none;-webkit-user-select:none}
3212
+ box-shadow:inset 0 -1px var(--_k-border-subtle);transition:box-shadow .15s;user-select:none;-webkit-user-select:none}
3160
3213
  .knobs-diff[data-scrolled]>.knobs-diff-head{box-shadow:inset 0 -1px var(--_k-border)}
3161
3214
  .knobs-diff-path{flex:0 1 auto;min-width:0;overflow:hidden;white-space:nowrap}
3162
3215
  .knobs-diff-dir,.knobs-diff-from{color:var(--_k-fg-3)}
@@ -3183,17 +3236,26 @@ var init_diff_css = __esm(() => {
3183
3236
  margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
3184
3237
  .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;
3185
3238
  transition:background .15s,color .15s,opacity .15s}
3186
- .knobs-ui .knobs-diff-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
3239
+ .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)}
3187
3240
  .knobs-ui .knobs-diff-btn[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
3188
3241
  .knobs-ui .knobs-diff-btn:disabled{opacity:.35;cursor:default;background:transparent}
3189
3242
  .knobs-diff-btn svg{width:16px;height:16px}
3190
3243
  /* Viewed: a check with its word, as on GitHub; the path dims once it's ticked. */
3191
3244
  .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);
3192
3245
  font-size:var(--_k-text-meta);font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:background .15s,color .15s}
3193
- .knobs-ui .knobs-diff-viewed:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
3246
+ .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)}
3194
3247
  .knobs-ui .knobs-diff-viewed[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
3195
3248
  .knobs-diff-viewed svg{width:16px;height:16px}
3196
3249
  .knobs-diff-viewed[aria-pressed="true"] svg{color:var(--kd-add)}
3250
+ /* Narrow, the header gives up words before anything is cut off at its end: Viewed keeps its
3251
+ check, then the counts' bar goes, then the counts; the path fades at its end throughout. */
3252
+ .knobs-diff-head,.knobs-diff-part-head{container-type:inline-size}
3253
+ @container (max-width:520px){
3254
+ .knobs-diff-viewed-label{display:none}
3255
+ .knobs-ui .knobs-diff-viewed{width:var(--_k-control-h);padding:0;justify-content:center}
3256
+ .knobs-diff-counts .knobs-diff-bar{display:none}
3257
+ }
3258
+ @container (max-width:400px){.knobs-diff-counts{display:none}.knobs-diff-nav>span{min-width:2.6em}}
3197
3259
  .knobs-diff-path{transition:opacity .2s}
3198
3260
  .knobs-diff[data-viewed]>.knobs-diff-head .knobs-diff-path,.knobs-diff-part[data-viewed] .knobs-diff-path{opacity:.6}
3199
3261
  /* Split | Unified: the selection pill on the one picked, its bright copy the picked one's text; a
@@ -3201,7 +3263,7 @@ var init_diff_css = __esm(() => {
3201
3263
  .knobs-diff-seg{position:relative;flex:none;display:grid;grid-template-columns:1fr 1fr;height:var(--_k-control-h);padding:2px;border-radius:var(--kd-ctl);background:var(--_k-fill)}
3202
3264
  .knobs-ui .knobs-diff-choice{padding:0 9px;border-radius:max(0px,calc(var(--kd-ctl) - 2px));color:var(--_k-fg-2);font-size:var(--_k-text-meta);
3203
3265
  font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:color calc(var(--_k-hover-out) * 1ms)}
3204
- .knobs-ui .knobs-diff-choice:is(:hover,:focus-visible){color:var(--_k-fg)}
3266
+ .knobs-ui .knobs-diff-choice:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
3205
3267
  /* The diff: one layer per file or layout on show, the old fading under the new. */
3206
3268
  .knobs-diff-body{position:relative;flex:1;min-height:0}
3207
3269
  .knobs-diff-probe{position:absolute;left:0;top:0;width:0;height:var(--kd-line);visibility:hidden;pointer-events:none}
@@ -3222,8 +3284,8 @@ var init_diff_css = __esm(() => {
3222
3284
  .knobs-diff-hbar{position:absolute;height:5px;pointer-events:none}
3223
3285
  .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;
3224
3286
  transition:opacity .3s,background .15s}
3225
- .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}
3226
- .knobs-diff-hbar>i:hover,.knobs-diff-hbar[data-dragging]>i{background:color-mix(in srgb,var(--_k-fg) 48%,transparent)}
3287
+ .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)}
3288
+ .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)}
3227
3289
  /* stack: every file down one scroller, each header sticking until the next pushes it off. It's
3228
3290
  flush (square, no card around its file), opaque in the code's own colour, with a hairline
3229
3291
  under it only while it's stuck; a gap, not a line, parts one file from the next. */
@@ -3258,24 +3320,26 @@ var init_diff_css = __esm(() => {
3258
3320
  --diffs-bg-deletion-number-override:color-mix(in srgb,var(--kd-del) 55%,var(--kd-solid));
3259
3321
  --diffs-bg-addition-emphasis-override:color-mix(in srgb,var(--kd-add) 22%,transparent);
3260
3322
  --diffs-bg-deletion-emphasis-override:color-mix(in srgb,var(--kd-del) 22%,transparent);
3261
- --diffs-bg-separator-override:color-mix(in srgb,var(--_k-fg) 4.5%,var(--kd-solid));
3262
- --diffs-bg-context-override:color-mix(in srgb,var(--_k-fg) 3%,var(--kd-solid));
3263
- --diffs-bg-buffer-override:color-mix(in srgb,var(--_k-fg) 3.5%,var(--kd-solid));
3323
+ /* Unchanged lines and the folds between hunks sit on the void; only a split's missing lines
3324
+ are faintly filled. */
3325
+ --diffs-bg-separator-override:var(--kd-solid);
3326
+ --diffs-bg-context-override:var(--kd-solid);
3327
+ --diffs-bg-buffer-override:color-mix(in srgb,var(--_k-fg) 2.5%,var(--kd-solid));
3264
3328
  --diffs-bg-selection-override:var(--_k-accent);
3265
3329
  --diffs-fg-number-override:var(--_k-fg-3);
3266
3330
  }
3267
- /* diffReview: one card, the file list a drawer at its left (its width springs; the list inside
3268
- is pinned to its right edge, so it slides out from under the diff), parted from the diff by a
3269
- hairline. The diff in it draws no card of its own. */
3270
- .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);
3331
+ /* diffReview, on the void as the diff is: the file list a drawer at its left (its width springs;
3332
+ the list inside is pinned to its right edge, so it slides out from under the diff), parted
3333
+ from the diff by a hairline; one hairline runs under both headers and one under it all. */
3334
+ .knobs-ui.knobs-diff-review{position:relative;display:flex;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;
3271
3335
  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}
3272
- .knobs-diff-review::after{content:"";position:absolute;inset:0;z-index:5;border-radius:inherit;box-shadow:var(--_ku-pane-ring);pointer-events:none}
3336
+ .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}
3273
3337
  :where(.knobs-diff-review,.knobs-diff-review *){box-sizing:border-box}
3274
- .knobs-diff-drawer{position:relative;flex:none;width:0;height:100%;overflow:hidden;box-shadow:inset -1px 0 var(--_k-border)}
3338
+ .knobs-diff-drawer{position:relative;flex:none;width:0;height:100%;overflow:hidden;box-shadow:inset -1px 0 var(--_k-border-subtle)}
3275
3339
  .knobs-diff-drawer[data-shut]{visibility:hidden}
3276
3340
  .knobs-diff-drawer-in{position:absolute;top:0;bottom:0;right:0;display:flex;flex-direction:column}
3277
3341
  .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);
3278
- user-select:none;-webkit-user-select:none}
3342
+ box-shadow:inset 0 -1px var(--_k-border-subtle);user-select:none;-webkit-user-select:none}
3279
3343
  .knobs-diff-drawer-title{font-weight:var(--_k-weight-title)}
3280
3344
  .knobs-diff-drawer-count{color:var(--_k-fg-3);font-size:var(--_k-text-meta);font-variant-numeric:tabular-nums}
3281
3345
  .knobs-diff-drawer-in>.knobs-diff-files{flex:1;min-height:0;height:auto}
@@ -3287,15 +3351,16 @@ var init_diff_css = __esm(() => {
3287
3351
  .knobs-ui .knobs-diff-toggle[aria-pressed]:not(:hover,:focus-visible){background:transparent}
3288
3352
  /* diffReview's file list: a filter over a compact tree, a status letter, the counts and their
3289
3353
  bar on each file; one highlight glides to the file on show. Viewed files dim, with a tick. */
3290
- .knobs-diff-files{--k-row:var(--k-diff-row,28px);display:flex;flex-direction:column;height:100%;min-height:0;padding-top:2px}
3354
+ .knobs-diff-files{--k-row:var(--k-diff-row,28px);display:flex;flex-direction:column;height:100%;min-height:0;padding-top:4px}
3291
3355
  .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;
3292
- 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}
3293
- .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)}
3356
+ 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}
3357
+ /* The filter is a line of text, no box; focused, a hairline ring. */
3358
+ .knobs-diff-filter:focus-within{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_k-accent) 45%,transparent)}
3294
3359
  .knobs-diff-filter svg{width:12px;height:12px;flex:none}
3295
3360
  .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}
3296
3361
  .knobs-ui .knobs-diff-filter>input::placeholder{color:var(--_k-fg-3)}
3297
3362
  .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}
3298
- .knobs-ui .knobs-diff-filter>button:hover{background:var(--_ku-wash-hi);color:var(--_k-fg)}
3363
+ .knobs-ui .knobs-diff-filter>button:hover{background:var(--_ku-wash-hi);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
3299
3364
  .knobs-diff-filter>button svg{width:12px;height:12px}
3300
3365
  .knobs-diff-list{position:relative;flex:1;min-height:0}
3301
3366
  .knobs-diff-files .knobs-diff-counts{font-size:var(--_k-text-meta);align-items:center}
@@ -6215,7 +6280,10 @@ function codeDiff(container, options) {
6215
6280
  const viewedToggle = () => {
6216
6281
  const b = make("button", "knobs-diff-viewed");
6217
6282
  b.type = "button";
6218
- b.append(make("span", ""), "Viewed");
6283
+ const label = make("span", "knobs-diff-viewed-label");
6284
+ label.textContent = "Viewed";
6285
+ b.append(make("span", ""), label);
6286
+ b.setAttribute("aria-label", "Viewed");
6219
6287
  return b;
6220
6288
  };
6221
6289
  const paintViewed = (b, on) => {
@@ -8502,6 +8570,17 @@ function currentMarker(markers, t, fps) {
8502
8570
  best = m;
8503
8571
  return best;
8504
8572
  }
8573
+ function minutes(t) {
8574
+ const s = Math.max(0, Math.floor(t + 0.000001));
8575
+ const h = Math.floor(s / 3600);
8576
+ const m = Math.floor(s % 3600 / 60);
8577
+ const ss = String(s % 60).padStart(2, "0");
8578
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
8579
+ }
8580
+ function chapterBounds(times, duration) {
8581
+ const inside = times.filter((t) => t > 0.000001 && t < duration - 0.000001);
8582
+ return [0, ...[...new Set(inside)].sort((a, b) => a - b), duration];
8583
+ }
8505
8584
  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;
8506
8585
  var init_timeline_model = __esm(() => {
8507
8586
  SECOND_STEPS = [0.1, 0.2, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600];
@@ -8519,7 +8598,8 @@ function timeline(container, options) {
8519
8598
  let speed = options.speed ?? 1;
8520
8599
  let repeat = options.repeat ?? true;
8521
8600
  const arrows = options.arrows ?? "frames";
8522
- const lane = options.variant === "lane";
8601
+ const line = options.variant === "line";
8602
+ const lane = line || options.variant === "lane";
8523
8603
  let time = Math.min(duration, Math.max(0, options.time ?? 0));
8524
8604
  let playing = false;
8525
8605
  let markers = [...options.markers ?? []];
@@ -8554,7 +8634,7 @@ function timeline(container, options) {
8554
8634
  root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight , . Home End [ ] I O M");
8555
8635
  applyLook2(root, options);
8556
8636
  if (lane)
8557
- root.dataset.variant = "lane";
8637
+ root.dataset.variant = line ? "line" : "lane";
8558
8638
  const transport = el("div", "knobs-tl-transport");
8559
8639
  const playBtn = el("button", "knobs-tl-play");
8560
8640
  playBtn.type = "button";
@@ -8567,6 +8647,10 @@ function timeline(container, options) {
8567
8647
  const hover = el("div", "knobs-tl-hover");
8568
8648
  const tag = el("span", "knobs-tl-tag");
8569
8649
  tag.setAttribute("aria-hidden", "true");
8650
+ const tagLabel = el("span", "knobs-tl-tag-label");
8651
+ const tagTime = el("span", "knobs-tl-tag-time");
8652
+ tag.append(tagLabel, tagTime);
8653
+ const segs = el("div", "knobs-tl-segs");
8570
8654
  const head = el("div", "knobs-tl-head");
8571
8655
  head.id = `${id}-head`;
8572
8656
  head.setAttribute("role", "slider");
@@ -8574,11 +8658,13 @@ function timeline(container, options) {
8574
8658
  head.setAttribute("aria-valuemin", "0");
8575
8659
  head.setAttribute("aria-valuemax", String(duration));
8576
8660
  track.append(canvas, markerLane, hover, head, tag);
8661
+ if (line)
8662
+ track.insertBefore(segs, markerLane);
8577
8663
  root.append(transport, track);
8578
8664
  container.append(root);
8579
8665
  let width = 1;
8580
8666
  let height = 1;
8581
- const fitView = () => clampView({ start: 0, span: duration }, duration, { pad: 0.02 });
8667
+ const fitView = () => clampView({ start: 0, span: duration }, duration, { pad: line ? 0 : 0.02 });
8582
8668
  let target = fitView();
8583
8669
  const vStart = new Spring(target.start, INSTANT2.omega, 1, 0.00001);
8584
8670
  const vSpan = new Spring(target.span, INSTANT2.omega, 1, 0.00001);
@@ -8587,13 +8673,13 @@ function timeline(container, options) {
8587
8673
  const xOf = (t) => (t - vStart.x) * pxPerSec();
8588
8674
  const tOf = (x) => vStart.x + x / pxPerSec();
8589
8675
  const minSpan = () => Math.max(4 / fps, 0.05);
8590
- const retarget = (next, motion = INSTANT2) => {
8591
- target = clampView(next, duration, { pad: 0.04, minSpan: minSpan() });
8676
+ const retarget = (next, motion = INSTANT2, hand = false) => {
8677
+ target = clampView(next, duration, { pad: line ? 0 : 0.04, minSpan: minSpan() });
8592
8678
  for (const s of [vStart, vSpan]) {
8593
8679
  s.omega = motion.omega;
8594
8680
  s.zeta = motion.zeta;
8595
8681
  }
8596
- if (reducedMotion(win) && motion === CALM) {
8682
+ if (hand || reducedMotion(win) && motion === CALM) {
8597
8683
  vStart.snap(target.start);
8598
8684
  vSpan.snap(target.span);
8599
8685
  } else {
@@ -8619,6 +8705,8 @@ function timeline(container, options) {
8619
8705
  };
8620
8706
  let drawn = "";
8621
8707
  const drawRuler = () => {
8708
+ if (line)
8709
+ return;
8622
8710
  const v = view();
8623
8711
  const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${height}:${units}:${fps}`;
8624
8712
  if (key === drawn)
@@ -8706,6 +8794,8 @@ function timeline(container, options) {
8706
8794
  n.title = `${m.label ? `${m.label} · ` : ""}${clock(m.time)}`;
8707
8795
  }
8708
8796
  placeMarkers();
8797
+ if (line)
8798
+ syncSegments();
8709
8799
  };
8710
8800
  const placeMarkers = () => {
8711
8801
  for (const m of markers) {
@@ -8714,6 +8804,36 @@ function timeline(container, options) {
8714
8804
  n.style.transform = `translateX(${xOf(m.time).toFixed(2)}px)`;
8715
8805
  }
8716
8806
  };
8807
+ let bounds = [];
8808
+ const segEls = [];
8809
+ const syncSegments = () => {
8810
+ bounds = chapterBounds(markers.map((m) => m.time), duration);
8811
+ while (segEls.length > bounds.length - 1)
8812
+ segEls.pop().el.remove();
8813
+ while (segEls.length < bounds.length - 1) {
8814
+ const n = el("i", "knobs-tl-seg");
8815
+ const fill = el("b", "knobs-tl-seg-fill");
8816
+ n.append(fill);
8817
+ segs.append(n);
8818
+ segEls.push({ el: n, fill });
8819
+ }
8820
+ placeSegments();
8821
+ };
8822
+ const placeSegments = () => {
8823
+ if (!line)
8824
+ return;
8825
+ const last = segEls.length - 1;
8826
+ segEls.forEach((s, i) => {
8827
+ const a = bounds[i];
8828
+ const b = bounds[i + 1];
8829
+ const x0 = xOf(a) + (i > 0 ? SEG_GAP / 2 : 0);
8830
+ const x1 = xOf(b) - (i < last ? SEG_GAP / 2 : 0);
8831
+ s.el.style.transform = `translateX(${x0.toFixed(2)}px)`;
8832
+ s.el.style.width = `${Math.max(0, x1 - x0).toFixed(2)}px`;
8833
+ const k = Math.min(1, Math.max(0, (shownHead - a) / Math.max(0.000000001, b - a)));
8834
+ s.fill.style.transform = `scaleX(${k.toFixed(4)})`;
8835
+ });
8836
+ };
8717
8837
  const syncRanges = () => {
8718
8838
  const keep = new Set(ranges.map((r) => r.id));
8719
8839
  for (const [k, n] of rangeEls)
@@ -8730,7 +8850,7 @@ function timeline(container, options) {
8730
8850
  b.dataset.edge = "end";
8731
8851
  const label = el("span", "knobs-tl-rlabel");
8732
8852
  n.append(label, a, b);
8733
- track.insertBefore(n, markerLane);
8853
+ track.insertBefore(n, line ? segs : markerLane);
8734
8854
  rangeEls.set(r.id, n);
8735
8855
  if (built)
8736
8856
  enter(n);
@@ -8782,6 +8902,7 @@ function timeline(container, options) {
8782
8902
  let currentId = null;
8783
8903
  const renderHead = () => {
8784
8904
  head.style.transform = `translateX(${xOf(shownHead).toFixed(2)}px)`;
8905
+ placeSegments();
8785
8906
  head.setAttribute("aria-valuenow", time.toFixed(3));
8786
8907
  head.setAttribute("aria-valuetext", `${time.toFixed(2)} seconds, frame ${toFrame(time, fps)}`);
8787
8908
  const cur = currentMarker(markers, time, fps)?.id ?? null;
@@ -9045,14 +9166,25 @@ function timeline(container, options) {
9045
9166
  setTime(s.t, "scrub", s.shown, press);
9046
9167
  root.toggleAttribute("data-stuck", s.stop !== null);
9047
9168
  };
9169
+ let pointerX = null;
9170
+ const zoomAtHand = (factor) => zoom(factor, pointerX === null ? time : target.start + pointerX / width * target.span);
9048
9171
  const showTag = (x) => {
9049
9172
  const at = Math.min(duration, Math.max(0, tOf(x)));
9050
- const text = `${at.toFixed(2)} s`;
9051
- if (tag.textContent !== text)
9052
- tag.textContent = text;
9173
+ const text = line ? minutes(at) : `${at.toFixed(2)} s`;
9174
+ if (tagTime.textContent !== text)
9175
+ tagTime.textContent = text;
9176
+ const label = line ? currentMarker(markers, at, fps)?.label ?? "" : "";
9177
+ if (tagLabel.textContent !== label)
9178
+ tagLabel.textContent = label;
9053
9179
  const cx = Math.min(width, Math.max(0, x));
9054
- const flip = cx > width - tag.offsetWidth - 12;
9055
- tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
9180
+ if (line) {
9181
+ const w = tag.offsetWidth;
9182
+ const left = Math.min(Math.max(0, width - w), Math.max(0, cx - w / 2));
9183
+ tag.style.transform = `translateX(${left.toFixed(1)}px)`;
9184
+ } else {
9185
+ const flip = cx > width - tag.offsetWidth - 12;
9186
+ tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
9187
+ }
9056
9188
  tag.dataset.on = "";
9057
9189
  };
9058
9190
  const hideTag = () => delete tag.dataset.on;
@@ -9113,6 +9245,7 @@ function timeline(container, options) {
9113
9245
  });
9114
9246
  track.addEventListener("pointermove", (e) => {
9115
9247
  const x = localX(e);
9248
+ pointerX = x;
9116
9249
  hover.style.transform = `translateX(${x.toFixed(1)}px)`;
9117
9250
  if (!drag || drag.kind === "scrub")
9118
9251
  showTag(x);
@@ -9186,6 +9319,7 @@ function timeline(container, options) {
9186
9319
  track.addEventListener("pointercancel", endDrag);
9187
9320
  track.addEventListener("lostpointercapture", endDrag);
9188
9321
  track.addEventListener("pointerleave", () => {
9322
+ pointerX = null;
9189
9323
  if (drag)
9190
9324
  return;
9191
9325
  hover.style.transform = "translateX(-100px)";
@@ -9193,7 +9327,7 @@ function timeline(container, options) {
9193
9327
  });
9194
9328
  track.addEventListener("dblclick", (e) => {
9195
9329
  if (lane)
9196
- return;
9330
+ return fit();
9197
9331
  const markerEl = e.target.closest(".knobs-tl-marker");
9198
9332
  if (markerEl)
9199
9333
  return rename(markerEl.dataset.id);
@@ -9204,18 +9338,40 @@ function timeline(container, options) {
9204
9338
  rename(m.id);
9205
9339
  });
9206
9340
  track.addEventListener("wheel", (e) => {
9207
- if (lane)
9341
+ const zooming = e.ctrlKey || e.metaKey;
9342
+ if (zooming && pinching)
9343
+ return e.preventDefault();
9344
+ const sideways = Math.abs(e.deltaX) > Math.abs(e.deltaY);
9345
+ if (lane && !zooming && !sideways)
9208
9346
  return;
9209
9347
  e.preventDefault();
9210
9348
  const scale = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? height : 1;
9211
- if (e.ctrlKey || e.metaKey) {
9349
+ if (zooming) {
9212
9350
  const at = target.start + localX(e) / width * target.span;
9213
- retarget(zoomView(target, Math.exp(-e.deltaY * scale * (e.ctrlKey ? 0.01 : 0.002)), at));
9351
+ const k = Math.exp(-e.deltaY * scale * (e.ctrlKey ? 0.01 : 0.002));
9352
+ retarget(zoomView(target, k, at), INSTANT2, lane);
9214
9353
  } else {
9215
- const d = (Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY) * scale;
9216
- retarget({ ...target, start: target.start + d / width * target.span });
9354
+ const d = (sideways ? e.deltaX : e.deltaY) * scale;
9355
+ retarget({ ...target, start: target.start + d / width * target.span }, INSTANT2, lane);
9217
9356
  }
9218
9357
  }, { passive: false });
9358
+ let pinchScale = 1;
9359
+ let pinching = false;
9360
+ const onGesture = (e) => {
9361
+ const g = e;
9362
+ e.preventDefault();
9363
+ if (e.type === "gesturestart")
9364
+ pinchScale = 1;
9365
+ pinching = e.type !== "gestureend";
9366
+ if (e.type !== "gesturechange")
9367
+ return;
9368
+ const s = g.scale ?? 1;
9369
+ const x = g.clientX !== undefined ? localX({ clientX: g.clientX }) : width / 2;
9370
+ retarget(zoomView(target, s / pinchScale, target.start + x / width * target.span), INSTANT2, true);
9371
+ pinchScale = s;
9372
+ };
9373
+ for (const type of ["gesturestart", "gesturechange", "gestureend"])
9374
+ track.addEventListener(type, onGesture);
9219
9375
  root.addEventListener("keydown", (e) => {
9220
9376
  if (e.target !== root || e.metaKey || e.ctrlKey) {
9221
9377
  if ((e.metaKey || e.ctrlKey) && (e.key === "=" || e.key === "-" || e.key === "0") && e.target === root) {
@@ -9223,7 +9379,7 @@ function timeline(container, options) {
9223
9379
  if (e.key === "0")
9224
9380
  fit();
9225
9381
  else
9226
- zoom(e.key === "=" ? 1.5 : 1 / 1.5);
9382
+ zoomAtHand(e.key === "=" ? 1.5 : 1 / 1.5);
9227
9383
  }
9228
9384
  return;
9229
9385
  }
@@ -9305,18 +9461,12 @@ function timeline(container, options) {
9305
9461
  return;
9306
9462
  case "=":
9307
9463
  case "+":
9308
- if (lane)
9309
- return;
9310
9464
  claim();
9311
- return zoom(1.5);
9465
+ return zoomAtHand(1.5);
9312
9466
  case "-":
9313
- if (lane)
9314
- return;
9315
9467
  claim();
9316
- return zoom(1 / 1.5);
9468
+ return zoomAtHand(1 / 1.5);
9317
9469
  case "0":
9318
- if (lane)
9319
- return;
9320
9470
  claim();
9321
9471
  return fit();
9322
9472
  }
@@ -9532,7 +9682,7 @@ function timeline(container, options) {
9532
9682
  };
9533
9683
  return api;
9534
9684
  }
9535
- 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>';
9685
+ 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>';
9536
9686
  var init_timeline = __esm(() => {
9537
9687
  init_rgba();
9538
9688
  init_motion();
@@ -9604,14 +9754,28 @@ function compareLayout(box, before, after) {
9604
9754
  const shape = after.w > 0 && after.h > 0 ? after : before;
9605
9755
  const pair = letterbox({ w: shape.w * 2, h: shape.h }, box);
9606
9756
  const half = pair.w / 2;
9757
+ const fused = { x: pair.x + half / 2, y: pair.y, w: half, h: pair.h };
9607
9758
  return {
9608
9759
  side: {
9609
9760
  before: letterbox(before, { x: pair.x, y: pair.y, w: half, h: pair.h }),
9610
9761
  after: letterbox(after, { x: pair.x + half, y: pair.y, w: half, h: pair.h }),
9611
9762
  split: pair.x + half
9612
9763
  },
9613
- overlay: { before: letterbox(before, box), after: letterbox(after, box) }
9614
- };
9764
+ overlay: { before: letterbox(before, fused), after: letterbox(after, fused) }
9765
+ };
9766
+ }
9767
+ function fuseFrame(L, before, after, k, wipe) {
9768
+ const t = Math.min(1, Math.max(0, k));
9769
+ const o = L.overlay;
9770
+ const left = Math.min(o.before.x, o.after.x);
9771
+ 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);
9772
+ const cut = L.side.split + (line - L.side.split) * t;
9773
+ const contact = Math.max(0, Math.min(cut - after.x, before.x + before.w - cut));
9774
+ const feather = Math.min(contact, 1 + FUSE.feather * Math.sin(Math.PI * t));
9775
+ const overlap = before.x + before.w - after.x;
9776
+ const seam = t > 0.001 && overlap >= 0 ? Math.min(1, Math.max(0, overlap / FUSE.seamFade)) * Math.min(1, t * 4) : 0;
9777
+ const blur = t > 0.005 && t < 0.995 ? FUSE.blur * Math.sin(Math.PI * t) : 0;
9778
+ return { cut, feather, seam, blur };
9615
9779
  }
9616
9780
  function clampZoom(r) {
9617
9781
  const w = Math.min(1, Math.max(1 / ZOOM_MAX, r.w));
@@ -9698,7 +9862,18 @@ function stripPeaks(diffs, o = {}) {
9698
9862
  });
9699
9863
  return out;
9700
9864
  }
9701
- var ALL_MODES, MODE_KEYS, modeKey = (mode) => MODE_KEYS[mode].toUpperCase(), VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), lastFrame = (duration, fps) => Math.max(0, Math.ceil(duration * fps - 0.0001) - 1), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, splitHeight = (w, media) => media.w > 0 && media.h > 0 ? w / 2 * (media.h / media.w) : w / 2 * (9 / 16), ZOOM_FIT, ZOOM_MAX = 16, panBy = (r, dx, dy, base) => clampZoom({
9865
+ 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) => {
9866
+ const inner = r * Math.min(1, Math.max(0, k));
9867
+ return { before: { left: r, right: inner }, after: { left: inner, right: r } };
9868
+ }, labelFold = (fold, mark, blur = 3) => {
9869
+ const f = Math.min(1, Math.max(0, fold));
9870
+ const open = 1 - f;
9871
+ return {
9872
+ width: mark * open,
9873
+ opacity: Math.min(1, Math.max(0, (open - 0.2) / 0.8)),
9874
+ blur: blur * Math.min(1, Math.max(0, (f - 0.25) / 0.75))
9875
+ };
9876
+ }, 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({
9702
9877
  u: r.u - dx / Math.max(1, base.w) * r.w,
9703
9878
  v: r.v - dy / Math.max(1, base.h) * r.h,
9704
9879
  w: r.w
@@ -9721,6 +9896,7 @@ var init_compare_model = __esm(() => {
9721
9896
  MODE_KEYS = { side: "s", wipe: "w", difference: "d" };
9722
9897
  VIDEO = /\.(mp4|m4v|mov|webm|ogv|mkv)$/i;
9723
9898
  SEEK_IDLE = { seeking: false, pending: null };
9899
+ FUSE = { feather: 32, blur: 8, seamFade: 48 };
9724
9900
  ZOOM_FIT = { u: 0, v: 0, w: 1, h: 1 };
9725
9901
  });
9726
9902
 
@@ -10453,8 +10629,8 @@ async function createCompareGpu(canvas, onLost) {
10453
10629
  onLost();
10454
10630
  };
10455
10631
  lostListeners.add(lost);
10456
- const uniforms = device.createBuffer({ size: 176, usage: 64 | 8 });
10457
- const data = new Float32Array(44);
10632
+ const uniforms = device.createBuffer({ size: 192, usage: 64 | 8 });
10633
+ const data = new Float32Array(48);
10458
10634
  const make = (w, h) => device.createTexture({
10459
10635
  size: [Math.max(1, w), Math.max(1, h)],
10460
10636
  format: "rgba8unorm",
@@ -10527,6 +10703,7 @@ async function createCompareGpu(canvas, onLost) {
10527
10703
  data.set([f.preview[0] ? 1 : 0, f.preview[1] ? 1 : 0, 0, 0], 32);
10528
10704
  r(36, f.beforeClip);
10529
10705
  r(40, f.afterClip);
10706
+ data.set(f.radii, 44);
10530
10707
  device.queue.writeBuffer(uniforms, 0, data);
10531
10708
  const encoder = device.createCommandEncoder();
10532
10709
  const pass = encoder.beginRenderPass({
@@ -10568,6 +10745,7 @@ struct U {
10568
10745
  loupe: vec4f, // x, y, r, zoom (0: none)
10569
10746
  src: vec4f, // Before, After from their previews
10570
10747
  ca: vec4f, cb: vec4f, // where each side may draw
10748
+ radii: vec4f, // the clips' corners: Before left, right; After left, right
10571
10749
  }
10572
10750
  @group(0) @binding(0) var<uniform> u: U;
10573
10751
  @group(0) @binding(1) var A: texture_2d<f32>;
@@ -10589,8 +10767,18 @@ fn cover(p: vec2f, r: vec4f) -> f32 {
10589
10767
  return clamp(d + 0.5, 0.0, 1.0);
10590
10768
  }
10591
10769
 
10592
- // A side's colour where rect r maps it to p, and its coverage inside clip c there.
10593
- fn take(t: texture_2d<f32>, p: vec2f, r: vec4f, c0: vec4f, near: bool, ready: f32) -> vec4f {
10770
+ // Coverage of rect r at p with its left corners rounded rl and its right ones rr.
10771
+ fn coverRound(p: vec2f, r: vec4f, rl: f32, rr: f32) -> f32 {
10772
+ let h = r.zw * 0.5;
10773
+ let q0 = p - (r.xy + h);
10774
+ let rad = min(select(rl, rr, q0.x > 0.0), min(h.x, h.y));
10775
+ let q = abs(q0) - h + vec2f(rad);
10776
+ let d = length(max(q, vec2f(0.0))) + min(max(q.x, q.y), 0.0) - rad;
10777
+ return clamp(0.5 - d, 0.0, 1.0);
10778
+ }
10779
+
10780
+ // A side's colour where rect r maps it to p, and its coverage inside clip c (corners rc) there.
10781
+ fn take(t: texture_2d<f32>, p: vec2f, r: vec4f, c0: vec4f, rc: vec2f, near: bool, ready: f32) -> vec4f {
10594
10782
  let uv = clamp((p - r.xy) / max(r.zw, vec2f(1e-3)), vec2f(0.0), vec2f(1.0));
10595
10783
  var c: vec3f;
10596
10784
  if (near) {
@@ -10599,27 +10787,22 @@ fn take(t: texture_2d<f32>, p: vec2f, r: vec4f, c0: vec4f, near: bool, ready: f3
10599
10787
  } else {
10600
10788
  c = textureSampleLevel(t, smp, uv, 0.0).rgb;
10601
10789
  }
10602
- return vec4f(c, min(cover(p, r), cover(p, c0)) * ready);
10790
+ return vec4f(c, min(cover(p, r), coverRound(p, c0, rc.x, rc.y)) * ready);
10603
10791
  }
10604
10792
 
10605
10793
  fn takeA(p: vec2f, r: vec4f, c0: vec4f, near: bool) -> vec4f {
10606
- if (u.src.x > 0.5) { return take(PA, p, r, c0, near, 1.0); }
10607
- return take(A, p, r, c0, near, u.diff.z);
10794
+ if (u.src.x > 0.5) { return take(PA, p, r, c0, u.radii.xy, near, 1.0); }
10795
+ return take(A, p, r, c0, u.radii.xy, near, u.diff.z);
10608
10796
  }
10609
10797
  fn takeB(p: vec2f, r: vec4f, c0: vec4f, near: bool) -> vec4f {
10610
- if (u.src.y > 0.5) { return take(PB, p, r, c0, near, 1.0); }
10611
- return take(B, p, r, c0, near, u.diff.w);
10798
+ if (u.src.y > 0.5) { return take(PB, p, r, c0, u.radii.zw, near, 1.0); }
10799
+ return take(B, p, r, c0, u.radii.zw, near, u.diff.w);
10612
10800
  }
10613
10801
 
10614
10802
  fn pm(c: vec4f) -> vec4f { return vec4f(c.rgb * c.a, c.a); }
10615
10803
 
10616
10804
  fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
10617
10805
  switch mode {
10618
- case 1u: {
10619
- let s = max(u.wipe.y, 0.5);
10620
- let m = smoothstep(u.wipe.x - s, u.wipe.x + s, p.x);
10621
- return pm(vec4f(mix(a.rgb, b.rgb, m), mix(a.a, b.a, m)));
10622
- }
10623
10806
  case 2u: {
10624
10807
  let d = abs(a.rgb - b.rgb);
10625
10808
  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);
@@ -10628,8 +10811,10 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
10628
10811
  }
10629
10812
  default: {}
10630
10813
  }
10631
- // Side by side: each side in its own place, Before over After where they meet.
10632
- return vec4f(a.rgb * a.a + b.rgb * b.a * (1.0 - a.a), a.a + b.a * (1.0 - a.a));
10814
+ // Side by side, fusing and wiping: Before over After, cut at the seam with its feather.
10815
+ let s = max(u.wipe.y, 0.5);
10816
+ let ka = a.a * (1.0 - smoothstep(u.wipe.x - s, u.wipe.x + s, p.x));
10817
+ return vec4f(a.rgb * ka + b.rgb * b.a * (1.0 - ka), ka + b.a * (1.0 - ka));
10633
10818
  }
10634
10819
 
10635
10820
  @fragment fn fs(@builtin(position) q: vec4f) -> @location(0) vec4f {
@@ -11031,6 +11216,29 @@ var init_compare_strip = __esm(() => {
11031
11216
  DIFF = { w: 32, h: 18 };
11032
11217
  });
11033
11218
 
11219
+ // src/ui/player.ts
11220
+ function playGlyph(doc) {
11221
+ const element = doc.createElement("span");
11222
+ element.className = "knobs-player-glyph";
11223
+ element.setAttribute("aria-hidden", "true");
11224
+ let timer = 0;
11225
+ const win = doc.defaultView;
11226
+ return {
11227
+ element,
11228
+ flash(playing) {
11229
+ element.innerHTML = playing ? PLAY2 : PAUSE;
11230
+ element.setAttribute("data-flash", "");
11231
+ win.clearTimeout(timer);
11232
+ timer = win.setTimeout(() => element.removeAttribute("data-flash"), FLASH_MS);
11233
+ },
11234
+ dispose: () => win.clearTimeout(timer)
11235
+ };
11236
+ }
11237
+ var FLASH_MS = 450;
11238
+ var init_player = __esm(() => {
11239
+ init_timeline();
11240
+ });
11241
+
11034
11242
  // src/ui/compare.ts
11035
11243
  function compare(container, options) {
11036
11244
  const doc = container.ownerDocument;
@@ -11138,6 +11346,8 @@ function compare(container, options) {
11138
11346
  if (name.textContent !== text)
11139
11347
  name.textContent = text;
11140
11348
  };
11349
+ const seam = el("div", "knobs-cmp-seam");
11350
+ seam.setAttribute("aria-hidden", "true");
11141
11351
  const handle = el("div", "knobs-cmp-handle");
11142
11352
  handle.setAttribute("role", "slider");
11143
11353
  handle.setAttribute("aria-label", "Wipe");
@@ -11147,7 +11357,8 @@ function compare(container, options) {
11147
11357
  grip.setAttribute("aria-hidden", "true");
11148
11358
  handle.append(grip);
11149
11359
  const note = el("div", "knobs-cmp-note");
11150
- stage.append(canvas, split, labels[0], labels[1], handle, zoomPill, note);
11360
+ const glyph = playGlyph(doc);
11361
+ stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note, glyph.element);
11151
11362
  root.append(bar, stage);
11152
11363
  const wantsStrip = options.strip === true;
11153
11364
  let tl = bound;
@@ -11162,7 +11373,7 @@ function compare(container, options) {
11162
11373
  root.append(host);
11163
11374
  tl = timeline(host, {
11164
11375
  ...look,
11165
- variant: "lane",
11376
+ variant: "line",
11166
11377
  duration: 1,
11167
11378
  fps: fps(),
11168
11379
  arrows: { step: 0.5, shift: 2 },
@@ -11183,12 +11394,13 @@ function compare(container, options) {
11183
11394
  let toMode = mode;
11184
11395
  const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
11185
11396
  let wipeCatching = false;
11186
- let gripY = null;
11397
+ const gripAt = new Spring(0.5, LAYOUT2.omega, LAYOUT2.zeta, 0.0001);
11187
11398
  let loupeHeld = false;
11188
11399
  let pointer = null;
11189
11400
  let drag = null;
11190
11401
  const touches = new Map;
11191
11402
  let pinch = null;
11403
+ let press = null;
11192
11404
  const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
11193
11405
  const zv = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
11194
11406
  const zw = new Spring(1, ZOOM.omega, ZOOM.zeta, 0.00001);
@@ -11200,6 +11412,7 @@ function compare(container, options) {
11200
11412
  let stageRect = null;
11201
11413
  let accent = [0.49, 0.81, 0.61];
11202
11414
  let labelW = [0, 0];
11415
+ let frameRadius = 0;
11203
11416
  let dirty = true;
11204
11417
  let disposed = false;
11205
11418
  let near = options.lazy === false;
@@ -11475,6 +11688,8 @@ function compare(container, options) {
11475
11688
  for (const s of sides) {
11476
11689
  const st = s.el.style;
11477
11690
  st.clipPath = "";
11691
+ st.maskImage = "";
11692
+ st.setProperty("-webkit-mask-image", "");
11478
11693
  st.mixBlendMode = "";
11479
11694
  st.opacity = "";
11480
11695
  s.shown = null;
@@ -11701,43 +11916,66 @@ function compare(container, options) {
11701
11916
  viewSent = key;
11702
11917
  options.onView({ zoom: clampZoom(z), loop: l });
11703
11918
  };
11704
- const labelsIn = (m, r, wr, wx) => {
11705
- if (m === "wipe") {
11706
- const at = seamLabels(wx, wr.x, wr.x + wr.w, labelW[0], labelW[1], INSET);
11707
- return [at.before, at.after];
11708
- }
11709
- const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * lay.x;
11710
- return [
11711
- { x: r.before.x + INSET, opacity: 1 },
11712
- { x: ax, opacity: 1 }
11713
- ];
11919
+ const differenceWeight = () => {
11920
+ const f = Math.min(1, Math.max(0, fade.x));
11921
+ return (fromMode === "difference" ? 1 - f : 0) + (toMode === "difference" ? f : 0);
11714
11922
  };
11715
11923
  const placeGrip = (wr) => {
11716
- const gy = gripTop(gripY ?? wr.y + wr.h / 2, wr, GRIP.h) - wr.y;
11924
+ const gy = gripTop(wr.y + gripAt.x * wr.h, wr, GRIP.h) - wr.y;
11717
11925
  grip.style.transform = `translate3d(0,${gy.toFixed(2)}px,0)`;
11718
11926
  };
11927
+ let folded = 0;
11719
11928
  const draw = () => {
11720
11929
  dirty = false;
11930
+ if (!root.hasAttribute("data-ready") && sides.every((x) => x.shown || x.failed))
11931
+ root.setAttribute("data-ready", "");
11721
11932
  const r = layoutNow();
11722
11933
  const wr = wipeRect(r.L.overlay);
11723
- const wx = wipeX(wr, wipeLine.x);
11724
- const k = lay.x;
11725
- const a = labelsIn(fromMode, r, wr, wx);
11726
- const b = labelsIn(toMode, r, wr, wx);
11727
- const f = Math.min(1, Math.max(0, fade.x));
11934
+ const k = Math.min(1, Math.max(0, lay.x));
11935
+ const fu = fuseFrame(r.L, r.before, r.after, k, wipeLine.x);
11936
+ const corners = fuseCorners(frameRadius, k);
11937
+ const dw = differenceWeight();
11938
+ const fusing = 1 - dw;
11939
+ folded = k * fusing;
11940
+ const fold = labelFold(folded, MARK.w);
11941
+ const w = labelW.map((x) => x - (MARK.w - fold.width) - MARK.gap * folded);
11942
+ const ride = seamLabels(fu.cut, r.before.x, r.after.x + r.after.w, w[0], w[1], INSET);
11943
+ const apart = [r.before.x + INSET, r.after.x + INSET];
11944
+ const corner = [r.before.x + INSET, r.after.x + r.after.w - labelW[1] - INSET];
11945
+ const rides = [ride.before, ride.after];
11728
11946
  const ys = [r.before.y + INSET, r.after.y + INSET];
11729
11947
  for (const i of [0, 1]) {
11730
- const x = a[i].x + (b[i].x - a[i].x) * f;
11731
- labels[i].style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
11732
- labels[i].style.opacity = (a[i].opacity + (b[i].opacity - a[i].opacity) * f).toFixed(3);
11948
+ const fused = apart[i] + (rides[i].x - apart[i]) * k;
11949
+ const x = fused + (corner[i] - fused) * dw;
11950
+ const opacity = (1 - k + k * rides[i].opacity) * fusing + dw;
11951
+ const l = labels[i];
11952
+ l.style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
11953
+ l.style.opacity = opacity.toFixed(3);
11954
+ const mark = l.firstElementChild;
11955
+ if (mark) {
11956
+ mark.style.width = `${fold.width.toFixed(2)}px`;
11957
+ mark.style.marginRight = `${(-MARK.gap * folded).toFixed(2)}px`;
11958
+ mark.style.opacity = fold.opacity.toFixed(3);
11959
+ mark.style.filter = fold.blur > 0.05 ? `blur(${fold.blur.toFixed(2)}px)` : "";
11960
+ }
11733
11961
  }
11734
11962
  const top = Math.min(r.L.side.before.y, r.L.side.after.y);
11735
11963
  split.style.transform = `translate(${r.L.side.split.toFixed(2)}px,${top.toFixed(2)}px)`;
11736
11964
  split.style.height = `${Math.max(0, r.L.side.after.h).toFixed(2)}px`;
11737
11965
  split.style.opacity = String(Math.max(0, 1 - k * 2));
11966
+ const seamOn = fu.seam * fusing;
11967
+ const blur = fu.blur > 0.01 ? `blur(${fu.blur.toFixed(2)}px)` : "none";
11968
+ seam.style.transform = `translate(${fu.cut.toFixed(2)}px,${wr.y.toFixed(2)}px) translateX(-50%)`;
11969
+ seam.style.width = `${(8 + 2 * fu.feather).toFixed(2)}px`;
11970
+ seam.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
11971
+ seam.style.opacity = (blur === "none" ? 0 : seamOn).toFixed(3);
11972
+ seam.style.backdropFilter = blur;
11973
+ seam.style.setProperty("-webkit-backdrop-filter", blur);
11738
11974
  handle.toggleAttribute("data-on", mode === "wipe");
11739
- handle.style.transform = `translate(${wx.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
11975
+ handle.style.transform = `translate(${fu.cut.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
11740
11976
  handle.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
11977
+ handle.style.opacity = seamOn.toFixed(3);
11978
+ handle.style.filter = blur;
11741
11979
  placeGrip(wr);
11742
11980
  handle.setAttribute("aria-valuenow", String(Math.round(wipe * 100)));
11743
11981
  handle.setAttribute("aria-valuetext", `${Math.round(wipe * 100)}% Before`);
@@ -11750,24 +11988,30 @@ function compare(container, options) {
11750
11988
  if (pending)
11751
11989
  return;
11752
11990
  if (renderer === "dom") {
11753
- const place = (n, rect, clip, cutAt) => {
11991
+ const cut = mode === "difference" ? null : fu;
11992
+ const place = (n, rect, clip, before) => {
11993
+ const round = before ? corners.before : corners.after;
11754
11994
  const st = n.style;
11755
11995
  st.transform = `translate(${rect.x.toFixed(2)}px,${rect.y.toFixed(2)}px)`;
11756
11996
  st.width = `${rect.w.toFixed(2)}px`;
11757
11997
  st.height = `${rect.h.toFixed(2)}px`;
11998
+ const edge = before && cut ? cut.cut + cut.feather : Number.POSITIVE_INFINITY;
11758
11999
  const t = Math.max(0, clip.y - rect.y);
11759
12000
  const l = Math.max(0, clip.x - rect.x);
11760
12001
  const b = Math.max(0, rect.y + rect.h - (clip.y + clip.h));
11761
- const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, cutAt));
11762
- st.clipPath = t + l + b + right > 0.01 ? `inset(${t.toFixed(2)}px ${right.toFixed(2)}px ${b.toFixed(2)}px ${l.toFixed(2)}px)` : "";
12002
+ const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, edge));
12003
+ const rr = edge < clip.x + clip.w - 0.5 ? 0 : round.right;
12004
+ const rl = round.left;
12005
+ 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)` : "";
12006
+ 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)` : "";
12007
+ st.maskImage = mask;
12008
+ st.setProperty("-webkit-mask-image", mask);
11763
12009
  };
11764
- const cut = mode === "wipe" ? wx : Number.POSITIVE_INFINITY;
11765
12010
  for (const s of sides) {
11766
12011
  const rect = s.index === 0 ? r.zBefore : r.zAfter;
11767
12012
  const clip = s.index === 0 ? r.before : r.after;
11768
- const at = s.index === 0 ? cut : Number.POSITIVE_INFINITY;
11769
- place(s.el, rect, clip, at);
11770
- place(s.preview, rect, clip, at);
12013
+ place(s.el, rect, clip, s.index === 0);
12014
+ place(s.preview, rect, clip, s.index === 0);
11771
12015
  if (s.index !== 0)
11772
12016
  continue;
11773
12017
  for (const n of [s.el, s.preview])
@@ -11787,8 +12031,8 @@ function compare(container, options) {
11787
12031
  width: Math.max(1, Math.round(stageW * dpr)),
11788
12032
  height: Math.max(1, Math.round(stageH * dpr)),
11789
12033
  dpr,
11790
- wipeX: wx * dpr,
11791
- soft: 0.75 * dpr,
12034
+ wipeX: fu.cut * dpr,
12035
+ soft: Math.max(0.75, fu.feather) * dpr,
11792
12036
  from: MODE_INDEX[fromMode],
11793
12037
  to: MODE_INDEX[toMode],
11794
12038
  mix: Math.min(1, Math.max(0, fade.x)),
@@ -11796,7 +12040,13 @@ function compare(container, options) {
11796
12040
  gain: 1,
11797
12041
  accent,
11798
12042
  loupe: loupeShown() && pointer ? { x: pointer.x * dpr, y: pointer.y * dpr, r: LOUPE.r * dpr, zoom: LOUPE.zoom } : null,
11799
- preview: [Boolean(sides[0].shown?.cached), Boolean(sides[1].shown?.cached)]
12043
+ preview: [Boolean(sides[0].shown?.cached), Boolean(sides[1].shown?.cached)],
12044
+ radii: [
12045
+ corners.before.left * dpr,
12046
+ corners.before.right * dpr,
12047
+ corners.after.left * dpr,
12048
+ corners.after.right * dpr
12049
+ ]
11800
12050
  });
11801
12051
  };
11802
12052
  const drawNow = () => {
@@ -11815,6 +12065,7 @@ function compare(container, options) {
11815
12065
  zu.step(dt);
11816
12066
  zv.step(dt);
11817
12067
  zw.step(dt);
12068
+ gripAt.step(dt);
11818
12069
  const playing = isPlaying();
11819
12070
  if (playing) {
11820
12071
  const m = master();
@@ -11845,11 +12096,9 @@ function compare(container, options) {
11845
12096
  pushFrame(s);
11846
12097
  }
11847
12098
  }
11848
- const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching;
12099
+ const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching || !gripAt.resting;
11849
12100
  if (moving)
11850
12101
  dirty = true;
11851
- if (auto && !lay.resting)
11852
- fitHeight();
11853
12102
  if (dirty)
11854
12103
  draw();
11855
12104
  return playing || moving;
@@ -11875,13 +12124,7 @@ function compare(container, options) {
11875
12124
  zw.set(c.w);
11876
12125
  requestDraw();
11877
12126
  };
11878
- const autoHeight = (w) => {
11879
- const shape = sizeOf(sides[1]);
11880
- const side = splitHeight(w, shape);
11881
- const over = w * shape.h / shape.w;
11882
- const h = side + (over - side) * lay.x;
11883
- return Math.max(60, Math.min(win.innerHeight * 0.8, h));
11884
- };
12127
+ const autoHeight = (w) => Math.max(60, Math.min(win.innerHeight * 0.8, splitHeight(w, sizeOf(sides[1]))));
11885
12128
  const fitHeight = () => {
11886
12129
  const h = autoHeight(stageW);
11887
12130
  if (Math.abs(h - stageH) < 0.25)
@@ -11893,7 +12136,9 @@ function compare(container, options) {
11893
12136
  const readColors = () => {
11894
12137
  const c = tokenColor(win.getComputedStyle(root), "--_ku-cmp-diff", [0.49, 0.81, 0.61, 1]);
11895
12138
  accent = [c[0], c[1], c[2]];
11896
- labelW = [labels[0].offsetWidth, labels[1].offsetWidth];
12139
+ const unfold = MARK.w - labelFold(folded, MARK.w).width + MARK.gap * folded;
12140
+ labelW = [labels[0].offsetWidth + unfold, labels[1].offsetWidth + unfold];
12141
+ frameRadius = Number.parseFloat(win.getComputedStyle(stage).borderTopLeftRadius) || 0;
11897
12142
  requestDraw();
11898
12143
  };
11899
12144
  const measure = () => {
@@ -11984,8 +12229,10 @@ function compare(container, options) {
11984
12229
  root.focus({ preventScroll: true });
11985
12230
  touches.set(e.pointerId, p);
11986
12231
  stage.setPointerCapture(e.pointerId);
12232
+ press = touches.size === 1 && mode !== "wipe" && master() ? p : null;
11987
12233
  if (touches.size >= 2) {
11988
12234
  e.preventDefault();
12235
+ press = null;
11989
12236
  return startPinch();
11990
12237
  }
11991
12238
  if (mode === "wipe") {
@@ -11995,7 +12242,7 @@ function compare(container, options) {
11995
12242
  if (inside && (grab.onLine || !zoomed())) {
11996
12243
  drag = { kind: "wipe", offset: grab.offset };
11997
12244
  root.dataset.dragging = "";
11998
- gripY = p.y;
12245
+ gripAt.snap(wr.h > 0 ? (p.y - wr.y) / wr.h : 0.5);
11999
12246
  e.preventDefault();
12000
12247
  if (!grab.onLine)
12001
12248
  setWipe(wipeAt(p.x, wr), true, true);
@@ -12013,6 +12260,8 @@ function compare(container, options) {
12013
12260
  const onMove = (e) => {
12014
12261
  const p = local(e);
12015
12262
  pointer = p;
12263
+ if (press && Math.hypot(p.x - press.x, p.y - press.y) > 4)
12264
+ press = null;
12016
12265
  if (touches.has(e.pointerId))
12017
12266
  touches.set(e.pointerId, p);
12018
12267
  if (pinch && touches.size >= 2) {
@@ -12026,14 +12275,12 @@ function compare(container, options) {
12026
12275
  pinch = { d, x: mid.x, y: mid.y };
12027
12276
  return handZoom(zoomAbout(panned, k, q.pu, q.pv));
12028
12277
  }
12029
- if (mode === "wipe" && drag?.kind !== "pan") {
12030
- gripY = p.y;
12031
- if (!drag)
12032
- placeGrip(wipeRect(layoutNow().L.overlay));
12033
- }
12034
- if (drag?.kind === "wipe")
12035
- setWipe(wipeAt(p.x, wipeRect(layoutNow().L.overlay), drag.offset));
12036
- else if (drag?.kind === "pan") {
12278
+ if (drag?.kind === "wipe") {
12279
+ const wr = wipeRect(layoutNow().L.overlay);
12280
+ gripAt.snap(wr.h > 0 ? (p.y - wr.y) / wr.h : 0.5);
12281
+ setWipe(wipeAt(p.x, wr, drag.offset));
12282
+ placeGrip(wr);
12283
+ } else if (drag?.kind === "pan") {
12037
12284
  const dx = p.x - drag.x;
12038
12285
  const dy = p.y - drag.y;
12039
12286
  drag.x = p.x;
@@ -12044,11 +12291,23 @@ function compare(container, options) {
12044
12291
  drawNow();
12045
12292
  };
12046
12293
  const endDrag = (e) => {
12294
+ if (press && e.type === "pointerup") {
12295
+ api.toggle();
12296
+ glyph.flash(isPlaying());
12297
+ }
12298
+ press = null;
12047
12299
  touches.delete(e.pointerId);
12048
12300
  if (touches.size < 2)
12049
12301
  pinch = null;
12050
12302
  if (!drag)
12051
12303
  return;
12304
+ if (drag.kind === "wipe") {
12305
+ if (reducedMotion(win))
12306
+ gripAt.snap(0.5);
12307
+ else
12308
+ gripAt.set(0.5);
12309
+ requestDraw();
12310
+ }
12052
12311
  drag = null;
12053
12312
  delete root.dataset.dragging;
12054
12313
  delete root.dataset.panning;
@@ -12198,6 +12457,7 @@ function compare(container, options) {
12198
12457
  old.el.replaceWith(s.el);
12199
12458
  old.preview.replaceWith(s.preview);
12200
12459
  sides[index] = s;
12460
+ root.toggleAttribute("data-video", sides.some((x) => x.kind === "video"));
12201
12461
  note.textContent = "";
12202
12462
  setLabel(index, labelOf(index));
12203
12463
  if (near)
@@ -12212,6 +12472,7 @@ function compare(container, options) {
12212
12472
  stage.insertBefore(sides[0].preview, labels[0]);
12213
12473
  setLabel(0, labelOf(0));
12214
12474
  setLabel(1, labelOf(1));
12475
+ root.toggleAttribute("data-video", sides.some((s) => s.kind === "video"));
12215
12476
  loupeBtn.setAttribute("aria-pressed", String(loupeOn));
12216
12477
  loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
12217
12478
  const modePill = modeList.length > 1 ? selectionPill(modes, {
@@ -12412,6 +12673,7 @@ function compare(container, options) {
12412
12673
  disposed = true;
12413
12674
  loop.stop();
12414
12675
  modePill?.dispose();
12676
+ glyph.dispose();
12415
12677
  win.clearTimeout(releaseTimer);
12416
12678
  ro.disconnect();
12417
12679
  nearIo?.disconnect();
@@ -12441,7 +12703,7 @@ function compare(container, options) {
12441
12703
  wake();
12442
12704
  return api;
12443
12705
  }
12444
- var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
12706
+ var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, MARK, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
12445
12707
  var init_compare = __esm(() => {
12446
12708
  init_motion();
12447
12709
  init_page_hints();
@@ -12452,15 +12714,17 @@ var init_compare = __esm(() => {
12452
12714
  init_icons();
12453
12715
  init_look();
12454
12716
  init_rgba();
12717
+ init_player();
12455
12718
  init_selection_pill();
12456
12719
  init_timeline();
12457
12720
  init_timeline_model();
12458
- LAYOUT2 = springMotion({ duration: 0.35, bounce: 0 });
12721
+ LAYOUT2 = springMotion(SPRINGS.default);
12459
12722
  FADE6 = springMotion(SPRINGS.snappy);
12460
12723
  FADE_REDUCED = springMotion(FADE);
12461
12724
  ZOOM = springMotion({ duration: 0.35, bounce: 0 });
12462
12725
  WIPE_CATCH = springMotion(CATCH_UP);
12463
12726
  GRIP = { w: 16, h: 28 };
12727
+ MARK = { w: 12, gap: 4 };
12464
12728
  LOUPE = { r: 88, zoom: 4 };
12465
12729
  MODE_LABEL = {
12466
12730
  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)