@kitlangton/knobs 0.4.0-next.5 → 0.4.0-next.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui-solid.js CHANGED
@@ -575,13 +575,14 @@ var compareCss = `
575
575
  picked: the pill's bright copy is. */
576
576
  .knobs-cmp-modes{position:relative;display:inline-flex;height:var(--_k-control-h);border-radius:var(--_k-radius);background:var(--_k-fill)}
577
577
  .knobs-cmp-modes[hidden]{display:none}
578
- .knobs-ui .knobs-cmp-mode{display:inline-flex;align-items:center;height:100%;padding:0 8px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:pointer;white-space:nowrap;
579
- transition:color calc(var(--_k-hover-out) * 1ms)}
580
- .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible){color:var(--_k-fg)}
578
+ /* Hover is a quiet fill under the arrow cursor (no hand, no brightening), there at once. */
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:default;white-space:nowrap;
580
+ transition:background calc(var(--_k-hover-out) * 1ms)}
581
+ .knobs-ui .knobs-cmp-mode:not([aria-checked="true"]):is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
581
582
  /* Toggles: icon buttons, the accent's wash when on. */
582
- .knobs-ui .knobs-cmp-toggle{display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:var(--_k-radius);color:var(--_k-fg-3);cursor:pointer;
583
- transition:color calc(var(--_k-hover-out) * 1ms),background calc(var(--_k-hover-out) * 1ms)}
584
- .knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash)}
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-2);cursor:default;
584
+ transition:background calc(var(--_k-hover-out) * 1ms)}
585
+ .knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
585
586
  .knobs-ui .knobs-cmp-toggle[aria-pressed="true"]{color:var(--_k-accent-fg);background:var(--_k-accent-soft)}
586
587
  .knobs-cmp-toggle svg{display:block}
587
588
  .knobs-cmp-fill{flex:1}
@@ -595,11 +596,10 @@ var compareCss = `
595
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}
596
597
  .knobs-compare[data-frame="flat"] .knobs-cmp-stage{border-radius:0}
597
598
  .knobs-compare[data-mode="wipe"] .knobs-cmp-stage{cursor:ew-resize}
598
- /* Videos: a click on the stage plays and pauses. */
599
- .knobs-compare[data-video]:not([data-mode="wipe"],[data-zoomed]) .knobs-cmp-stage{cursor:pointer}
600
599
  /* Unzoomed, a finger still scrolls the page up and down; zoomed, the stage takes every gesture. */
601
- .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:grab;touch-action:none}
602
- .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:grabbing!important}
600
+ /* Zoomed, it pans: a move cursor, never a hand. */
601
+ .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:move;touch-action:none}
602
+ .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:move!important}
603
603
  /* Wiping, the grip stands in for the cursor. */
604
604
  .knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:none!important}
605
605
  .knobs-cmp-sizer{position:absolute;left:0;right:0;top:0;height:0;visibility:hidden;pointer-events:none}
@@ -646,8 +646,8 @@ var compareCss = `
646
646
  opacity:0;scale:.96;pointer-events:none;transition:opacity .16s cubic-bezier(.4,0,1,1),scale .16s cubic-bezier(.4,0,1,1)}
647
647
  .knobs-cmp-zoom[data-on]{opacity:1;scale:1;pointer-events:auto;transition:opacity .2s cubic-bezier(.2,.8,.2,1),scale .2s cubic-bezier(.2,.8,.2,1)}
648
648
  :where(.knobs-ui) .knobs-cmp-zoom-reset{all:revert;box-sizing:border-box;font:inherit;letter-spacing:inherit;margin:0;border:0;outline:none;box-shadow:none;
649
- display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border-radius:999px;background:transparent;color:var(--_k-fg-2);cursor:pointer;transition:color .15s,background .15s}
650
- :where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){color:var(--_k-fg);background:color-mix(in srgb,var(--_k-fg) 12%,transparent)}
649
+ display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border-radius:999px;background:transparent;color:var(--_k-fg-2);cursor:default;transition:background calc(var(--_k-hover-out) * 1ms)}
650
+ :where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
651
651
  .knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);font-size:var(--_k-text-meta);pointer-events:none}
652
652
  .knobs-cmp-note:empty{display:none}
653
653
  .knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
@@ -665,19 +665,29 @@ var playerCss = `
665
665
  .knobs-ui.knobs-player{--_kp-stage:var(--_k-compare-stage);display:flex;flex-direction:column;gap:6px;width:100%;outline:none;
666
666
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
667
667
  :where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-ui.knobs-player,.knobs-ui.knobs-player[data-nested]{--_kp-stage:var(--k-compare-stage,transparent)}
668
- .knobs-player-frame{position:relative;width:100%;aspect-ratio:var(--kp-aspect,16/9);overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_kp-stage);cursor:pointer}
668
+ .knobs-player-frame{position:relative;width:100%;aspect-ratio:var(--kp-aspect,16/9);overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_kp-stage)}
669
669
  .knobs-player[data-frame="flat"] .knobs-player-frame{border-radius:0}
670
670
  .knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
671
671
  opacity:0;filter:blur(4px);transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
672
672
  .knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}
673
- .knobs-player-glyph{position:absolute;left:50%;top:50%;display:grid;place-items:center;width:44px;height:44px;margin:-22px 0 0 -22px;border-radius:50%;color:var(--_k-fg);pointer-events:none;
674
- opacity:0;filter:blur(4px);scale:.96;
675
- background:color-mix(in srgb,var(--_k-bg) 52%,transparent);-webkit-backdrop-filter:blur(14px) saturate(1.6);backdrop-filter:blur(14px) saturate(1.6);
676
- box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim);
677
- transition:opacity calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy),filter calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy),scale calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy)}
678
- .knobs-player-glyph svg{width:16px;height:16px}
679
- .knobs-player-glyph[data-flash]{opacity:1;filter:none;scale:1}
680
673
  .knobs-player-line{flex:none;min-height:0}
674
+ /* The timeline's own stylesheet is look.ts's; what the player's row and every timeline share
675
+ about the hand is set here, after it. No hand cursor anywhere (the arrow, or a resize where it
676
+ resizes); hover is a quiet fill (fill-hover), in at once (hover-in, 0 ms), out easing. */
677
+ .knobs-ui .knobs-tl-play,.knobs-ui.knobs-timeline :is(.knobs-tl-pin,.knobs-tl-rlabel),
678
+ .knobs-ui.knobs-timeline[data-dragging] .knobs-tl-marker :is(.knobs-tl-pin,.knobs-tl-mlabel){cursor:default}
679
+ .knobs-ui .knobs-tl-play:hover,.knobs-tl-edge:hover::after,.knobs-tl-rlabel:hover,.knobs-tl-track:hover .knobs-tl-hover,
680
+ .knobs-timeline[data-variant="line"] .knobs-tl-track:hover .knobs-tl-segs{transition-duration:calc(var(--_k-hover-in) * 1ms)}
681
+ /* The line's row: a minimal play/pause (the filled pair, a 28px target, no fill at rest), then
682
+ the line, centred on it. */
683
+ .knobs-ui.knobs-timeline[data-variant="line"]{gap:4px;align-items:center}
684
+ .knobs-timeline[data-variant="line"] .knobs-tl-transport{display:flex}
685
+ .knobs-ui.knobs-timeline[data-variant="line"] .knobs-tl-play{width:28px;height:28px;min-width:28px;min-height:28px;border-radius:var(--_k-radius);background:transparent;color:var(--_k-fg-2);
686
+ transition:background calc(var(--_k-hover-out) * 1ms),transform .15s var(--_ku-ease)}
687
+ .knobs-ui.knobs-timeline[data-variant="line"][data-playing] .knobs-tl-play{background:transparent;color:var(--_k-fg-2)}
688
+ .knobs-ui.knobs-timeline[data-variant="line"] .knobs-tl-play:hover{background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
689
+ .knobs-ui.knobs-timeline[data-variant="line"] .knobs-tl-track{cursor:default;transition:background calc(var(--_k-hover-out) * 1ms)}
690
+ .knobs-ui.knobs-timeline[data-variant="line"] .knobs-tl-track:hover{background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
681
691
  `;
682
692
 
683
693
  // src/ui/overlay-css.ts
@@ -3229,16 +3239,17 @@ var init_diff_css = __esm(() => {
3229
3239
  .knobs-diff-nav>span{min-width:3.6em;text-align:center}
3230
3240
  :where(.knobs-ui) :is(.knobs-diff-btn,.knobs-diff-choice,.knobs-diff-viewed,.knobs-diff-filter>input,.knobs-diff-filter>button){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;border-radius:0;
3231
3241
  margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
3232
- .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;
3233
- transition:background .15s,color .15s,opacity .15s}
3234
- .knobs-ui .knobs-diff-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
3242
+ .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-2);cursor:default;
3243
+ transition:background calc(var(--_k-hover-out) * 1ms),opacity .15s}
3244
+ /* Hover is a quiet fill under the arrow cursor (no hand, no brightening), there at once. */
3245
+ .knobs-ui .knobs-diff-btn:is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
3235
3246
  .knobs-ui .knobs-diff-btn[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
3236
3247
  .knobs-ui .knobs-diff-btn:disabled{opacity:.35;cursor:default;background:transparent}
3237
3248
  .knobs-diff-btn svg{width:16px;height:16px}
3238
3249
  /* Viewed: a check with its word, as on GitHub; the path dims once it's ticked. */
3239
3250
  .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);
3240
- font-size:var(--_k-text-meta);font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:background .15s,color .15s}
3241
- .knobs-ui .knobs-diff-viewed:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
3251
+ font-size:var(--_k-text-meta);font-weight:var(--_k-weight-label);white-space:nowrap;cursor:default;transition:background calc(var(--_k-hover-out) * 1ms),color .15s}
3252
+ .knobs-ui .knobs-diff-viewed:is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
3242
3253
  .knobs-ui .knobs-diff-viewed[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
3243
3254
  .knobs-diff-viewed svg{width:16px;height:16px}
3244
3255
  .knobs-diff-viewed[aria-pressed="true"] svg{color:var(--kd-add)}
@@ -3257,8 +3268,8 @@ var init_diff_css = __esm(() => {
3257
3268
  choice isn't brighter for being picked. Choices are styled by class, as the copy holds them too. */
3258
3269
  .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)}
3259
3270
  .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);
3260
- font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:color calc(var(--_k-hover-out) * 1ms)}
3261
- .knobs-ui .knobs-diff-choice:is(:hover,:focus-visible){color:var(--_k-fg)}
3271
+ font-weight:var(--_k-weight-label);white-space:nowrap;cursor:default;transition:background calc(var(--_k-hover-out) * 1ms)}
3272
+ .knobs-ui .knobs-diff-choice:not([aria-checked="true"]):is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
3262
3273
  /* The diff: one layer per file or layout on show, the old fading under the new. */
3263
3274
  .knobs-diff-body{position:relative;flex:1;min-height:0}
3264
3275
  .knobs-diff-probe{position:absolute;left:0;top:0;width:0;height:var(--kd-line);visibility:hidden;pointer-events:none}
@@ -3279,8 +3290,8 @@ var init_diff_css = __esm(() => {
3279
3290
  .knobs-diff-hbar{position:absolute;height:5px;pointer-events:none}
3280
3291
  .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;
3281
3292
  transition:opacity .3s,background .15s}
3282
- .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}
3283
- .knobs-diff-hbar>i:hover,.knobs-diff-hbar[data-dragging]>i{background:color-mix(in srgb,var(--_k-fg) 48%,transparent)}
3293
+ .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)}
3294
+ .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)}
3284
3295
  /* stack: every file down one scroller, each header sticking until the next pushes it off. It's
3285
3296
  flush (square, no card around its file), opaque in the code's own colour, with a hairline
3286
3297
  under it only while it's stuck; a gap, not a line, parts one file from the next. */
@@ -3354,8 +3365,8 @@ var init_diff_css = __esm(() => {
3354
3365
  .knobs-diff-filter svg{width:12px;height:12px;flex:none}
3355
3366
  .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}
3356
3367
  .knobs-ui .knobs-diff-filter>input::placeholder{color:var(--_k-fg-3)}
3357
- .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}
3358
- .knobs-ui .knobs-diff-filter>button:hover{background:var(--_ku-wash-hi);color:var(--_k-fg)}
3368
+ .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:default;transition:background calc(var(--_k-hover-out) * 1ms),opacity .12s}
3369
+ .knobs-ui .knobs-diff-filter>button:hover{background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}
3359
3370
  .knobs-diff-filter>button svg{width:12px;height:12px}
3360
3371
  .knobs-diff-list{position:relative;flex:1;min-height:0}
3361
3372
  .knobs-diff-files .knobs-diff-counts{font-size:var(--_k-text-meta);align-items:center}
@@ -9769,7 +9780,8 @@ function fuseFrame(L, before, after, k, wipe) {
9769
9780
  const feather = Math.min(contact, 1 + FUSE.feather * Math.sin(Math.PI * t));
9770
9781
  const overlap = before.x + before.w - after.x;
9771
9782
  const seam = t > 0.001 && overlap >= 0 ? Math.min(1, Math.max(0, overlap / FUSE.seamFade)) * Math.min(1, t * 4) : 0;
9772
- return { cut, feather, seam, blur: t < 0.995 ? FUSE.blur * (1 - t) : 0 };
9783
+ const blur = t > 0.005 && t < 0.995 ? FUSE.blur * Math.sin(Math.PI * t) : 0;
9784
+ return { cut, feather, seam, blur };
9773
9785
  }
9774
9786
  function clampZoom(r) {
9775
9787
  const w = Math.min(1, Math.max(1 / ZOOM_MAX, r.w));
@@ -9856,7 +9868,10 @@ function stripPeaks(diffs, o = {}) {
9856
9868
  });
9857
9869
  return out;
9858
9870
  }
9859
- 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, labelFold = (fold, mark, blur = 3) => {
9871
+ 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) => {
9872
+ const inner = r * Math.min(1, Math.max(0, k));
9873
+ return { before: { left: r, right: inner }, after: { left: inner, right: r } };
9874
+ }, labelFold = (fold, mark, blur = 3) => {
9860
9875
  const f = Math.min(1, Math.max(0, fold));
9861
9876
  const open = 1 - f;
9862
9877
  return {
@@ -9887,7 +9902,7 @@ var init_compare_model = __esm(() => {
9887
9902
  MODE_KEYS = { side: "s", wipe: "w", difference: "d" };
9888
9903
  VIDEO = /\.(mp4|m4v|mov|webm|ogv|mkv)$/i;
9889
9904
  SEEK_IDLE = { seeking: false, pending: null };
9890
- FUSE = { feather: 24, blur: 4, seamFade: 48 };
9905
+ FUSE = { feather: 32, blur: 8, seamFade: 48 };
9891
9906
  ZOOM_FIT = { u: 0, v: 0, w: 1, h: 1 };
9892
9907
  });
9893
9908
 
@@ -10620,8 +10635,8 @@ async function createCompareGpu(canvas, onLost) {
10620
10635
  onLost();
10621
10636
  };
10622
10637
  lostListeners.add(lost);
10623
- const uniforms = device.createBuffer({ size: 176, usage: 64 | 8 });
10624
- const data = new Float32Array(44);
10638
+ const uniforms = device.createBuffer({ size: 192, usage: 64 | 8 });
10639
+ const data = new Float32Array(48);
10625
10640
  const make = (w, h) => device.createTexture({
10626
10641
  size: [Math.max(1, w), Math.max(1, h)],
10627
10642
  format: "rgba8unorm",
@@ -10694,6 +10709,7 @@ async function createCompareGpu(canvas, onLost) {
10694
10709
  data.set([f.preview[0] ? 1 : 0, f.preview[1] ? 1 : 0, 0, 0], 32);
10695
10710
  r(36, f.beforeClip);
10696
10711
  r(40, f.afterClip);
10712
+ data.set(f.radii, 44);
10697
10713
  device.queue.writeBuffer(uniforms, 0, data);
10698
10714
  const encoder = device.createCommandEncoder();
10699
10715
  const pass = encoder.beginRenderPass({
@@ -10735,6 +10751,7 @@ struct U {
10735
10751
  loupe: vec4f, // x, y, r, zoom (0: none)
10736
10752
  src: vec4f, // Before, After from their previews
10737
10753
  ca: vec4f, cb: vec4f, // where each side may draw
10754
+ radii: vec4f, // the clips' corners: Before left, right; After left, right
10738
10755
  }
10739
10756
  @group(0) @binding(0) var<uniform> u: U;
10740
10757
  @group(0) @binding(1) var A: texture_2d<f32>;
@@ -10756,8 +10773,18 @@ fn cover(p: vec2f, r: vec4f) -> f32 {
10756
10773
  return clamp(d + 0.5, 0.0, 1.0);
10757
10774
  }
10758
10775
 
10759
- // A side's colour where rect r maps it to p, and its coverage inside clip c there.
10760
- fn take(t: texture_2d<f32>, p: vec2f, r: vec4f, c0: vec4f, near: bool, ready: f32) -> vec4f {
10776
+ // Coverage of rect r at p with its left corners rounded rl and its right ones rr.
10777
+ fn coverRound(p: vec2f, r: vec4f, rl: f32, rr: f32) -> f32 {
10778
+ let h = r.zw * 0.5;
10779
+ let q0 = p - (r.xy + h);
10780
+ let rad = min(select(rl, rr, q0.x > 0.0), min(h.x, h.y));
10781
+ let q = abs(q0) - h + vec2f(rad);
10782
+ let d = length(max(q, vec2f(0.0))) + min(max(q.x, q.y), 0.0) - rad;
10783
+ return clamp(0.5 - d, 0.0, 1.0);
10784
+ }
10785
+
10786
+ // A side's colour where rect r maps it to p, and its coverage inside clip c (corners rc) there.
10787
+ fn take(t: texture_2d<f32>, p: vec2f, r: vec4f, c0: vec4f, rc: vec2f, near: bool, ready: f32) -> vec4f {
10761
10788
  let uv = clamp((p - r.xy) / max(r.zw, vec2f(1e-3)), vec2f(0.0), vec2f(1.0));
10762
10789
  var c: vec3f;
10763
10790
  if (near) {
@@ -10766,16 +10793,16 @@ fn take(t: texture_2d<f32>, p: vec2f, r: vec4f, c0: vec4f, near: bool, ready: f3
10766
10793
  } else {
10767
10794
  c = textureSampleLevel(t, smp, uv, 0.0).rgb;
10768
10795
  }
10769
- return vec4f(c, min(cover(p, r), cover(p, c0)) * ready);
10796
+ return vec4f(c, min(cover(p, r), coverRound(p, c0, rc.x, rc.y)) * ready);
10770
10797
  }
10771
10798
 
10772
10799
  fn takeA(p: vec2f, r: vec4f, c0: vec4f, near: bool) -> vec4f {
10773
- if (u.src.x > 0.5) { return take(PA, p, r, c0, near, 1.0); }
10774
- return take(A, p, r, c0, near, u.diff.z);
10800
+ if (u.src.x > 0.5) { return take(PA, p, r, c0, u.radii.xy, near, 1.0); }
10801
+ return take(A, p, r, c0, u.radii.xy, near, u.diff.z);
10775
10802
  }
10776
10803
  fn takeB(p: vec2f, r: vec4f, c0: vec4f, near: bool) -> vec4f {
10777
- if (u.src.y > 0.5) { return take(PB, p, r, c0, near, 1.0); }
10778
- return take(B, p, r, c0, near, u.diff.w);
10804
+ if (u.src.y > 0.5) { return take(PB, p, r, c0, u.radii.zw, near, 1.0); }
10805
+ return take(B, p, r, c0, u.radii.zw, near, u.diff.w);
10779
10806
  }
10780
10807
 
10781
10808
  fn pm(c: vec4f) -> vec4f { return vec4f(c.rgb * c.a, c.a); }
@@ -11195,29 +11222,6 @@ var init_compare_strip = __esm(() => {
11195
11222
  DIFF = { w: 32, h: 18 };
11196
11223
  });
11197
11224
 
11198
- // src/ui/player.ts
11199
- function playGlyph(doc) {
11200
- const element = doc.createElement("span");
11201
- element.className = "knobs-player-glyph";
11202
- element.setAttribute("aria-hidden", "true");
11203
- let timer = 0;
11204
- const win = doc.defaultView;
11205
- return {
11206
- element,
11207
- flash(playing) {
11208
- element.innerHTML = playing ? PLAY2 : PAUSE;
11209
- element.setAttribute("data-flash", "");
11210
- win.clearTimeout(timer);
11211
- timer = win.setTimeout(() => element.removeAttribute("data-flash"), FLASH_MS);
11212
- },
11213
- dispose: () => win.clearTimeout(timer)
11214
- };
11215
- }
11216
- var FLASH_MS = 450;
11217
- var init_player = __esm(() => {
11218
- init_timeline();
11219
- });
11220
-
11221
11225
  // src/ui/compare.ts
11222
11226
  function compare(container, options) {
11223
11227
  const doc = container.ownerDocument;
@@ -11336,8 +11340,7 @@ function compare(container, options) {
11336
11340
  grip.setAttribute("aria-hidden", "true");
11337
11341
  handle.append(grip);
11338
11342
  const note = el("div", "knobs-cmp-note");
11339
- const glyph = playGlyph(doc);
11340
- stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note, glyph.element);
11343
+ stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note);
11341
11344
  root.append(bar, stage);
11342
11345
  const wantsStrip = options.strip === true;
11343
11346
  let tl = bound;
@@ -11379,7 +11382,6 @@ function compare(container, options) {
11379
11382
  let drag = null;
11380
11383
  const touches = new Map;
11381
11384
  let pinch = null;
11382
- let press = null;
11383
11385
  const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
11384
11386
  const zv = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
11385
11387
  const zw = new Spring(1, ZOOM.omega, ZOOM.zeta, 0.00001);
@@ -11391,6 +11393,7 @@ function compare(container, options) {
11391
11393
  let stageRect = null;
11392
11394
  let accent = [0.49, 0.81, 0.61];
11393
11395
  let labelW = [0, 0];
11396
+ let frameRadius = 0;
11394
11397
  let dirty = true;
11395
11398
  let disposed = false;
11396
11399
  let near = options.lazy === false;
@@ -11911,6 +11914,7 @@ function compare(container, options) {
11911
11914
  const wr = wipeRect(r.L.overlay);
11912
11915
  const k = Math.min(1, Math.max(0, lay.x));
11913
11916
  const fu = fuseFrame(r.L, r.before, r.after, k, wipeLine.x);
11917
+ const corners = fuseCorners(frameRadius, k);
11914
11918
  const dw = differenceWeight();
11915
11919
  const fusing = 1 - dw;
11916
11920
  folded = k * fusing;
@@ -11967,6 +11971,7 @@ function compare(container, options) {
11967
11971
  if (renderer === "dom") {
11968
11972
  const cut = mode === "difference" ? null : fu;
11969
11973
  const place = (n, rect, clip, before) => {
11974
+ const round = before ? corners.before : corners.after;
11970
11975
  const st = n.style;
11971
11976
  st.transform = `translate(${rect.x.toFixed(2)}px,${rect.y.toFixed(2)}px)`;
11972
11977
  st.width = `${rect.w.toFixed(2)}px`;
@@ -11976,7 +11981,9 @@ function compare(container, options) {
11976
11981
  const l = Math.max(0, clip.x - rect.x);
11977
11982
  const b = Math.max(0, rect.y + rect.h - (clip.y + clip.h));
11978
11983
  const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, edge));
11979
- 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)` : "";
11984
+ const rr = edge < clip.x + clip.w - 0.5 ? 0 : round.right;
11985
+ const rl = round.left;
11986
+ 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)` : "";
11980
11987
  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)` : "";
11981
11988
  st.maskImage = mask;
11982
11989
  st.setProperty("-webkit-mask-image", mask);
@@ -12014,7 +12021,13 @@ function compare(container, options) {
12014
12021
  gain: 1,
12015
12022
  accent,
12016
12023
  loupe: loupeShown() && pointer ? { x: pointer.x * dpr, y: pointer.y * dpr, r: LOUPE.r * dpr, zoom: LOUPE.zoom } : null,
12017
- preview: [Boolean(sides[0].shown?.cached), Boolean(sides[1].shown?.cached)]
12024
+ preview: [Boolean(sides[0].shown?.cached), Boolean(sides[1].shown?.cached)],
12025
+ radii: [
12026
+ corners.before.left * dpr,
12027
+ corners.before.right * dpr,
12028
+ corners.after.left * dpr,
12029
+ corners.after.right * dpr
12030
+ ]
12018
12031
  });
12019
12032
  };
12020
12033
  const drawNow = () => {
@@ -12106,6 +12119,7 @@ function compare(container, options) {
12106
12119
  accent = [c[0], c[1], c[2]];
12107
12120
  const unfold = MARK.w - labelFold(folded, MARK.w).width + MARK.gap * folded;
12108
12121
  labelW = [labels[0].offsetWidth + unfold, labels[1].offsetWidth + unfold];
12122
+ frameRadius = Number.parseFloat(win.getComputedStyle(stage).borderTopLeftRadius) || 0;
12109
12123
  requestDraw();
12110
12124
  };
12111
12125
  const measure = () => {
@@ -12196,10 +12210,8 @@ function compare(container, options) {
12196
12210
  root.focus({ preventScroll: true });
12197
12211
  touches.set(e.pointerId, p);
12198
12212
  stage.setPointerCapture(e.pointerId);
12199
- press = touches.size === 1 && mode !== "wipe" && master() ? p : null;
12200
12213
  if (touches.size >= 2) {
12201
12214
  e.preventDefault();
12202
- press = null;
12203
12215
  return startPinch();
12204
12216
  }
12205
12217
  if (mode === "wipe") {
@@ -12227,8 +12239,6 @@ function compare(container, options) {
12227
12239
  const onMove = (e) => {
12228
12240
  const p = local(e);
12229
12241
  pointer = p;
12230
- if (press && Math.hypot(p.x - press.x, p.y - press.y) > 4)
12231
- press = null;
12232
12242
  if (touches.has(e.pointerId))
12233
12243
  touches.set(e.pointerId, p);
12234
12244
  if (pinch && touches.size >= 2) {
@@ -12258,11 +12268,6 @@ function compare(container, options) {
12258
12268
  drawNow();
12259
12269
  };
12260
12270
  const endDrag = (e) => {
12261
- if (press && e.type === "pointerup") {
12262
- api.toggle();
12263
- glyph.flash(isPlaying());
12264
- }
12265
- press = null;
12266
12271
  touches.delete(e.pointerId);
12267
12272
  if (touches.size < 2)
12268
12273
  pinch = null;
@@ -12424,7 +12429,6 @@ function compare(container, options) {
12424
12429
  old.el.replaceWith(s.el);
12425
12430
  old.preview.replaceWith(s.preview);
12426
12431
  sides[index] = s;
12427
- root.toggleAttribute("data-video", sides.some((x) => x.kind === "video"));
12428
12432
  note.textContent = "";
12429
12433
  setLabel(index, labelOf(index));
12430
12434
  if (near)
@@ -12439,7 +12443,6 @@ function compare(container, options) {
12439
12443
  stage.insertBefore(sides[0].preview, labels[0]);
12440
12444
  setLabel(0, labelOf(0));
12441
12445
  setLabel(1, labelOf(1));
12442
- root.toggleAttribute("data-video", sides.some((s) => s.kind === "video"));
12443
12446
  loupeBtn.setAttribute("aria-pressed", String(loupeOn));
12444
12447
  loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
12445
12448
  const modePill = modeList.length > 1 ? selectionPill(modes, {
@@ -12640,7 +12643,6 @@ function compare(container, options) {
12640
12643
  disposed = true;
12641
12644
  loop.stop();
12642
12645
  modePill?.dispose();
12643
- glyph.dispose();
12644
12646
  win.clearTimeout(releaseTimer);
12645
12647
  ro.disconnect();
12646
12648
  nearIo?.disconnect();
@@ -12681,7 +12683,6 @@ var init_compare = __esm(() => {
12681
12683
  init_icons();
12682
12684
  init_look();
12683
12685
  init_rgba();
12684
- init_player();
12685
12686
  init_selection_pill();
12686
12687
  init_timeline();
12687
12688
  init_timeline_model();
@@ -17198,11 +17199,629 @@ function toast(message, options = {}) {
17198
17199
  };
17199
17200
  }
17200
17201
 
17202
+ // src/ui/walkthrough-annotations.ts
17203
+ 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";
17204
+ 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";
17205
+ 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>`;
17206
+ 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>';
17207
+ var shapeOf = (a) => a.shape ?? (a.w !== undefined && a.h !== undefined ? "box" : "dot");
17208
+ var showsAt = (a, time) => !a.at || time === undefined || time >= a.at[0] && time <= a.at[1];
17209
+ function placeMark(a, rect, view = ALL) {
17210
+ const w = a.w ?? 0;
17211
+ const h = a.h ?? 0;
17212
+ const x = (a.x - view.u) / view.w;
17213
+ const y = (a.y - view.v) / view.h;
17214
+ const ww = w / view.w;
17215
+ const hh = h / view.h;
17216
+ if (x + ww < 0 || y + hh < 0 || x > 1 || y > 1)
17217
+ return null;
17218
+ return { x: rect.x + x * rect.w, y: rect.y + y * rect.h, w: ww * rect.w, h: hh * rect.h };
17219
+ }
17220
+ var ALL = { u: 0, v: 0, w: 1, h: 1 };
17221
+ var scopeOf = (el) => el.closest(".knobs-walkthrough") ?? el.closest(".knobs-wt-slot") ?? el;
17222
+ var annotationsOn = (scope) => scope.dataset.annotations !== "off";
17223
+ function setAnnotations(scope, on) {
17224
+ if (on)
17225
+ delete scope.dataset.annotations;
17226
+ else
17227
+ scope.dataset.annotations = "off";
17228
+ for (const b of Array.from(scope.querySelectorAll(".knobs-wt-anno-toggle")))
17229
+ syncToggle(b, on);
17230
+ }
17231
+ function syncToggle(b, on) {
17232
+ b.setAttribute("aria-pressed", String(on));
17233
+ b.title = on ? "Hide annotations (a)" : "Show annotations (a)";
17234
+ b.innerHTML = glyph(on ? EYE : EYE_OFF);
17235
+ }
17236
+ function annotationLayer(host, list, place, corner = host) {
17237
+ const doc = host.ownerDocument;
17238
+ const layer = doc.createElement("div");
17239
+ layer.className = "knobs-wt-anno";
17240
+ layer.setAttribute("aria-hidden", "true");
17241
+ host.append(layer);
17242
+ const toggle = doc.createElement("button");
17243
+ toggle.type = "button";
17244
+ toggle.className = "knobs-wt-anno-toggle";
17245
+ toggle.setAttribute("aria-label", "Annotations");
17246
+ syncToggle(toggle, annotationsOn(scopeOf(host)));
17247
+ toggle.addEventListener("click", (e) => {
17248
+ e.stopPropagation();
17249
+ const scope = scopeOf(host);
17250
+ setAnnotations(scope, !annotationsOn(scope));
17251
+ });
17252
+ corner.append(toggle);
17253
+ let marks = [];
17254
+ const make = (a) => {
17255
+ const m = doc.createElement("div");
17256
+ const shape = shapeOf(a);
17257
+ m.className = "knobs-wt-mark";
17258
+ m.dataset.shape = shape;
17259
+ m.dataset.hidden = "";
17260
+ if (shape === "arrow")
17261
+ m.innerHTML = ARROW;
17262
+ if (a.label) {
17263
+ const l = doc.createElement("span");
17264
+ l.className = "knobs-wt-mark-label";
17265
+ l.textContent = a.label;
17266
+ m.append(l);
17267
+ }
17268
+ layer.append(m);
17269
+ return m;
17270
+ };
17271
+ let settled = false;
17272
+ const update = () => {
17273
+ const p = place();
17274
+ if (marks.length !== p.rects.length) {
17275
+ for (const row of marks)
17276
+ for (const m of row)
17277
+ m.remove();
17278
+ marks = p.rects.map(() => list.map(make));
17279
+ }
17280
+ p.rects.forEach((rect, i) => {
17281
+ list.forEach((a, j) => {
17282
+ const m = marks[i][j];
17283
+ const box = placeMark(a, rect, p.view);
17284
+ const show = box !== null && showsAt(a, p.time);
17285
+ if (box) {
17286
+ m.style.transform = `translate(${box.x.toFixed(2)}px,${box.y.toFixed(2)}px)`;
17287
+ m.style.width = `${box.w.toFixed(2)}px`;
17288
+ m.style.height = `${box.h.toFixed(2)}px`;
17289
+ }
17290
+ m.toggleAttribute("data-hidden", !show);
17291
+ });
17292
+ });
17293
+ if (!settled) {
17294
+ settled = true;
17295
+ requestAnimationFrame(() => requestAnimationFrame(() => layer.dataset.settled = ""));
17296
+ }
17297
+ };
17298
+ const ro = new ResizeObserver(() => update());
17299
+ ro.observe(host);
17300
+ update();
17301
+ return {
17302
+ element: layer,
17303
+ update,
17304
+ dispose() {
17305
+ ro.disconnect();
17306
+ layer.remove();
17307
+ toggle.remove();
17308
+ }
17309
+ };
17310
+ }
17311
+
17201
17312
  // src/ui/walkthrough-blocks.ts
17202
17313
  init_motion();
17203
- init_icons();
17314
+
17315
+ // src/ui/history.ts
17316
+ init_motion();
17317
+ init_glide();
17318
+
17319
+ // src/ui/history-css.ts
17320
+ var historyCss = `
17321
+ :where(.knobs-history,.knobs-history *){box-sizing:border-box}
17322
+ .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);
17323
+ --hy-fade:linear-gradient(to right,#000 calc(100% - 1.5em),transparent);
17324
+ position:relative;display:flex;flex-direction:column;width:100%;min-width:0;outline:none;color:var(--_k-fg);
17325
+ font:var(--_k-weight) var(--_k-type-body)/var(--_k-leading-body) var(--_k-font);font-variant-numeric:tabular-nums;font-feature-settings:"tnum";
17326
+ text-align:left;letter-spacing:normal;-webkit-font-smoothing:antialiased;user-select:none;-webkit-user-select:none}
17327
+ .knobs-history[data-bands]{--hy-bandrow:var(--_k-leading-small)}
17328
+ .knobs-history[data-narrow]{--hy-gutter:0px}
17329
+ .knobs-history [hidden]{display:none!important}
17330
+ :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;
17331
+ background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
17332
+ .knobs-history [data-tone="good"]{--hy-c:var(--_k-ok)}
17333
+ .knobs-history [data-tone="warn"]{--hy-c:var(--_k-warn)}
17334
+ .knobs-history [data-tone="bad"]{--hy-c:var(--_k-danger)}
17335
+ .knobs-history [data-tone="info"]{--hy-c:var(--_k-info)}
17336
+ .knobs-history [data-tone="dim"]{--hy-c:var(--_k-fg-3)}
17337
+
17338
+ /* ——— the chart: lane labels beside the plot (above each lane where it's narrow) ——— */
17339
+ .knobs-hy-chart{position:relative;display:grid;grid-template-columns:var(--hy-gutter) minmax(0,1fr)}
17340
+ .knobs-hy-axis{position:relative;grid-column:2;grid-row:1;height:calc(var(--hy-bandrow) + var(--hy-axis))}
17341
+ :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)}
17342
+ .knobs-hy-tick{bottom:2px;padding-left:4px}
17343
+ .knobs-hy-now{bottom:2px;color:var(--_k-accent-fg);font-weight:var(--_k-weight-label)}
17344
+ .knobs-hy-blabel{top:0;padding-left:4px;overflow:hidden;color:var(--_k-fg-2)}
17345
+ .knobs-hy-label{grid-column:1;align-self:center;min-width:0;margin-right:var(--_k-space-title);overflow:hidden;white-space:nowrap;
17346
+ font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
17347
+ .knobs-history[data-narrow] .knobs-hy-label{grid-column:2;align-self:end;margin:0}
17348
+ :is(.knobs-hy-label,.knobs-hy-blabel)[data-overflow]{-webkit-mask-image:var(--hy-fade);mask-image:var(--hy-fade)}
17349
+ .knobs-hy-track{position:relative;grid-column:2;height:var(--hy-lane)}
17350
+ .knobs-hy-layer{position:absolute;inset:0;grid-column:2;grid-row:1/-1;pointer-events:none}
17351
+
17352
+ /* Bands and month lines behind the lanes; today, the selection and the pointer's line over them. */
17353
+ .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)}
17354
+ :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}
17355
+ .knobs-hy-grid{background:var(--_k-border-subtle)}
17356
+ .knobs-hy-today{top:calc(var(--hy-bandrow) + var(--hy-axis) - 4px);background:var(--_k-accent)}
17357
+ .knobs-hy-hover{background:color-mix(in oklch,var(--_k-fg) 32%,transparent);opacity:0;transition:opacity calc(var(--_k-hover-out) * 1ms)}
17358
+ .knobs-hy-hover[data-on]{opacity:1;transition-duration:calc(var(--_k-hover-in) * 1ms)}
17359
+ .knobs-hy-sel{background:var(--_k-fg-2);opacity:0}
17360
+ .knobs-hy-sel[data-on]{opacity:.6}
17361
+
17362
+ /* Spans: a soft bar in their state's tone, inferred ones hatched; the tag at the end. */
17363
+ .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;
17364
+ 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)}
17365
+ .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)}
17366
+ .knobs-hy-span[data-hover]{background-color:color-mix(in oklch,var(--hy-c) 34%,transparent);transition-duration:calc(var(--_k-hover-in) * 1ms)}
17367
+ .knobs-hy-span[data-selected]{box-shadow:inset 0 0 0 1px var(--hy-c)}
17368
+ .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)}
17369
+ .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)}
17370
+ .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;
17371
+ color:color-mix(in oklch,var(--hy-c) 55%,var(--_k-fg))}
17372
+ .knobs-hy-tag[data-place="after"]{right:auto;left:calc(100% + 6px)}
17373
+ .knobs-hy-tag[data-place="none"]{visibility:hidden}
17374
+
17375
+ /* 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
17376
+ channel's switch, a triangle for a finding; inferred ones hollow. */
17377
+ .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}
17378
+ .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);
17379
+ border-radius:50%;background:var(--hy-c);transition:scale calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy)}
17380
+ .knobs-hy-ev[data-range]{width:calc(var(--hy-w) + 16px)}
17381
+ .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)}
17382
+ .knobs-hy-ev[data-type="release"]{--hy-s:8px}
17383
+ .knobs-hy-ev[data-type="release"]::before{border-radius:1.5px;rotate:45deg}
17384
+ .knobs-hy-ev[data-type="channel"]{--hy-s:8px}
17385
+ .knobs-hy-ev[data-type="channel"]::before{border-radius:2px}
17386
+ .knobs-hy-ev[data-type="audit"]{--hy-s:10px}
17387
+ .knobs-hy-ev[data-type="audit"]::before{border-radius:0;clip-path:polygon(50% 4%,100% 92%,0 92%)}
17388
+ .knobs-hy-ev[data-type="deletion"]::before{border-radius:0;rotate:45deg;
17389
+ 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}
17390
+ .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)}
17391
+ .knobs-hy-ev[data-inferred]:is([data-type="audit"],[data-type="deletion"])::before{opacity:.5}
17392
+ /* Held-back marks (commits, by default) come up while the pointer is on their lane, and stay with
17393
+ "Show commits" or while selected. */
17394
+ .knobs-hy-ev[data-quiet]{opacity:0;transition:opacity calc(var(--_k-hover-out) * 1ms)}
17395
+ .knobs-hy-track:hover .knobs-hy-ev[data-quiet]{opacity:.6;transition-duration:calc(var(--_k-motion-snappy-ms) * 1ms)}
17396
+ :is(.knobs-history[data-all] .knobs-hy-ev[data-quiet],.knobs-hy-ev[data-quiet]:is([data-selected],[data-hover])){opacity:1}
17397
+ .knobs-hy-ev[data-hover]::before{scale:1.25}
17398
+ .knobs-hy-ev[data-selected]::before{scale:1.3}
17399
+ .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);
17400
+ 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)}
17401
+ .knobs-hy-ev[data-range][data-selected]::after{left:3px;right:3px;width:auto;margin-left:0;border-radius:var(--_k-round)}
17402
+ .knobs-history:focus-visible .knobs-hy-ev[data-selected]::after{box-shadow:0 0 0 2px var(--_k-focus)}
17403
+ .knobs-history:focus-visible .knobs-hy-span[data-selected]{box-shadow:inset 0 0 0 2px var(--_k-focus)}
17404
+
17405
+ /* ——— under the chart: "Show commits", and a legend only when inferred and verified both appear ——— */
17406
+ .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);
17407
+ font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
17408
+ .knobs-hy-legend{display:flex;flex-wrap:wrap;gap:var(--_k-space-inside) var(--_k-space-title)}
17409
+ .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;
17410
+ transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
17411
+ .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)}
17412
+ .knobs-hy-key{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
17413
+ .knobs-hy-swatch{flex:none;width:14px;height:8px;border-radius:2px;background:color-mix(in oklch,var(--hy-c) 34%,transparent)}
17414
+ .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)}
17415
+
17416
+ /* ——— the detail: one area under the chart, its height on a spring ——— */
17417
+ .knobs-hy-detail{position:relative;overflow:hidden;user-select:text;-webkit-user-select:text}
17418
+ .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)}
17419
+ .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)}
17420
+ .knobs-hy-dc:is([data-enter],[data-leave]){opacity:0}
17421
+ .knobs-hy-dc[data-leave]{position:absolute;left:0;right:0;top:0;transition-duration:calc(var(--_k-motion-snappy-ms) * .8ms)}
17422
+ .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)}
17423
+ .knobs-hy-dmeta>[data-tone]{color:var(--hy-c)}
17424
+ .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}
17425
+ .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}
17426
+ .knobs-hy-dtext code{font:calc(1em - 1px) var(--_k-mono);padding:1px 4px;border-radius:var(--_k-radius-sm);background:var(--_k-fill)}
17427
+ .knobs-hy-dfoot{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:var(--_k-space-sibling)}
17428
+ .knobs-hy-dfoot:empty{display:none}
17429
+ .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);
17430
+ 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)}
17431
+ a.knobs-hy-evidence:hover{background:var(--_k-fill-hover);color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
17432
+ .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;
17433
+ font-size:var(--_k-type-small);transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
17434
+ .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)}
17435
+ .knobs-hy-jump svg{width:12px;height:12px}
17436
+
17437
+ /* ——— the pointer's tooltip: title, date, one line ——— */
17438
+ .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);
17439
+ 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)}
17440
+ .knobs-hy-tip[data-on]{opacity:1;transition-duration:calc(var(--_k-hover-in) * 1ms)}
17441
+ .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}
17442
+ :is(.knobs-hy-td,.knobs-hy-tl){font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
17443
+ .knobs-hy-tl{margin-top:2px;color:var(--_k-fg-2);text-wrap:pretty}
17444
+ .knobs-hy-tl:empty{display:none}
17445
+
17446
+ @media(prefers-reduced-motion:reduce){
17447
+ .knobs-history :where(*,::before,::after){transition-property:opacity,background,color,box-shadow!important}
17448
+ .knobs-hy-dc{transition-duration:calc(var(--_k-hover-out) * 1ms)!important}
17449
+ }
17450
+ `;
17451
+
17452
+ // src/ui/history-model.ts
17453
+ var MS_DAY = 86400000;
17454
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
17455
+ var dayOf = (y, m, d) => Math.floor(Date.UTC(y, m, d) / MS_DAY);
17456
+ var parts = (day) => {
17457
+ const t = new Date(day * MS_DAY);
17458
+ return { y: t.getUTCFullYear(), m: t.getUTCMonth(), d: t.getUTCDate() };
17459
+ };
17460
+ var todayDay = (now = Date.now()) => {
17461
+ const t = new Date(now);
17462
+ return dayOf(t.getFullYear(), t.getMonth(), t.getDate());
17463
+ };
17464
+ var monthIndex = (name) => MONTHS.findIndex((m) => m.toLowerCase() === name.slice(0, 3).toLowerCase());
17465
+ function parseDay(text, year) {
17466
+ const s = text.trim();
17467
+ const iso = /^(\d{4})-(\d{2})-(\d{2})/.exec(s);
17468
+ if (iso) {
17469
+ const y = Number(iso[1]);
17470
+ const day = dayOf(y, Number(iso[2]) - 1, Number(iso[3]));
17471
+ return { from: day, to: day, year: y };
17472
+ }
17473
+ 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);
17474
+ if (!m)
17475
+ return null;
17476
+ const m1 = monthIndex(m[1]);
17477
+ const m2 = m[3] ? monthIndex(m[3]) : m1;
17478
+ if (m1 < 0 || m2 < 0)
17479
+ return null;
17480
+ const written = m[5] ? Number(m[5]) : undefined;
17481
+ const y = written ?? year;
17482
+ if (y === undefined)
17483
+ return null;
17484
+ const from = dayOf(y, m1, Number(m[2]));
17485
+ let to = m[4] ? dayOf(y, m2, Number(m[4])) : from;
17486
+ if (to < from)
17487
+ to = dayOf(y + 1, m2, Number(m[4]));
17488
+ return { from, to, ...written !== undefined ? { year: written } : {} };
17489
+ }
17490
+ function formatDay(day) {
17491
+ const { y, m, d } = parts(day);
17492
+ return `${MONTHS[m]} ${d}, ${y}`;
17493
+ }
17494
+ function formatRange(from, to, open = false) {
17495
+ if (open)
17496
+ return `${formatDay(from)} – today`;
17497
+ if (to <= from)
17498
+ return formatDay(from);
17499
+ const a = parts(from);
17500
+ const b = parts(to);
17501
+ if (a.y === b.y && a.m === b.m)
17502
+ return `${MONTHS[a.m]} ${a.d}–${b.d}, ${a.y}`;
17503
+ if (a.y === b.y)
17504
+ return `${MONTHS[a.m]} ${a.d} – ${MONTHS[b.m]} ${b.d}, ${a.y}`;
17505
+ return `${formatDay(from)} – ${formatDay(to)}`;
17506
+ }
17507
+ var formatDays = (n) => `${n} ${n === 1 ? "day" : "days"}`;
17508
+ function guessTone(state) {
17509
+ const s = state.toLowerCase();
17510
+ if (/broken|bad|fail|missing|deleted|down/.test(s))
17511
+ return "bad";
17512
+ if (/warn|redirect|partial|beta|next|pre/.test(s))
17513
+ return "warn";
17514
+ if (/work|ok|good|fixed|live|green/.test(s))
17515
+ return "good";
17516
+ return "dim";
17517
+ }
17518
+ var bandLabel = (heading) => heading.replace(/^\s*\d+\s*[·.:)\-–]\s*/, "").replace(/\s*\([^)]*\)\s*$/, "").trim();
17519
+ function fromGroups(groups, today) {
17520
+ const events = [];
17521
+ const bands = [];
17522
+ const all = groups.flatMap((g) => g.events ?? g.rows ?? []);
17523
+ let year = all.map((e) => parseDay(e.date, 0)?.year).find((y) => y !== undefined) ?? parts(today).y;
17524
+ for (const g of groups) {
17525
+ let lo = Infinity;
17526
+ let hi = -Infinity;
17527
+ for (const e of g.events ?? g.rows ?? []) {
17528
+ const d = parseDay(e.date, year);
17529
+ if (!d)
17530
+ continue;
17531
+ if (d.year !== undefined)
17532
+ year = d.year;
17533
+ lo = Math.min(lo, d.from);
17534
+ hi = Math.max(hi, d.to);
17535
+ events.push({
17536
+ ...e,
17537
+ date: iso(d.from),
17538
+ ...d.to > d.from ? { to: iso(d.to) } : {},
17539
+ lane: e.lane ?? "code"
17540
+ });
17541
+ }
17542
+ const label = g.heading ? bandLabel(g.heading) : "";
17543
+ if (label && lo <= hi)
17544
+ bands.push({ from: iso(lo), to: iso(hi), label });
17545
+ }
17546
+ return { events, bands };
17547
+ }
17548
+ function iso(day) {
17549
+ const { y, m, d } = parts(day);
17550
+ return `${y}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
17551
+ }
17552
+ var startOfMonth = (day) => {
17553
+ const { y, m } = parts(day);
17554
+ return dayOf(y, m, 1);
17555
+ };
17556
+ var TYPE_TONE = { deletion: "bad" };
17557
+ function normalizeHistory(data, now = todayDay()) {
17558
+ const today = data.axis?.today && parseDay(data.axis.today, parts(now).y)?.from || now;
17559
+ const converted = data.groups?.length ? fromGroups(data.groups, today) : { events: [], bands: [] };
17560
+ const rawEvents = [...data.events ?? [], ...converted.events];
17561
+ const rawBands = [...data.bands ?? [], ...converted.bands];
17562
+ const year = parts(today).y;
17563
+ const day = (s) => s ? parseDay(s, year) ?? null : null;
17564
+ const states = new Map;
17565
+ const stateOf = (key) => {
17566
+ const known = states.get(key);
17567
+ if (known)
17568
+ return known;
17569
+ const def = data.states?.[key];
17570
+ const s = { key, label: def?.label ?? key, tone: def?.tone ?? guessTone(key) };
17571
+ states.set(key, s);
17572
+ return s;
17573
+ };
17574
+ const items = new Map;
17575
+ const laneOrder = [];
17576
+ const lanes = new Map;
17577
+ const lane = (id, label = id === "code" ? "Code" : id) => {
17578
+ let l = lanes.get(id);
17579
+ if (!l) {
17580
+ l = { id, label, spans: [], events: [] };
17581
+ lanes.set(id, l);
17582
+ laneOrder.push(id);
17583
+ }
17584
+ return l;
17585
+ };
17586
+ if (rawEvents.some((e) => !e.lane || e.lane === "code") && !data.lanes?.some((l) => l.id === "code"))
17587
+ lane("code");
17588
+ let inferred = false;
17589
+ for (const l of data.lanes ?? []) {
17590
+ const into = lane(l.id, l.label);
17591
+ (l.spans ?? []).forEach((s, i) => {
17592
+ const a = day(s.from);
17593
+ if (!a)
17594
+ return;
17595
+ const open = !s.to;
17596
+ const b = open ? null : day(s.to);
17597
+ const to = open ? today : b?.to ?? a.to;
17598
+ const st = stateOf(s.state);
17599
+ const days = Math.max(0, to - a.from);
17600
+ const own = s.label ?? data.states?.[s.state]?.tag ?? "{d}d";
17601
+ const tags = own === "" ? [] : [...new Set([own, "{d}d"])].map((t) => t.replace(/\{d\}/g, String(days)));
17602
+ const id = s.id ?? `${l.id}/${i}`;
17603
+ const item = {
17604
+ id,
17605
+ kind: "span",
17606
+ lane: l.id,
17607
+ from: a.from,
17608
+ to,
17609
+ open,
17610
+ title: s.title ?? `${l.label} · ${st.label}`,
17611
+ ...s.detail ? { detail: s.detail } : {},
17612
+ tone: st.tone,
17613
+ inferred: s.provenance === "inferred",
17614
+ ...s.provenance ? { provenance: s.provenance } : {},
17615
+ ...s.source ? { source: s.source } : {},
17616
+ evidence: s.evidence ?? [],
17617
+ ...s.step ? { step: s.step } : {},
17618
+ state: s.state,
17619
+ tags
17620
+ };
17621
+ if (item.inferred)
17622
+ inferred = true;
17623
+ into.spans.push(item);
17624
+ items.set(id, item);
17625
+ });
17626
+ }
17627
+ const types = [];
17628
+ const events = [];
17629
+ rawEvents.forEach((e, index) => {
17630
+ const a = day(e.date);
17631
+ if (!a)
17632
+ return;
17633
+ const to = Math.max(a.to, day(e.to)?.to ?? a.to);
17634
+ const into = lane(e.lane ?? "code");
17635
+ const id = e.id ?? `e${index}`;
17636
+ const item = {
17637
+ id,
17638
+ kind: "event",
17639
+ lane: into.id,
17640
+ from: a.from,
17641
+ to,
17642
+ open: false,
17643
+ title: e.title,
17644
+ ...e.detail ? { detail: e.detail } : {},
17645
+ tone: e.tone ?? TYPE_TONE[e.type ?? ""] ?? "dim",
17646
+ ...e.type ? { type: e.type } : {},
17647
+ inferred: e.provenance === "inferred",
17648
+ ...e.provenance ? { provenance: e.provenance } : {},
17649
+ ...e.source ? { source: e.source } : {},
17650
+ evidence: e.evidence ?? [],
17651
+ ...e.step ? { step: e.step } : {},
17652
+ tags: []
17653
+ };
17654
+ if (item.inferred)
17655
+ inferred = true;
17656
+ if (e.type && !types.includes(e.type))
17657
+ types.push(e.type);
17658
+ into.events.push(item);
17659
+ items.set(id, item);
17660
+ events.push({ item, index });
17661
+ });
17662
+ const bands = [];
17663
+ for (const b of rawBands) {
17664
+ const a = day(b.from);
17665
+ if (!a)
17666
+ continue;
17667
+ const z = b.to ? day(b.to) : null;
17668
+ bands.push({ from: a.from, to: z ? z.to : today, label: b.label });
17669
+ }
17670
+ bands.sort((x, y) => x.from - y.from);
17671
+ const all = [...items.values()].flatMap((i) => [i.from, i.to]);
17672
+ for (const b of bands)
17673
+ all.push(b.from, b.to);
17674
+ const hasOpen = [...items.values()].some((i) => i.open);
17675
+ const lo = all.length ? Math.min(...all) : today - 90;
17676
+ let hi = all.length ? Math.max(...all) : today;
17677
+ if (hasOpen || data.axis?.today)
17678
+ hi = Math.max(hi, today);
17679
+ const from = day(data.axis?.from)?.from ?? startOfMonth(lo);
17680
+ const to = day(data.axis?.to)?.to ?? hi + Math.max(6, Math.round((hi - from) * 0.05));
17681
+ const laneIndex = (id) => laneOrder.indexOf(id);
17682
+ const order = eventOrder(events.map((e) => ({ ...e, laneIndex: laneIndex(e.item.lane) })));
17683
+ for (const l of lanes.values())
17684
+ l.events.sort((a, b) => a.from - b.from);
17685
+ return {
17686
+ from,
17687
+ to: Math.max(to, from + 1),
17688
+ today,
17689
+ lanes: laneOrder.map((id) => lanes.get(id)),
17690
+ events: order,
17691
+ bands,
17692
+ states: [...states.values()],
17693
+ types,
17694
+ inferred,
17695
+ verified: [...items.values()].some((i) => i.provenance === "verified"),
17696
+ items
17697
+ };
17698
+ }
17699
+ var DEFAULT_MARKS = ["release", "deletion"];
17700
+ function quietEvents(events, marks) {
17701
+ if (marks === "all")
17702
+ return new Set;
17703
+ const quiet = events.filter((e) => e.type && !marks.includes(e.type));
17704
+ return quiet.length === events.length ? new Set : new Set(quiet.map((e) => e.id));
17705
+ }
17706
+ var PLURALS = {
17707
+ commit: "commits",
17708
+ release: "releases",
17709
+ deletion: "deletions",
17710
+ channel: "channel switches",
17711
+ audit: "findings"
17712
+ };
17713
+ function quietLabel(events, quiet) {
17714
+ const types = [];
17715
+ for (const e of events)
17716
+ if (quiet.has(e.id) && e.type && !types.includes(e.type))
17717
+ types.push(e.type);
17718
+ const names = types.map((t) => PLURALS[t] ?? `${t}s`);
17719
+ return names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}` : names[0] ?? "";
17720
+ }
17721
+ function eventOrder(events) {
17722
+ return [...events].sort((a, b) => a.item.from - b.item.from || a.laneIndex - b.laneIndex || a.index - b.index).map((e) => e.item);
17723
+ }
17724
+ function stepEvent(order, current, dir, at) {
17725
+ if (!order.length)
17726
+ return null;
17727
+ const i = current === null ? -1 : order.findIndex((e) => e.id === current);
17728
+ if (i < 0 && at !== undefined) {
17729
+ const next = dir > 0 ? order.find((e) => e.from >= at) : [...order].reverse().find((e) => e.from <= at);
17730
+ return next ?? (dir > 0 ? order[order.length - 1] : order[0]);
17731
+ }
17732
+ if (i < 0)
17733
+ return dir > 0 ? order[0] : order[order.length - 1];
17734
+ return order[Math.max(0, Math.min(order.length - 1, i + dir))];
17735
+ }
17736
+ function dayScale(from, to, width) {
17737
+ const span = Math.max(1, to - from);
17738
+ return {
17739
+ x: (day) => (day - from) / span * width,
17740
+ day: (x) => from + x / Math.max(1, width) * span,
17741
+ f: (day) => (day - from) / span
17742
+ };
17743
+ }
17744
+ var STEPS = [1, 2, 3, 6, 12];
17745
+ function monthTicks(from, to, width, minGap = 44) {
17746
+ const perMonth = width / Math.max(1, to - from) * 30.44;
17747
+ const step = STEPS.find((s) => s * perMonth >= minGap) ?? 12;
17748
+ const ticks = [];
17749
+ const { y } = parts(from);
17750
+ let { m } = parts(from);
17751
+ let d = dayOf(y, m, 1);
17752
+ if (d < from) {
17753
+ m++;
17754
+ d = dayOf(y, m, 1);
17755
+ }
17756
+ while (d <= to) {
17757
+ const p = parts(d);
17758
+ if (p.m % step === 0)
17759
+ ticks.push({ day: d, label: p.m === 0 ? String(p.y) : MONTHS[p.m] });
17760
+ m++;
17761
+ d = dayOf(y, m, 1);
17762
+ }
17763
+ return ticks;
17764
+ }
17765
+ function fitLabels(labels, width, reserved = [], gap = 8) {
17766
+ let end = -Infinity;
17767
+ return labels.map(({ x, w }) => {
17768
+ const x1 = x + w;
17769
+ if (x < end + gap || x1 > width)
17770
+ return false;
17771
+ if (reserved.some((r) => x < r.x1 + gap && x1 > r.x0 - gap))
17772
+ return false;
17773
+ end = x1;
17774
+ return true;
17775
+ });
17776
+ }
17777
+ function clipSpan(from, to, axisFrom, axisTo) {
17778
+ if (to < axisFrom || from > axisTo)
17779
+ return null;
17780
+ return {
17781
+ from: Math.max(from, axisFrom),
17782
+ to: Math.min(to, axisTo),
17783
+ clipStart: from < axisFrom,
17784
+ clipEnd: to > axisTo
17785
+ };
17786
+ }
17787
+ function fitTag(widths, spanWidth, roomAfter, pad = 6) {
17788
+ for (let i = 0;i < widths.length; i++)
17789
+ if (widths[i] + pad * 2 <= spanWidth)
17790
+ return { index: i, place: "inside" };
17791
+ for (let i = 0;i < widths.length; i++)
17792
+ if (widths[i] + pad <= roomAfter)
17793
+ return { index: i, place: "after" };
17794
+ return null;
17795
+ }
17796
+ function bandRoom(bands, x) {
17797
+ return bands.map((b, i) => {
17798
+ const next = bands.slice(i + 1).find((n) => n.from > b.from);
17799
+ const end = Math.min(b.to, next ? next.from : Infinity);
17800
+ return Math.max(0, x(end) - x(b.from));
17801
+ });
17802
+ }
17803
+ function nearestEvent(events, day, tolerance) {
17804
+ let best = null;
17805
+ let gap = tolerance;
17806
+ for (const e of events) {
17807
+ const d = day < e.from ? e.from - day : day > e.to ? day - e.to : 0;
17808
+ if (d <= gap) {
17809
+ best = e;
17810
+ gap = d;
17811
+ }
17812
+ }
17813
+ return best;
17814
+ }
17815
+ function firstSentence(text) {
17816
+ if (!text)
17817
+ return "";
17818
+ const plain = text.replace(/`([^`]*)`/g, "$1").replace(/\*\*?([^*]+)\*\*?/g, "$1").trim();
17819
+ const m = /^.*?[.!?](?=\s|$)/.exec(plain);
17820
+ return (m ? m[0] : plain).trim();
17821
+ }
17822
+
17823
+ // src/ui/history.ts
17204
17824
  init_look();
17205
- init_selection_pill();
17206
17825
 
17207
17826
  // src/ui/walkthrough-model.ts
17208
17827
  var DEFAULT_VERDICTS = ["ship", "change", "unsure"];
@@ -17268,7 +17887,14 @@ function fillMedia(m, base) {
17268
17887
  case "image":
17269
17888
  return { ...m, src: resolveSrc(m.src, base) };
17270
17889
  case "history":
17271
- return { kind: "history", groups: fillHistory(m) };
17890
+ if ("data" in m)
17891
+ return m;
17892
+ return {
17893
+ kind: "history",
17894
+ data: historyData(m),
17895
+ ...m.label ? { label: m.label } : {},
17896
+ ...m.marks ? { marks: m.marks } : {}
17897
+ };
17272
17898
  case "stack":
17273
17899
  return {
17274
17900
  kind: "stack",
@@ -17281,17 +17907,26 @@ function fillMedia(m, base) {
17281
17907
  return m;
17282
17908
  }
17283
17909
  }
17284
- function fillHistory(m) {
17910
+ function historyData(m) {
17911
+ const events = m.events ?? [];
17912
+ if (m.lanes || m.bands || m.states || m.axis)
17913
+ return {
17914
+ ...m.axis ? { axis: m.axis } : {},
17915
+ ...m.lanes ? { lanes: m.lanes } : {},
17916
+ ...events.length ? { events } : {},
17917
+ ...m.bands ? { bands: m.bands } : {},
17918
+ ...m.states ? { states: m.states } : {}
17919
+ };
17285
17920
  const groups = [];
17286
- if (m.events?.length)
17287
- groups.push({ events: m.events });
17921
+ if (events.length)
17922
+ groups.push({ events });
17288
17923
  for (const g of m.groups ?? [])
17289
17924
  groups.push({
17290
17925
  ...g.heading ? { heading: g.heading } : {},
17291
17926
  ...g.summary ? { summary: g.summary } : {},
17292
17927
  events: g.events ?? g.rows ?? []
17293
17928
  });
17294
- return groups;
17929
+ return { groups };
17295
17930
  }
17296
17931
  function parseBranch(branch) {
17297
17932
  if (!branch?.trim())
@@ -17578,9 +18213,10 @@ function knownPRs(doc, sections) {
17578
18213
  }
17579
18214
  function findPR(prs, n, repo) {
17580
18215
  const known = prs[n];
18216
+ const href = repo ? `https://github.com/${repo}/pull/${n}` : undefined;
17581
18217
  if (known)
17582
- return known;
17583
- return repo ? { href: `https://github.com/${repo}/pull/${n}` } : null;
18218
+ return known.href || !href ? known : { ...known, href };
18219
+ return href ? { href } : null;
17584
18220
  }
17585
18221
  var isPullRequest = (href) => /\/(?:pull|merge_requests)\/\d+(?:[/?#]|$)/.test(href);
17586
18222
  function withKickers(sections, source, kickers = "pr") {
@@ -17910,11 +18546,18 @@ function inlineMarkdown(text) {
17910
18546
  return s.replace(/\u0000(\d+)\u0000/g, (_, i) => `<code>${escapeHtml(codes[Number(i)])}</code>`);
17911
18547
  }
17912
18548
 
17913
- // src/ui/walkthrough-blocks.ts
17914
- var GLYPH = {
17915
- go: icon("caretRight", { small: true }),
17916
- caret: icon("caretRight", { small: true })
18549
+ // src/ui/history.ts
18550
+ var HEIGHT2 = springMotion(SPRINGS.default);
18551
+ var GO = svg('<path d="M5 5h6v6M11 5l-6 6"/>');
18552
+ var TYPE_NAMES = {
18553
+ commit: "Commit",
18554
+ release: "Release",
18555
+ deletion: "Deletion",
18556
+ channel: "Channel switch",
18557
+ audit: "Finding"
17917
18558
  };
18559
+ var typeName = (t) => TYPE_NAMES[t] ?? t.charAt(0).toUpperCase() + t.slice(1);
18560
+ var REACH = 8;
17918
18561
  var el = (doc, tag, cls) => {
17919
18562
  const n = doc.createElement(tag);
17920
18563
  if (cls)
@@ -17926,112 +18569,625 @@ var text = (doc, tag, cls, value) => {
17926
18569
  n.textContent = value;
17927
18570
  return n;
17928
18571
  };
18572
+ function injectHistoryStyles(doc) {
18573
+ if (doc.querySelector("style[data-knobs-history]"))
18574
+ return;
18575
+ const style = doc.createElement("style");
18576
+ style.dataset.knobsHistory = "";
18577
+ style.textContent = aliased(historyCss);
18578
+ doc.head.append(style);
18579
+ }
18580
+ var when = (it) => it.kind === "span" ? `${formatRange(it.from, it.to, it.open)} · ${formatDays(it.to - it.from)}` : formatRange(it.from, it.to);
18581
+ function history(container, options) {
18582
+ const doc = container.ownerDocument;
18583
+ const win = doc.defaultView;
18584
+ const root = el(doc, "div", "knobs-history");
18585
+ applyLook2(root, options);
18586
+ injectHistoryStyles(doc);
18587
+ root.tabIndex = 0;
18588
+ root.setAttribute("role", "group");
18589
+ root.setAttribute("aria-label", options.label ?? "History");
18590
+ root.setAttribute("aria-roledescription", "timeline");
18591
+ const chart = el(doc, "div", "knobs-hy-chart");
18592
+ const foot = el(doc, "div", "knobs-hy-foot");
18593
+ const detail = el(doc, "div", "knobs-hy-detail");
18594
+ detail.setAttribute("aria-live", "polite");
18595
+ const tip = el(doc, "div", "knobs-hy-tip");
18596
+ tip.setAttribute("aria-hidden", "true");
18597
+ const tipTitle = el(doc, "div", "knobs-hy-tt");
18598
+ const tipDate = el(doc, "div", "knobs-hy-td");
18599
+ const tipLine = el(doc, "div", "knobs-hy-tl");
18600
+ tip.append(tipTitle, tipDate, tipLine);
18601
+ root.append(chart, foot, detail, tip);
18602
+ container.append(root);
18603
+ let model = normalizeHistory(options.data);
18604
+ let selected = options.selected && model.items.has(options.selected) ? options.selected : null;
18605
+ let disposed = false;
18606
+ let quiet = quietEvents(model.events, options.marks ?? DEFAULT_MARKS);
18607
+ let showAll = false;
18608
+ const walked = () => showAll ? model.events : model.events.filter((e) => !quiet.has(e.id));
18609
+ let nodes = new Map;
18610
+ let tracks = new Map;
18611
+ let over = null;
18612
+ let hoverLine = null;
18613
+ let selLine = null;
18614
+ let scale = dayScale(model.from, model.to, 1);
18615
+ let drawnWidth = -1;
18616
+ let hovered = null;
18617
+ let tipKey = "";
18618
+ const fontOf = (n) => {
18619
+ const c = win.getComputedStyle(n);
18620
+ return `${c.fontStyle} ${c.fontWeight} ${c.fontSize} ${c.fontFamily}`;
18621
+ };
18622
+ let measureCtx;
18623
+ const measure = (s, font) => {
18624
+ if (measureCtx === undefined)
18625
+ measureCtx = doc.createElement("canvas").getContext("2d");
18626
+ if (!measureCtx)
18627
+ return s.length * 6.4;
18628
+ measureCtx.font = font;
18629
+ return measureCtx.measureText(s).width;
18630
+ };
18631
+ function drawFoot() {
18632
+ foot.textContent = "";
18633
+ root.toggleAttribute("data-all", showAll);
18634
+ if (quiet.size) {
18635
+ const t = el(doc, "button", "knobs-hy-more");
18636
+ t.type = "button";
18637
+ t.setAttribute("aria-pressed", String(showAll));
18638
+ t.textContent = `${showAll ? "Hide" : "Show"} ${quietLabel(model.events, quiet)}`;
18639
+ t.addEventListener("click", () => {
18640
+ showAll = !showAll;
18641
+ if (!showAll && selected && quiet.has(selected))
18642
+ select(null, true, true);
18643
+ drawFoot();
18644
+ });
18645
+ foot.append(t);
18646
+ }
18647
+ if (model.inferred && model.verified) {
18648
+ const legend = el(doc, "span", "knobs-hy-legend");
18649
+ for (const [label, inferred] of [
18650
+ ["Verified", false],
18651
+ ["Inferred", true]
18652
+ ]) {
18653
+ const k = el(doc, "span", "knobs-hy-key");
18654
+ const sw = el(doc, "span", "knobs-hy-swatch");
18655
+ sw.dataset.tone = "dim";
18656
+ if (inferred)
18657
+ sw.dataset.inferred = "";
18658
+ k.append(sw, text(doc, "span", "", label));
18659
+ legend.append(k);
18660
+ }
18661
+ foot.append(legend);
18662
+ }
18663
+ foot.hidden = !foot.childElementCount;
18664
+ }
18665
+ function draw() {
18666
+ const width = root.clientWidth;
18667
+ drawnWidth = width;
18668
+ const narrow = width > 0 && width < (options.narrow ?? 560);
18669
+ root.toggleAttribute("data-narrow", narrow);
18670
+ root.toggleAttribute("data-bands", model.bands.length > 0);
18671
+ hideHover();
18672
+ chart.textContent = "";
18673
+ nodes = new Map;
18674
+ tracks = new Map;
18675
+ let gutter = 0;
18676
+ if (!narrow) {
18677
+ const probe = el(doc, "div", "knobs-hy-label");
18678
+ chart.append(probe);
18679
+ const font = fontOf(probe);
18680
+ const gap = parseFloat(win.getComputedStyle(probe).marginRight) || 0;
18681
+ probe.remove();
18682
+ const longest = Math.max(0, ...model.lanes.map((l) => measure(l.label, font)));
18683
+ gutter = Math.round(Math.min(Math.max(longest + gap + 2, 64), Math.max(96, width * 0.3), 200));
18684
+ }
18685
+ root.style.setProperty("--hy-gutter", `${gutter}px`);
18686
+ const plotW = Math.max(1, width - gutter);
18687
+ scale = dayScale(model.from, model.to, plotW);
18688
+ const pct = (d) => `${(scale.f(d) * 100).toFixed(4)}%`;
18689
+ const rows = ["auto"];
18690
+ for (let i = 0;i < model.lanes.length; i++)
18691
+ rows.push(...narrow ? ["auto", "var(--hy-lane)"] : ["var(--hy-lane)"]);
18692
+ chart.style.gridTemplateRows = rows.join(" ");
18693
+ const behind = el(doc, "div", "knobs-hy-layer");
18694
+ behind.setAttribute("aria-hidden", "true");
18695
+ for (const b of model.bands) {
18696
+ const c = clipSpan(b.from, b.to, model.from, model.to);
18697
+ if (!c)
18698
+ continue;
18699
+ const band = el(doc, "div", "knobs-hy-band");
18700
+ band.style.left = pct(c.from);
18701
+ band.style.width = pct(model.from + (c.to - c.from));
18702
+ behind.append(band);
18703
+ }
18704
+ const ticks = monthTicks(model.from, model.to, plotW);
18705
+ for (const t of ticks) {
18706
+ const line = el(doc, "div", "knobs-hy-grid");
18707
+ line.style.left = pct(t.day);
18708
+ behind.append(line);
18709
+ }
18710
+ chart.append(behind);
18711
+ const axis = el(doc, "div", "knobs-hy-axis");
18712
+ axis.setAttribute("aria-hidden", "true");
18713
+ const tickEls = ticks.map((t) => {
18714
+ const n = text(doc, "span", "knobs-hy-tick", t.label);
18715
+ n.style.left = pct(t.day);
18716
+ axis.append(n);
18717
+ return n;
18718
+ });
18719
+ const showToday = model.today >= model.from && model.today <= model.to;
18720
+ const now = showToday ? text(doc, "span", "knobs-hy-now", "Today") : null;
18721
+ if (now)
18722
+ axis.append(now);
18723
+ const room = bandRoom(model.bands, scale.x);
18724
+ const bandEls = model.bands.map((b, i) => {
18725
+ const n = text(doc, "span", "knobs-hy-blabel", b.label);
18726
+ n.style.left = pct(Math.max(b.from, model.from));
18727
+ n.style.maxWidth = `${Math.max(0, room[i])}px`;
18728
+ axis.append(n);
18729
+ return n;
18730
+ });
18731
+ chart.append(axis);
18732
+ model.lanes.forEach((lane, i) => {
18733
+ const row = narrow ? 2 + i * 2 : 2 + i;
18734
+ const label = text(doc, "div", "knobs-hy-label", lane.label);
18735
+ label.style.gridRow = String(row);
18736
+ label.title = lane.label;
18737
+ const track = el(doc, "div", "knobs-hy-track");
18738
+ track.style.gridRow = String(narrow ? row + 1 : row);
18739
+ track.dataset.lane = lane.id;
18740
+ tracks.set(lane.id, track);
18741
+ for (const s of lane.spans) {
18742
+ const c = clipSpan(s.from, s.to, model.from, model.to);
18743
+ if (!c)
18744
+ continue;
18745
+ const b = el(doc, "button", "knobs-hy-span");
18746
+ b.type = "button";
18747
+ b.tabIndex = -1;
18748
+ b.dataset.id = s.id;
18749
+ b.dataset.tone = s.tone;
18750
+ if (s.inferred)
18751
+ b.dataset.inferred = "";
18752
+ if (c.clipStart)
18753
+ b.dataset.clipStart = "";
18754
+ if (c.clipEnd)
18755
+ b.dataset.clipEnd = "";
18756
+ b.style.left = pct(c.from);
18757
+ b.style.width = pct(model.from + (c.to - c.from));
18758
+ b.setAttribute("aria-label", `${s.title}, ${when(s)}`);
18759
+ if (s.tags.length)
18760
+ b.append(text(doc, "span", "knobs-hy-tag", s.tags[0]));
18761
+ track.append(b);
18762
+ nodes.set(s.id, b);
18763
+ }
18764
+ for (const e of lane.events) {
18765
+ if (e.to < model.from || e.from > model.to)
18766
+ continue;
18767
+ const b = el(doc, "button", "knobs-hy-ev");
18768
+ b.type = "button";
18769
+ b.tabIndex = -1;
18770
+ b.dataset.id = e.id;
18771
+ b.dataset.tone = e.tone;
18772
+ if (e.type)
18773
+ b.dataset.type = e.type;
18774
+ if (e.inferred)
18775
+ b.dataset.inferred = "";
18776
+ if (quiet.has(e.id))
18777
+ b.dataset.quiet = "";
18778
+ b.style.left = pct(Math.max(e.from, model.from));
18779
+ if (e.to > e.from) {
18780
+ b.dataset.range = "";
18781
+ b.style.setProperty("--hy-w", `${scale.x(model.from + (e.to - e.from)).toFixed(2)}px`);
18782
+ }
18783
+ b.setAttribute("aria-label", `${when(e)}: ${e.title}`);
18784
+ track.append(b);
18785
+ nodes.set(e.id, b);
18786
+ }
18787
+ chart.append(label, track);
18788
+ });
18789
+ over = el(doc, "div", "knobs-hy-layer");
18790
+ over.setAttribute("aria-hidden", "true");
18791
+ if (showToday) {
18792
+ const today = el(doc, "div", "knobs-hy-today");
18793
+ today.style.left = pct(model.today);
18794
+ over.append(today);
18795
+ }
18796
+ selLine = el(doc, "div", "knobs-hy-sel");
18797
+ hoverLine = el(doc, "div", "knobs-hy-hover");
18798
+ over.append(selLine, hoverLine);
18799
+ chart.append(over);
18800
+ const reserved = [];
18801
+ if (now) {
18802
+ const w = now.offsetWidth;
18803
+ const x0 = Math.max(0, Math.min(plotW - w, scale.x(model.today) - w / 2));
18804
+ now.style.left = `${x0}px`;
18805
+ reserved.push({ x0, x1: x0 + w });
18806
+ }
18807
+ const fits = fitLabels(ticks.map((t, i) => ({ x: scale.x(t.day), w: tickEls[i].offsetWidth })), plotW, reserved);
18808
+ tickEls.forEach((n, i) => n.hidden = !fits[i]);
18809
+ for (const n of bandEls)
18810
+ n.toggleAttribute("data-overflow", n.scrollWidth > n.clientWidth + 1);
18811
+ for (const n of chart.querySelectorAll(".knobs-hy-label"))
18812
+ n.toggleAttribute("data-overflow", n.scrollWidth > n.clientWidth + 1);
18813
+ for (const lane of model.lanes) {
18814
+ 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);
18815
+ spans.forEach(({ s, c }, i) => {
18816
+ const b = nodes.get(s.id);
18817
+ const tag = b?.querySelector(".knobs-hy-tag");
18818
+ if (!b || !tag)
18819
+ return;
18820
+ const font = fontOf(tag);
18821
+ const widths = s.tags.map((t) => measure(t, font));
18822
+ const next = spans[i + 1]?.c;
18823
+ const spanW = scale.x(c.to) - scale.x(c.from);
18824
+ const after = (next ? scale.x(next.from) : plotW) - scale.x(c.to);
18825
+ const fit = fitTag(widths, spanW, after);
18826
+ tag.textContent = fit ? s.tags[fit.index] : s.tags[0] ?? "";
18827
+ tag.dataset.place = fit ? fit.place : "none";
18828
+ });
18829
+ }
18830
+ paintSelection();
18831
+ }
18832
+ const h = new Spring(0, HEIGHT2.omega, HEIGHT2.zeta, 0.25);
18833
+ let goal = 0;
18834
+ let content = null;
18835
+ const loop = frameLoop(win, (dt) => {
18836
+ h.step(dt);
18837
+ const moving = !h.resting;
18838
+ detail.style.height = moving ? `${Math.max(0, h.x).toFixed(2)}px` : goal > 0 ? "" : "0px";
18839
+ detail.toggleAttribute("data-moving", moving);
18840
+ return moving;
18841
+ });
18842
+ function buildDetail(it) {
18843
+ const c = el(doc, "div", "knobs-hy-dc");
18844
+ const meta = el(doc, "div", "knobs-hy-dmeta");
18845
+ meta.append(text(doc, "time", "", when(it)));
18846
+ const lane = model.lanes.find((l) => l.id === it.lane);
18847
+ if (lane && (model.lanes.length > 1 || lane.id !== "code"))
18848
+ meta.append(text(doc, "span", "", lane.label));
18849
+ if (it.type)
18850
+ meta.append(text(doc, "span", "", typeName(it.type)));
18851
+ if (it.provenance || it.source) {
18852
+ const p = text(doc, "span", "", [it.provenance, it.source].filter(Boolean).join(" · "));
18853
+ if (it.inferred) {
18854
+ p.dataset.tone = "warn";
18855
+ p.title = "Worked out from behaviour, not seen in the record";
18856
+ }
18857
+ meta.append(p);
18858
+ }
18859
+ c.append(meta, text(doc, "div", "knobs-hy-dtitle", it.title));
18860
+ if (it.detail) {
18861
+ const p = el(doc, "p", "knobs-hy-dtext");
18862
+ p.innerHTML = inlineMarkdown(it.detail);
18863
+ c.append(p);
18864
+ }
18865
+ const foot = el(doc, "div", "knobs-hy-dfoot");
18866
+ for (const e of it.evidence) {
18867
+ const tag = text(doc, e.href ? "a" : "span", "knobs-hy-evidence", e.label);
18868
+ if (tag instanceof HTMLAnchorElement && e.href) {
18869
+ tag.href = e.href;
18870
+ tag.target = "_blank";
18871
+ tag.rel = "noreferrer";
18872
+ }
18873
+ foot.append(tag);
18874
+ }
18875
+ if (it.step && options.onJump) {
18876
+ const go = el(doc, "button", "knobs-hy-jump");
18877
+ go.type = "button";
18878
+ go.append(text(doc, "span", "", "Go to step"));
18879
+ go.insertAdjacentHTML("beforeend", GO);
18880
+ const step = it.step;
18881
+ go.addEventListener("click", () => options.onJump?.(step));
18882
+ foot.append(go);
18883
+ }
18884
+ c.append(foot);
18885
+ return c;
18886
+ }
18887
+ function showDetail(it, animate) {
18888
+ const from = detail.offsetHeight;
18889
+ const old = content;
18890
+ const next = it ? buildDetail(it) : null;
18891
+ if (old) {
18892
+ if (animate) {
18893
+ old.dataset.leave = "";
18894
+ const drop = () => old.remove();
18895
+ old.addEventListener("transitionend", drop, { once: true });
18896
+ win.setTimeout(drop, 400);
18897
+ } else
18898
+ old.remove();
18899
+ }
18900
+ content = next;
18901
+ if (next) {
18902
+ detail.append(next);
18903
+ if (animate) {
18904
+ next.dataset.enter = "";
18905
+ next.offsetHeight;
18906
+ delete next.dataset.enter;
18907
+ }
18908
+ }
18909
+ goal = next ? next.offsetHeight : 0;
18910
+ if (!animate || reducedMotion(win)) {
18911
+ loop.stop();
18912
+ h.snap(goal);
18913
+ detail.style.height = goal > 0 ? "" : "0px";
18914
+ detail.removeAttribute("data-moving");
18915
+ return;
18916
+ }
18917
+ if (h.resting)
18918
+ h.snap(from);
18919
+ h.set(goal);
18920
+ loop.wake();
18921
+ }
18922
+ function paintSelection() {
18923
+ for (const [id, n] of nodes)
18924
+ n.toggleAttribute("data-selected", id === selected);
18925
+ const it = selected ? model.items.get(selected) : undefined;
18926
+ if (selLine) {
18927
+ const on = it?.kind === "event";
18928
+ selLine.toggleAttribute("data-on", on);
18929
+ if (on)
18930
+ selLine.style.left = `${(scale.f(it.from) * 100).toFixed(4)}%`;
18931
+ }
18932
+ }
18933
+ function select(id, animate, notify) {
18934
+ const next = id && model.items.has(id) ? id : null;
18935
+ if (next === selected)
18936
+ return;
18937
+ selected = next;
18938
+ paintSelection();
18939
+ showDetail(next ? model.items.get(next) : null, animate);
18940
+ if (notify)
18941
+ options.onSelect?.(next);
18942
+ }
18943
+ function itemAt(e) {
18944
+ if (!over)
18945
+ return null;
18946
+ const r = over.getBoundingClientRect();
18947
+ if (!r.width)
18948
+ return null;
18949
+ const x = Math.max(0, Math.min(r.width, e.clientX - r.left));
18950
+ const target = e.target instanceof Element ? e.target : null;
18951
+ const hit = target?.closest("[data-id]");
18952
+ if (hit && chart.contains(hit))
18953
+ return { it: model.items.get(hit.dataset.id) ?? null, x };
18954
+ const laneId = target?.closest(".knobs-hy-track")?.dataset.lane;
18955
+ const lane = laneId ? model.lanes.find((l) => l.id === laneId) : undefined;
18956
+ if (lane) {
18957
+ const reach = REACH / r.width * (model.to - model.from);
18958
+ const near = nearestEvent(lane.events, scale.day(x), reach);
18959
+ if (near)
18960
+ return { it: near, x };
18961
+ }
18962
+ return { it: null, x };
18963
+ }
18964
+ function hideHover() {
18965
+ hoverLine?.removeAttribute("data-on");
18966
+ tip.removeAttribute("data-on");
18967
+ hovered?.removeAttribute("data-hover");
18968
+ hovered = null;
18969
+ tipKey = "";
18970
+ }
18971
+ function hover(e) {
18972
+ const at = itemAt(e);
18973
+ if (!at || !over || !hoverLine)
18974
+ return hideHover();
18975
+ const { it, x } = at;
18976
+ const lineX = it?.kind === "event" ? scale.x(it.from) : x;
18977
+ hoverLine.style.left = `${lineX.toFixed(2)}px`;
18978
+ hoverLine.setAttribute("data-on", "");
18979
+ const node = it ? nodes.get(it.id) ?? null : null;
18980
+ if (node !== hovered) {
18981
+ hovered?.removeAttribute("data-hover");
18982
+ node?.setAttribute("data-hover", "");
18983
+ hovered = node;
18984
+ }
18985
+ const key = it ? it.id : `d${Math.round(scale.day(x))}`;
18986
+ if (key !== tipKey) {
18987
+ tipKey = key;
18988
+ tipTitle.textContent = it ? it.title : formatDay(Math.round(scale.day(x)));
18989
+ tipDate.textContent = it ? [when(it), it.type ? typeName(it.type) : ""].filter(Boolean).join(" · ") : "";
18990
+ tipDate.hidden = !it;
18991
+ tipLine.textContent = it ? firstSentence(it.detail) : "";
18992
+ }
18993
+ const rr = root.getBoundingClientRect();
18994
+ const or = over.getBoundingClientRect();
18995
+ const laneId = it?.lane ?? (e.target instanceof Element ? e.target.closest(".knobs-hy-track")?.dataset.lane : undefined);
18996
+ const lr = laneId && tracks.get(laneId)?.getBoundingClientRect() || or;
18997
+ const tw = tip.offsetWidth;
18998
+ const th = tip.offsetHeight;
18999
+ const left = Math.max(0, Math.min(rr.width - tw, or.left - rr.left + lineX - tw / 2));
19000
+ let top = lr.top - rr.top - th - 6;
19001
+ if (top + rr.top < 0)
19002
+ top = lr.bottom - rr.top + 6;
19003
+ tip.style.transform = `translate3d(${Math.round(left)}px,${Math.round(top)}px,0)`;
19004
+ tip.setAttribute("data-on", "");
19005
+ }
19006
+ chart.addEventListener("pointermove", (e) => {
19007
+ if (e.pointerType === "touch")
19008
+ return;
19009
+ hover(e);
19010
+ });
19011
+ chart.addEventListener("pointerleave", hideHover);
19012
+ chart.addEventListener("mousedown", (e) => {
19013
+ if (!(e.target instanceof Element) || !e.target.closest("button"))
19014
+ return;
19015
+ e.preventDefault();
19016
+ root.focus({ preventScroll: true });
19017
+ });
19018
+ chart.addEventListener("click", (e) => {
19019
+ const at = itemAt(e);
19020
+ const id = at?.it?.id ?? null;
19021
+ select(id !== null && id === selected ? null : id, true, true);
19022
+ });
19023
+ root.addEventListener("keydown", (e) => {
19024
+ const onChart = e.target instanceof Node && chart.contains(e.target);
19025
+ if (e.target !== root && !onChart || e.metaKey || e.ctrlKey || e.altKey)
19026
+ return;
19027
+ let next;
19028
+ const order = walked();
19029
+ const at = selected ? model.items.get(selected)?.from : undefined;
19030
+ if (e.key === "ArrowRight")
19031
+ next = stepEvent(order, selected, 1, at);
19032
+ else if (e.key === "ArrowLeft")
19033
+ next = stepEvent(order, selected, -1, at);
19034
+ else if (e.key === "Home")
19035
+ next = order[0] ?? null;
19036
+ else if (e.key === "End")
19037
+ next = order[order.length - 1] ?? null;
19038
+ else if (e.key === "Escape" && selected)
19039
+ next = null;
19040
+ else if (e.key === "Enter" && selected) {
19041
+ const step = model.items.get(selected)?.step;
19042
+ if (!step || !options.onJump)
19043
+ return;
19044
+ options.onJump(step);
19045
+ e.preventDefault();
19046
+ e.stopPropagation();
19047
+ return;
19048
+ } else
19049
+ return;
19050
+ e.preventDefault();
19051
+ e.stopPropagation();
19052
+ hideHover();
19053
+ select(next?.id ?? null, true, true);
19054
+ });
19055
+ let raf = 0;
19056
+ const ro = new ResizeObserver(() => {
19057
+ if (raf || disposed)
19058
+ return;
19059
+ raf = win.requestAnimationFrame(() => {
19060
+ raf = 0;
19061
+ if (!disposed && root.clientWidth !== drawnWidth)
19062
+ draw();
19063
+ });
19064
+ });
19065
+ ro.observe(root);
19066
+ const restyle = () => draw();
19067
+ root.addEventListener("knobs-restyle", restyle);
19068
+ drawFoot();
19069
+ draw();
19070
+ detail.style.height = "0px";
19071
+ if (selected)
19072
+ showDetail(model.items.get(selected), false);
19073
+ const dispose = () => {
19074
+ if (disposed)
19075
+ return;
19076
+ disposed = true;
19077
+ ro.disconnect();
19078
+ if (raf)
19079
+ win.cancelAnimationFrame(raf);
19080
+ loop.stop();
19081
+ root.removeEventListener("knobs-restyle", restyle);
19082
+ root.remove();
19083
+ };
19084
+ return {
19085
+ element: root,
19086
+ get model() {
19087
+ return model;
19088
+ },
19089
+ get selected() {
19090
+ return selected;
19091
+ },
19092
+ select(id) {
19093
+ select(id, true, false);
19094
+ },
19095
+ set(data) {
19096
+ model = normalizeHistory(data);
19097
+ const keep = selected && model.items.has(selected) ? selected : null;
19098
+ selected = keep;
19099
+ quiet = quietEvents(model.events, options.marks ?? DEFAULT_MARKS);
19100
+ drawFoot();
19101
+ draw();
19102
+ showDetail(keep ? model.items.get(keep) : null, false);
19103
+ },
19104
+ focus() {
19105
+ root.focus();
19106
+ },
19107
+ dispose,
19108
+ destroy: dispose
19109
+ };
19110
+ }
19111
+
19112
+ // src/ui/walkthrough-blocks.ts
19113
+ init_icons();
19114
+ init_look();
19115
+ init_selection_pill();
19116
+ var GLYPH = {
19117
+ caret: icon("caretRight", { small: true })
19118
+ };
19119
+ var el2 = (doc, tag, cls) => {
19120
+ const n = doc.createElement(tag);
19121
+ if (cls)
19122
+ n.className = cls;
19123
+ return n;
19124
+ };
19125
+ var text2 = (doc, tag, cls, value) => {
19126
+ const n = el2(doc, tag, cls);
19127
+ n.textContent = value;
19128
+ return n;
19129
+ };
17929
19130
  var prose = (doc, tag, cls, value) => {
17930
- const n = el(doc, tag, cls);
19131
+ const n = el2(doc, tag, cls);
17931
19132
  n.innerHTML = inlineMarkdown(value);
17932
19133
  return n;
17933
19134
  };
17934
19135
  var link = (doc, cls, label, href) => {
17935
19136
  if (!href)
17936
- return text(doc, "span", cls, label);
17937
- const a = text(doc, "a", cls, label);
19137
+ return text2(doc, "span", cls, label);
19138
+ const a = text2(doc, "a", cls, label);
17938
19139
  a.href = href;
17939
19140
  a.target = "_blank";
17940
19141
  a.rel = "noreferrer";
17941
19142
  return a;
17942
19143
  };
17943
- function stepButton(doc, ref, steps) {
19144
+ function rowLink(row, ref, steps) {
17944
19145
  const s = ref && steps?.find(ref);
17945
19146
  if (!s || !steps)
17946
- return null;
17947
- const b = el(doc, "button", "knobs-wt-steplink");
17948
- b.innerHTML = GLYPH.go;
17949
- b.type = "button";
17950
- b.title = `Go to “${s.title}”`;
17951
- b.setAttribute("aria-label", b.title);
17952
- b.addEventListener("click", (e) => {
17953
- e.stopPropagation();
19147
+ return;
19148
+ const doc = row.ownerDocument;
19149
+ row.dataset.go = "";
19150
+ row.tabIndex = 0;
19151
+ row.title = `Go to “${plainMarkdown(s.title)}”`;
19152
+ row.addEventListener("click", (e) => {
19153
+ if (e.target?.closest("a,button"))
19154
+ return;
19155
+ if (doc.getSelection()?.toString())
19156
+ return;
19157
+ steps.go(s.key);
19158
+ });
19159
+ row.addEventListener("keydown", (e) => {
19160
+ if (e.target !== row || e.key !== "Enter" && e.key !== " ")
19161
+ return;
19162
+ e.preventDefault();
17954
19163
  steps.go(s.key);
17955
19164
  });
17956
- return b;
17957
19165
  }
17958
- var tags = (doc, list) => {
17959
- if (!list?.length)
17960
- return null;
17961
- const row = el(doc, "div", "knobs-wt-tags");
17962
- for (const e of list)
17963
- row.append(link(doc, "knobs-wt-tag", e.label, e.href));
17964
- return row;
17965
- };
17966
19166
  function mountHistory(host, m, steps) {
17967
- const doc = host.ownerDocument;
17968
- const root = el(doc, "div", "knobs-wt-history");
17969
- const longest = Math.max(6, ...m.groups.flatMap((g) => g.events.flatMap((e) => [e.date.length, e.age?.length ?? 0])));
17970
- root.style.setProperty("--kw-hw", `${Math.min(16, longest + 1)}ch`);
17971
- for (const g of m.groups) {
17972
- const group = el(doc, "section", "knobs-wt-hgroup");
17973
- if (g.heading || g.summary) {
17974
- const head = el(doc, "header", "knobs-wt-hhead");
17975
- if (g.heading)
17976
- head.append(text(doc, "h4", "knobs-wt-hheading", g.heading));
17977
- if (g.summary)
17978
- head.append(text(doc, "span", "knobs-wt-hsum", g.summary));
17979
- group.append(head);
17980
- }
17981
- const list = el(doc, "ol", "knobs-wt-hlist");
17982
- for (const e of g.events)
17983
- list.append(historyEvent(doc, e, steps));
17984
- group.append(list);
17985
- root.append(group);
17986
- }
17987
- host.append(root);
17988
- return { dispose: () => root.remove(), context: () => ({ kind: "history" }) };
17989
- }
17990
- function historyEvent(doc, e, steps) {
17991
- const li = el(doc, "li", "knobs-wt-hev");
17992
- if (e.tone)
17993
- li.dataset.tone = e.tone;
17994
- if (e.type)
17995
- li.dataset.type = e.type;
17996
- if (e.provenance)
17997
- li.dataset.provenance = e.provenance;
17998
- const when = el(doc, "div", "knobs-wt-hwhen");
17999
- when.append(text(doc, "time", "knobs-wt-hdate", e.date));
18000
- if (e.age)
18001
- when.append(text(doc, "span", "knobs-wt-hage", e.age));
18002
- const spine = el(doc, "span", "knobs-wt-hspine");
18003
- spine.setAttribute("aria-hidden", "true");
18004
- spine.append(el(doc, "span", "knobs-wt-hdot"));
18005
- const main = el(doc, "div", "knobs-wt-hmain");
18006
- const title = el(doc, "div", "knobs-wt-htitle");
18007
- title.append(text(doc, "span", "knobs-wt-htext", e.title));
18008
- if (e.provenance || e.source) {
18009
- const p = text(doc, "span", "knobs-wt-prov", [e.provenance, e.source].filter(Boolean).join(" · "));
18010
- if (e.provenance === "inferred")
18011
- p.title = "Inferred from behaviour, not seen in the record";
18012
- else if (e.provenance === "verified")
18013
- p.title = "Seen in the record";
18014
- title.append(p);
18015
- }
18016
- const go = stepButton(doc, e.step, steps);
18017
- if (go)
18018
- title.append(go);
18019
- main.append(title);
18020
- if (e.detail)
18021
- main.append(prose(doc, "p", "knobs-wt-hdetail", e.detail));
18022
- const t = tags(doc, e.evidence);
18023
- if (t)
18024
- main.append(t);
18025
- li.append(when, spine, main);
18026
- return li;
18027
- }
18028
- function mountStack(host, m, steps) {
19167
+ const h = history(host, {
19168
+ data: m.data,
19169
+ ...m.label ? { label: m.label } : {},
19170
+ ...m.marks ? { marks: m.marks } : {},
19171
+ ...steps ? {
19172
+ onJump: (ref) => {
19173
+ const s = steps.find(ref);
19174
+ if (s)
19175
+ steps.go(s.key);
19176
+ }
19177
+ } : {}
19178
+ });
19179
+ return {
19180
+ dispose: () => h.dispose(),
19181
+ context: () => ({ kind: "history" })
19182
+ };
19183
+ }
19184
+ function mountStack(host, m, steps, findPR) {
18029
19185
  const doc = host.ownerDocument;
18030
19186
  const layout = stackLayout(m);
18031
- const root = el(doc, "div", "knobs-wt-stack");
19187
+ const root = el2(doc, "div", "knobs-wt-stack");
18032
19188
  root.toggleAttribute("data-flat", layout.flat);
18033
19189
  const lane = (kind) => {
18034
- const s = el(doc, "span", "knobs-wt-lane");
19190
+ const s = el2(doc, "span", "knobs-wt-lane");
18035
19191
  s.dataset.rail = kind;
18036
19192
  return s;
18037
19193
  };
@@ -18042,19 +19198,19 @@ function mountStack(host, m, steps) {
18042
19198
  };
18043
19199
  const from = layout.trunk ? 0 : 1;
18044
19200
  if (layout.trunk && layout.rows.length) {
18045
- const row = el(doc, "div", "knobs-wt-srow");
19201
+ const row = el2(doc, "div", "knobs-wt-srow");
18046
19202
  row.dataset.trunk = "";
18047
- const lanes = el(doc, "span", "knobs-wt-lanes");
19203
+ const lanes = el2(doc, "span", "knobs-wt-lanes");
18048
19204
  lanes.setAttribute("aria-hidden", "true");
18049
19205
  lanes.append(node(true));
18050
- row.append(lanes, text(doc, "span", "knobs-wt-trunk", layout.trunk));
19206
+ row.append(lanes, text2(doc, "span", "knobs-wt-trunk", layout.trunk));
18051
19207
  root.append(row);
18052
19208
  }
18053
19209
  for (const r of layout.rows) {
18054
19210
  const it = r.item;
18055
- const row = el(doc, "div", "knobs-wt-srow");
19211
+ const row = el2(doc, "div", "knobs-wt-srow");
18056
19212
  if (!layout.flat) {
18057
- const lanes = el(doc, "span", "knobs-wt-lanes");
19213
+ const lanes = el2(doc, "span", "knobs-wt-lanes");
18058
19214
  lanes.setAttribute("aria-hidden", "true");
18059
19215
  for (let k = from;k < r.depth; k++)
18060
19216
  lanes.append(lane(r.rails[k] ? "pass" : "none"));
@@ -18063,24 +19219,24 @@ function mountStack(host, m, steps) {
18063
19219
  lanes.append(node(r.children > 0));
18064
19220
  row.append(lanes);
18065
19221
  }
18066
- const main = el(doc, "div", "knobs-wt-smain");
18067
- const line = el(doc, "div", "knobs-wt-sline");
19222
+ const main = el2(doc, "div", "knobs-wt-smain");
19223
+ const line = el2(doc, "div", "knobs-wt-sline");
18068
19224
  if (it.order)
18069
- line.append(text(doc, "span", "knobs-wt-sord", it.order));
18070
- line.append(link(doc, "knobs-wt-spr", it.pr, it.href));
19225
+ line.append(text2(doc, "span", "knobs-wt-sord", it.order));
19226
+ const known = findPR?.(it.pr.replace(/^#/, "")) ?? null;
19227
+ const pr = link(doc, "knobs-wt-spr", it.pr, it.href ?? known?.href);
19228
+ pr.addEventListener("click", (e) => e.stopPropagation());
19229
+ line.append(pr);
18071
19230
  const target = it.step ? steps?.find(it.step) : null;
18072
19231
  if (target && steps) {
18073
- const title = text(doc, "button", "knobs-wt-stitle", it.title);
19232
+ const title = text2(doc, "button", "knobs-wt-stitle", it.title);
18074
19233
  title.type = "button";
18075
- title.title = `Go to “${target.title}”`;
19234
+ title.title = `Go to “${plainMarkdown(target.title)}”`;
18076
19235
  title.addEventListener("click", (e) => {
18077
19236
  e.stopPropagation();
18078
19237
  steps.go(target.key);
18079
19238
  });
18080
19239
  line.append(title);
18081
- const go = stepButton(doc, it.step, steps);
18082
- if (go)
18083
- line.append(go);
18084
19240
  row.dataset.go = "";
18085
19241
  row.addEventListener("click", (e) => {
18086
19242
  const t = e.target;
@@ -18091,22 +19247,26 @@ function mountStack(host, m, steps) {
18091
19247
  steps.go(target.key);
18092
19248
  });
18093
19249
  } else
18094
- line.append(text(doc, "span", "knobs-wt-stitle", it.title));
19250
+ line.append(text2(doc, "span", "knobs-wt-stitle", it.title));
18095
19251
  main.append(line);
18096
19252
  if (it.summary)
18097
19253
  main.append(prose(doc, "p", "knobs-wt-ssummary", it.summary));
18098
- const meta = el(doc, "div", "knobs-wt-smeta");
19254
+ const meta = el2(doc, "div", "knobs-wt-smeta");
18099
19255
  if (it.head) {
18100
19256
  const branch = r.offBase ? `${it.head} → ${r.offBase}` : it.head;
18101
- meta.append(text(doc, "span", "knobs-wt-sbranch", branch));
19257
+ meta.append(text2(doc, "span", "knobs-wt-sbranch", branch));
18102
19258
  }
18103
19259
  if (it.delta !== undefined)
18104
19260
  meta.append(delta(doc, it.delta));
18105
19261
  if (meta.childNodes.length)
18106
19262
  main.append(meta);
18107
19263
  row.append(main);
18108
- if (it.status) {
18109
- const chip = text(doc, "span", "knobs-wt-chip knobs-wt-sstatus", it.status);
19264
+ const said = prStatus(it.status);
19265
+ const state = known?.state ?? said.state;
19266
+ if (state)
19267
+ row.append(prStateLabel(doc, state, said.checks, it.status));
19268
+ else if (it.status) {
19269
+ const chip = text2(doc, "span", "knobs-wt-chip knobs-wt-sstatus", it.status);
18110
19270
  chip.dataset.tone = it.tone ?? "dim";
18111
19271
  row.append(chip);
18112
19272
  }
@@ -18116,40 +19276,38 @@ function mountStack(host, m, steps) {
18116
19276
  return { dispose: () => root.remove(), context: () => ({ kind: "stack" }) };
18117
19277
  }
18118
19278
  function delta(doc, d) {
18119
- const s = el(doc, "span", "knobs-wt-delta");
19279
+ const s = el2(doc, "span", "knobs-wt-delta");
18120
19280
  if (typeof d === "string") {
18121
19281
  s.textContent = d;
18122
19282
  return s;
18123
19283
  }
18124
19284
  const n = (v) => v.toLocaleString("en-US");
18125
- s.append(text(doc, "span", "knobs-wt-add", `+${n(d.add)}`));
18126
- s.append(text(doc, "span", "knobs-wt-del", `−${n(d.del)}`));
19285
+ s.append(text2(doc, "span", "knobs-wt-add", `+${n(d.add)}`));
19286
+ s.append(text2(doc, "span", "knobs-wt-del", `−${n(d.del)}`));
18127
19287
  s.setAttribute("aria-label", `${d.add} added, ${d.del} removed`);
18128
19288
  return s;
18129
19289
  }
18130
- function cellContent(doc, into, cell, steps) {
19290
+ function cellContent(doc, into, cell) {
18131
19291
  let content;
18132
19292
  if (cell.badge) {
18133
19293
  content = link(doc, "knobs-wt-chip", cell.text, cell.href);
18134
19294
  content.dataset.tone = cell.badge;
18135
19295
  } else {
18136
- const inner = cell.code ? text(doc, "code", "", cell.text) : null;
19296
+ const inner = cell.code ? text2(doc, "code", "", cell.text) : null;
18137
19297
  if (cell.href) {
18138
19298
  content = link(doc, "knobs-wt-clink", inner ? "" : cell.text, cell.href);
18139
19299
  if (inner)
18140
19300
  content.append(inner);
18141
19301
  } else
18142
- content = inner ?? text(doc, "span", "", cell.text);
19302
+ content = inner ?? text2(doc, "span", "", cell.text);
18143
19303
  if (cell.tone) {
18144
19304
  content.classList.add("knobs-wt-ctone");
18145
19305
  content.dataset.tone = cell.tone;
18146
19306
  }
18147
19307
  }
18148
19308
  into.append(content);
18149
- const go = stepButton(doc, cell.step, steps);
18150
- if (go)
18151
- into.append(go);
18152
19309
  }
19310
+ var rowStep = (r, columns) => columns.map((c) => r[c.key]?.step).find((step) => step !== undefined);
18153
19311
  var cellAttrs = (td, c) => {
18154
19312
  td.dataset.align = c.align;
18155
19313
  if (c.mono)
@@ -18158,83 +19316,85 @@ var cellAttrs = (td, c) => {
18158
19316
  function mountTable(host, m, steps) {
18159
19317
  const doc = host.ownerDocument;
18160
19318
  const win = doc.defaultView;
18161
- const box = el(doc, "div", "knobs-wt-tablebox");
18162
- const scroller = el(doc, "div", "knobs-wt-table");
18163
- const table = el(doc, "table");
19319
+ const box = el2(doc, "div", "knobs-wt-tablebox");
19320
+ const scroller = el2(doc, "div", "knobs-wt-table");
19321
+ const table = el2(doc, "table");
18164
19322
  if (m.caption)
18165
- table.append(text(doc, "caption", "", m.caption));
18166
- const head = el(doc, "thead");
18167
- const hr = el(doc, "tr");
19323
+ table.append(text2(doc, "caption", "", m.caption));
19324
+ const head = el2(doc, "thead");
19325
+ const hr = el2(doc, "tr");
18168
19326
  for (const c of m.columns) {
18169
- const th = text(doc, "th", "", c.label);
19327
+ const th = text2(doc, "th", "", c.label);
18170
19328
  th.scope = "col";
18171
19329
  th.dataset.align = c.align;
18172
19330
  hr.append(th);
18173
19331
  }
18174
19332
  head.append(hr);
18175
- const body = el(doc, "tbody");
19333
+ const body = el2(doc, "tbody");
18176
19334
  for (const r of m.rows) {
18177
- const tr = el(doc, "tr");
19335
+ const tr = el2(doc, "tr");
18178
19336
  for (const c of m.columns) {
18179
- const td = el(doc, "td");
19337
+ const td = el2(doc, "td");
18180
19338
  cellAttrs(td, c);
18181
19339
  const cell = r[c.key];
18182
19340
  if (cell) {
18183
19341
  if (cell.text.length > 40 && !c.mono && !cell.code)
18184
19342
  td.dataset.long = "";
18185
- cellContent(doc, td, cell, steps);
19343
+ cellContent(doc, td, cell);
18186
19344
  }
18187
19345
  tr.append(td);
18188
19346
  }
19347
+ rowLink(tr, rowStep(r, m.columns), steps);
18189
19348
  body.append(tr);
18190
19349
  }
18191
19350
  table.append(head, body);
18192
19351
  scroller.append(table);
18193
- const stack = el(doc, "div", "knobs-wt-tstack");
19352
+ const stack = el2(doc, "div", "knobs-wt-tstack");
18194
19353
  stack.setAttribute("role", "list");
18195
19354
  if (m.caption)
18196
19355
  stack.setAttribute("aria-label", m.caption);
18197
19356
  const primary = m.columns.find((c) => c.key === m.primary) ?? m.columns[0];
18198
19357
  for (const r of m.rows) {
18199
- const item = el(doc, "div", "knobs-wt-trow");
19358
+ const item = el2(doc, "div", "knobs-wt-trow");
18200
19359
  item.setAttribute("role", "listitem");
18201
- const title = el(doc, "div", "knobs-wt-ttitle");
19360
+ const title = el2(doc, "div", "knobs-wt-ttitle");
18202
19361
  if (primary) {
18203
19362
  cellAttrs(title, primary);
18204
19363
  const cell = r[primary.key];
18205
19364
  if (cell)
18206
- cellContent(doc, title, cell, steps);
19365
+ cellContent(doc, title, cell);
18207
19366
  }
18208
- const line = el(doc, "div", "knobs-wt-tline");
19367
+ const line = el2(doc, "div", "knobs-wt-tline");
18209
19368
  line.append(title);
18210
- const badges = el(doc, "div", "knobs-wt-tbadges");
19369
+ const badges = el2(doc, "div", "knobs-wt-tbadges");
18211
19370
  for (const c of m.columns) {
18212
19371
  const cell = r[c.key];
18213
19372
  if (c !== primary && cell?.badge)
18214
- cellContent(doc, badges, cell, steps);
19373
+ cellContent(doc, badges, cell);
18215
19374
  }
18216
19375
  if (badges.childNodes.length)
18217
19376
  line.append(badges);
18218
19377
  item.append(line);
18219
- const terms = el(doc, "dl", "knobs-wt-tterms");
19378
+ const terms = el2(doc, "dl", "knobs-wt-tterms");
18220
19379
  for (const c of m.columns) {
18221
19380
  if (c === primary)
18222
19381
  continue;
18223
19382
  const cell = r[c.key];
18224
19383
  if (!cell || cell.text === "" || cell.badge)
18225
19384
  continue;
18226
- const dd = el(doc, "dd");
19385
+ const dd = el2(doc, "dd");
18227
19386
  cellAttrs(dd, c);
18228
19387
  dd.removeAttribute("data-align");
18229
- cellContent(doc, dd, cell, steps);
18230
- terms.append(text(doc, "dt", "", c.label), dd);
19388
+ cellContent(doc, dd, cell);
19389
+ terms.append(text2(doc, "dt", "", c.label), dd);
18231
19390
  }
18232
19391
  if (terms.childNodes.length)
18233
19392
  item.append(terms);
19393
+ rowLink(item, rowStep(r, m.columns), steps);
18234
19394
  stack.append(item);
18235
19395
  }
18236
19396
  if (m.caption)
18237
- stack.append(text(doc, "p", "knobs-wt-tcaption", m.caption));
19397
+ stack.append(text2(doc, "p", "knobs-wt-tcaption", m.caption));
18238
19398
  box.append(scroller, stack);
18239
19399
  host.append(box);
18240
19400
  let natural = 0;
@@ -18288,16 +19448,16 @@ function mountTable(host, m, steps) {
18288
19448
  }
18289
19449
  function chapterPills(host, chapters, io) {
18290
19450
  const doc = host.ownerDocument;
18291
- const list = el(doc, "div", "knobs-wt-chapters");
19451
+ const list = el2(doc, "div", "knobs-wt-chapters");
18292
19452
  list.setAttribute("role", "group");
18293
19453
  list.setAttribute("aria-label", "Chapters");
18294
19454
  let at = -2;
18295
19455
  const rows = chapters.map((c, i) => {
18296
- const b = el(doc, "button", "knobs-wt-chapter");
19456
+ const b = el2(doc, "button", "knobs-wt-chapter");
18297
19457
  b.type = "button";
18298
- b.append(text(doc, "span", "knobs-wt-chapter-n", String(i + 1)));
18299
- b.append(text(doc, "span", "knobs-wt-chapter-label", c.label));
18300
- b.append(text(doc, "span", "knobs-wt-chapter-time", clock(c.start)));
19458
+ b.append(text2(doc, "span", "knobs-wt-chapter-n", String(i + 1)));
19459
+ b.append(text2(doc, "span", "knobs-wt-chapter-label", c.label));
19460
+ b.append(text2(doc, "span", "knobs-wt-chapter-time", clock(c.start)));
18301
19461
  b.addEventListener("click", () => {
18302
19462
  io.onEvent?.({ what: "seek", time: Math.round(c.start * 100) / 100, detail: c.label });
18303
19463
  io.seek(c.start);
@@ -18335,15 +19495,13 @@ function disclosure(host, options) {
18335
19495
  const doc = host.ownerDocument;
18336
19496
  const win = doc.defaultView;
18337
19497
  let open = options.open === true;
18338
- const root = el(doc, "div", "knobs-wt-disc");
18339
- const toggle = el(doc, "button", "knobs-wt-disc-toggle");
19498
+ const root = el2(doc, "div", "knobs-wt-disc");
19499
+ const toggle = el2(doc, "button", "knobs-wt-disc-toggle");
18340
19500
  toggle.type = "button";
18341
19501
  toggle.innerHTML = GLYPH.caret;
18342
- toggle.append(text(doc, "span", "", options.label));
18343
- if (options.count !== undefined)
18344
- toggle.append(text(doc, "span", "knobs-wt-disc-count", String(options.count)));
18345
- const viewport = el(doc, "div", "knobs-wt-disc-body");
18346
- const content = el(doc, "div", "knobs-wt-disc-content");
19502
+ toggle.append(text2(doc, "span", "", options.label));
19503
+ const viewport = el2(doc, "div", "knobs-wt-disc-body");
19504
+ const content = el2(doc, "div", "knobs-wt-disc-content");
18347
19505
  content.append(options.content);
18348
19506
  viewport.append(content);
18349
19507
  root.append(toggle, viewport);
@@ -18355,7 +19513,9 @@ function disclosure(host, options) {
18355
19513
  const k = full > 0 ? Math.max(0, Math.min(1, h.x / full)) : open ? 1 : 0;
18356
19514
  viewport.style.height = !moving && open ? "" : `${Math.max(0, h.x).toFixed(2)}px`;
18357
19515
  viewport.toggleAttribute("data-moving", moving);
18358
- content.style.opacity = (open ? Math.min(1, k * 1.25) : Math.max(0, k * 1.5 - 0.5)).toFixed(3);
19516
+ const o = open ? Math.min(1, k * 1.25) : Math.max(0, k * 1.5 - 0.5);
19517
+ content.style.opacity = o.toFixed(3);
19518
+ content.style.filter = o >= 1 ? "" : `blur(${((1 - o) * 3).toFixed(2)}px)`;
18359
19519
  };
18360
19520
  const loop = frameLoop(win, (dt) => {
18361
19521
  h.step(dt);
@@ -18391,6 +19551,7 @@ function disclosure(host, options) {
18391
19551
  h.snap(open ? full : 0);
18392
19552
  viewport.style.height = open ? "" : "0px";
18393
19553
  content.style.opacity = open ? "1" : "0";
19554
+ content.style.filter = "";
18394
19555
  return;
18395
19556
  }
18396
19557
  if (h.resting)
@@ -18411,10 +19572,36 @@ var PR_PATHS = {
18411
19572
  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",
18412
19573
  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"
18413
19574
  };
18414
- 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>`;
19575
+ var DRAFT = "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,192ZM232,192a32,32,0,1,1-32-32A32,32,0,0,1,232,192ZM216,192a16,16,0,1,0-16,16A16,16,0,0,0,216,192ZM212,120a12,12,0,1,1-12-12A12,12,0,0,1,212,120ZM212,60a12,12,0,1,1-12-12A12,12,0,0,1,212,60Z";
19576
+ var X = "M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z";
19577
+ var CHECK3 = "M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z";
19578
+ var svg12 = (d, evenodd = false) => `<svg class="knobs-icon" viewBox="0 0 256 256" width="12" height="12" fill="currentColor"${evenodd ? ' fill-rule="evenodd"' : ""} aria-hidden="true" focusable="false"><path d="${d}"/></svg>`;
19579
+ var prGlyph = (state) => state === "merged" ? svg12(PR_PATHS.merge) : state === "draft" ? svg12(DRAFT, true) : state === "closed" ? svg12(X) : svg12(PR_PATHS.pull);
18415
19580
  var PR_STATE = { merged: "Merged", open: "Open", draft: "Draft", closed: "Closed" };
19581
+ function prStatus(status) {
19582
+ const words = (status ?? "").trim().toLowerCase();
19583
+ const first = /^[a-z]+/.exec(words)?.[0] ?? "";
19584
+ const state = first in PR_STATE ? first : null;
19585
+ return { state, checks: /\b(green|passing|passed|ci|checks? pass)/.test(words) };
19586
+ }
19587
+ function prStateLabel(doc, state, checks, title) {
19588
+ const s = el2(doc, "span", "knobs-wt-prstate");
19589
+ s.dataset.state = state;
19590
+ s.innerHTML = prGlyph(state);
19591
+ s.append(text2(doc, "span", "", PR_STATE[state]));
19592
+ if (checks) {
19593
+ const c = el2(doc, "span", "knobs-wt-prchecks");
19594
+ c.innerHTML = svg12(CHECK3);
19595
+ c.title = "Checks pass";
19596
+ c.setAttribute("aria-label", "checks pass");
19597
+ s.append(c);
19598
+ }
19599
+ if (title)
19600
+ s.title = title;
19601
+ return s;
19602
+ }
18416
19603
  function prChip(doc, n, pr) {
18417
- const chip = el(doc, pr.href ? "a" : "span", "knobs-wt-pr");
19604
+ const chip = el2(doc, pr.href ? "a" : "span", "knobs-wt-pr");
18418
19605
  if (pr.href) {
18419
19606
  chip.setAttribute("href", pr.href);
18420
19607
  chip.setAttribute("target", "_blank");
@@ -18422,8 +19609,8 @@ function prChip(doc, n, pr) {
18422
19609
  }
18423
19610
  if (pr.state)
18424
19611
  chip.dataset.state = pr.state;
18425
- chip.innerHTML = prGlyph(pr.state === "merged");
18426
- chip.append(text(doc, "span", "", `#${n}`));
19612
+ chip.innerHTML = prGlyph(pr.state);
19613
+ chip.append(text2(doc, "span", "", `#${n}`));
18427
19614
  const tip = [`#${n}`, pr.title, pr.state ? PR_STATE[pr.state] : ""].filter(Boolean).join(" · ");
18428
19615
  chip.title = tip;
18429
19616
  chip.setAttribute("aria-label", `Pull request ${tip}`);
@@ -18479,6 +19666,7 @@ function linkPRs(root, find) {
18479
19666
  // src/ui/walkthrough-media.ts
18480
19667
  init_compare_model();
18481
19668
  init_glide();
19669
+ init_motion();
18482
19670
  init_icons();
18483
19671
  init_look();
18484
19672
 
@@ -18491,8 +19679,9 @@ var quick = springCss(FADE);
18491
19679
  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)";
18492
19680
  var walkthroughCss = `
18493
19681
  :where(.knobs-walkthrough,.knobs-walkthrough *,.knobs-wt-slot,.knobs-wt-slot *){box-sizing:border-box}
19682
+ .knobs-ui:is(.knobs-walkthrough,.knobs-wt-slot){--k-glass:0;--k-glow:0;--kw-quick:${quick.easing};--kw-quick-ms:${quick.duration}ms}
18494
19683
  .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;
18495
- --kw-measure:var(--k-walkthrough-measure,68ch);--kw-width:var(--k-walkthrough-width,960px);--kw-rail-w:220px;--kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));
19684
+ --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));
18496
19685
  --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);
18497
19686
  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;
18498
19687
  letter-spacing:normal;-webkit-font-smoothing:antialiased}
@@ -18508,7 +19697,7 @@ var walkthroughCss = `
18508
19697
  /* ——— layout: the contents rail sticky at the left edge (right if asked), and the column centred
18509
19698
  in the room beside it, up to --kw-width: media take the column, prose stops at the measure.
18510
19699
  One column where it's narrow ——— */
18511
- .knobs-wt-layout{display:grid;grid-template-columns:var(--kw-rail-w) minmax(0,1fr);gap:var(--_k-space-column);align-items:start}
19700
+ .knobs-wt-layout{display:grid;grid-template-columns:var(--kw-rail-w) minmax(0,1fr);gap:var(--kw-rail-gap);align-items:start}
18512
19701
  .knobs-walkthrough[data-rail="right"] .knobs-wt-layout{grid-template-columns:minmax(0,1fr) var(--kw-rail-w)}
18513
19702
  .knobs-walkthrough[data-rail="right"] .knobs-wt-main{grid-column:1;grid-row:1}
18514
19703
  .knobs-walkthrough[data-rail="right"] .knobs-wt-rail{grid-column:2;grid-row:1}
@@ -18586,7 +19775,6 @@ var walkthroughCss = `
18586
19775
  .knobs-wt-feed{display:flex;flex-direction:column;gap:var(--_k-space-title);min-width:0;scroll-margin-top:16px}
18587
19776
  .knobs-wt-feed-head{display:flex;align-items:baseline;gap:var(--_k-space-sibling)}
18588
19777
  .knobs-wt-feed-title{font-weight:var(--kw-strong)!important}
18589
- .knobs-wt-feed-count{font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3)}
18590
19778
  .knobs-wt-feed-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0}
18591
19779
  .knobs-wt-fi{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0}
18592
19780
  .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)}
@@ -18597,10 +19785,6 @@ var walkthroughCss = `
18597
19785
  :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;
18598
19786
  transition:color calc(var(--_k-hover-out) * 1ms)}
18599
19787
  .knobs-ui .knobs-wt-fi-step:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18600
- :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;
18601
- 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)}
18602
- .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)}
18603
- .knobs-wt-steplink svg{width:12px;height:12px}
18604
19788
 
18605
19789
  /* ——— a disclosure: a small faint toggle with a caret, no fill. The body's height springs (in
18606
19790
  script) and its content fades in place, top-anchored, under a soft bottom mask ——— */
@@ -18611,7 +19795,6 @@ var walkthroughCss = `
18611
19795
  .knobs-ui .knobs-wt-disc-toggle:focus-visible{box-shadow:0 0 0 2px var(--_k-focus)}
18612
19796
  .knobs-wt-disc-toggle svg{flex:none;width:12px;height:12px;transition:rotate var(--kw-state-ms) var(--kw-state)}
18613
19797
  .knobs-wt-disc[data-open]>.knobs-wt-disc-toggle svg{rotate:90deg}
18614
- .knobs-wt-disc-count{color:var(--_k-fg-3)}
18615
19798
  .knobs-wt-disc-body{overflow:hidden}
18616
19799
  .knobs-wt-disc[data-open]>.knobs-wt-disc-body:not([data-moving]){overflow:visible}
18617
19800
  .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)}
@@ -18791,6 +19974,10 @@ var walkthroughCss = `
18791
19974
  /* A slot's label: sentence case as written, small and faint, never a heading. */
18792
19975
  .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}}
18793
19976
  .knobs-wt-body{min-width:0}
19977
+ /* Media mounting after the first paint, and a picture still decoding, come into focus: a fade with
19978
+ a small blur on the quick spring, from the top; at rest, nothing. */
19979
+ .knobs-wt-body,.knobs-wt-frame>img{transition:opacity var(--kw-quick-ms) var(--kw-quick),filter var(--kw-quick-ms) var(--kw-quick)}
19980
+ .knobs-wt-body[data-entering],.knobs-wt-frame[data-loading]>img{opacity:0;filter:blur(3px);transition:none}
18794
19981
  /* One frame for every slot that has one (a picture, a video, a code block, a compare's stage, a
18795
19982
  slot still loading): the panel radius and a quiet fill, no ring; concentric on a card. */
18796
19983
  .knobs-wt-slot .knobs-cmp-stage{border-radius:var(--kw-frame-r)}
@@ -18805,9 +19992,12 @@ var walkthroughCss = `
18805
19992
  .knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
18806
19993
  .knobs-wt-frame[data-natural]{aspect-ratio:auto;background:transparent}
18807
19994
  .knobs-wt-frame[data-natural]>img{position:static;height:auto}
18808
- .knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:var(--kw-frame-r);background:var(--_k-fill);color:var(--_k-fg);
19995
+ /* Diff, code and terminal: one plain container (no glass): a flat inset fill, a hairline ring, the
19996
+ control radius and room inside, so a diff's buttons and the code under them can breathe. */
19997
+ .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)}
19998
+ .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);
18809
19999
  font:var(--_k-type-label)/var(--_k-leading-body) var(--_k-mono);white-space:pre;tab-size:2;scrollbar-width:thin}
18810
- .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)}
20000
+ .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)}
18811
20001
  .knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
18812
20002
  .knobs-wt-prompt{color:var(--_k-fg-3);user-select:none;-webkit-user-select:none}
18813
20003
  .knobs-wt-cmd{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
@@ -18834,48 +20024,37 @@ var walkthroughCss = `
18834
20024
  .knobs-wt-hold{border-radius:var(--kw-frame-r);background:var(--_k-fill)}
18835
20025
  .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)}
18836
20026
 
20027
+ /* Annotations: a calm ring round a box, a dot or an arrow at a point, a small label beside it;
20028
+ each fades in and out with a small blur on the quick spring as its time comes and goes, and a
20029
+ page's switch (the eye, or a) hides them all. Placed in px by script, 1:1 with the picture;
20030
+ last in their box, so they sit over the picture (and under a compare's grip and labels). */
20031
+ .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)}
20032
+ .knobs-wt-anno:not([data-settled]),.knobs-wt-anno:not([data-settled]) *{transition:none!important}
20033
+ [data-annotations="off"] .knobs-wt-anno,.knobs-wt-anno[data-moving]{opacity:0;filter:blur(3px)}
20034
+ .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)}
20035
+ .knobs-wt-mark[data-hidden]{opacity:0;filter:blur(3px)}
20036
+ .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)}
20037
+ .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)}
20038
+ .knobs-wt-anno-arrow{position:absolute;right:0;bottom:0;filter:drop-shadow(0 0 2px var(--_k-bg))}
20039
+ .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);
20040
+ 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}}
20041
+ .knobs-wt-mark:is([data-shape="dot"],[data-shape="arrow"]) .knobs-wt-mark-label{left:12px;bottom:auto;top:10px}
20042
+ .knobs-wt-mark[data-shape="arrow"] .knobs-wt-mark-label{left:auto;right:30px;top:auto;bottom:30px}
20043
+ .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;
20044
+ 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;
20045
+ transition:opacity calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
20046
+ .knobs-ui .knobs-wt-anno-toggle:is(:hover,:focus-visible){opacity:1;color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
20047
+ .knobs-ui .knobs-wt-anno-toggle:focus-visible{box-shadow:inset 0 0 0 1px var(--_k-focus)}
20048
+ .knobs-cmp-stage>.knobs-wt-anno-toggle{top:auto;bottom:8px}
20049
+
18837
20050
  /* ——— data blocks: history, stack, table, chapters. On the step's card, so no card of their own:
18838
20051
  hairlines and fills only. Tones read the status tokens; hover fades are the panel's own. ——— */
18839
- :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)}
20052
+ :where(.knobs-wt-stack,.knobs-wt-tablebox,.knobs-wt-chapters){--kw-line:var(--_k-leading-body);--kw-rail:var(--_k-border-strong)}
18840
20053
  :where(.knobs-wt-slot) [data-tone="good"]{--kw-tone:var(--kw-good)}
18841
20054
  :where(.knobs-wt-slot) [data-tone="warn"]{--kw-tone:var(--_k-warn)}
18842
20055
  :where(.knobs-wt-slot) [data-tone="bad"]{--kw-tone:var(--_k-danger)}
18843
20056
  :where(.knobs-wt-slot) [data-tone="info"]{--kw-tone:var(--kw-info)}
18844
20057
  :where(.knobs-wt-slot) [data-tone="dim"]{--kw-tone:var(--_k-fg-3)}
18845
- .knobs-wt-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
18846
- .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);
18847
- text-decoration:none;white-space:nowrap;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
18848
- .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)}
18849
-
18850
- /* A history: dates down the left, a spine of tone dots joined by a hairline (broken around each
18851
- dot), the event beside it. Inferred events take a ring for a dot and a dashed tag. */
18852
- .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)}
18853
- .knobs-wt-hgroup{display:flex;flex-direction:column;gap:6px;min-width:0}
18854
- .knobs-wt-hhead{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px}
18855
- .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)}
18856
- .knobs-wt-hsum{color:var(--_k-fg-3);font-size:var(--_k-type-small);font-variant-numeric:tabular-nums}
18857
- .knobs-wt-hlist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
18858
- .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}
18859
- .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)}
18860
- .knobs-wt-hage{color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18861
- .knobs-wt-hspine{grid-area:spine;position:relative;align-self:stretch;margin-block:-5px}
18862
- .knobs-wt-hspine::before,.knobs-wt-hspine::after{content:"";position:absolute;left:6.5px;width:1px;background:var(--kw-rail)}
18863
- .knobs-wt-hspine::before{top:0;height:calc(var(--kw-hy) - 6px)}
18864
- .knobs-wt-hspine::after{top:calc(var(--kw-hy) + 6px);bottom:0}
18865
- .knobs-wt-hev:first-child .knobs-wt-hspine::before,.knobs-wt-hev:last-child .knobs-wt-hspine::after{content:none}
18866
- .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))}
18867
- .knobs-wt-hev[data-type="release"] .knobs-wt-hdot{border-radius:1.5px;rotate:45deg;scale:.9}
18868
- .knobs-wt-hev[data-type="audit"] .knobs-wt-hdot{border-radius:1.5px}
18869
- .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))}
18870
- .knobs-wt-hmain{grid-area:main;display:flex;flex-direction:column;gap:2px;min-width:0}
18871
- .knobs-wt-htitle{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 8px;font-weight:var(--_k-weight-label);color:var(--_k-fg)}
18872
- .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}
18873
- .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}
18874
- .knobs-ui .knobs-wt-hdetail{margin:0;max-width:var(--k-walkthrough-measure,68ch);color:var(--_k-fg-2)}
18875
- @container knobs-wt-slot (max-width:520px){
18876
- .knobs-wt-hev{grid-template-columns:14px minmax(0,1fr);grid-template-areas:"spine when" "spine main"}
18877
- .knobs-wt-hwhen{flex-direction:row;flex-wrap:wrap;align-items:baseline;column-gap:8px}
18878
- }
18879
20058
 
18880
20059
  /* A stack: the trunk at the top, each PR hanging off the branch it merges into, rails drawn as a
18881
20060
  git graph (solid, so where two meet they don't darken). A PR with a step goes there on a click. */
@@ -18907,12 +20086,21 @@ var walkthroughCss = `
18907
20086
  .knobs-wt-add{color:var(--_k-diff-add)}
18908
20087
  .knobs-wt-del{color:var(--_k-diff-del)}
18909
20088
  .knobs-wt-sstatus{margin-top:calc((var(--kw-line) - 20px) / 2)}
20089
+ /* A PR's state the GitHub way: its glyph and word, open and merged quietly tinted (as PR chips),
20090
+ draft and closed faint; no fill, no dot. A check after it where the checks pass, faint too. One
20091
+ width for every row, so the glyphs and words line up. */
20092
+ .knobs-ui .knobs-wt-prstate{--kw-pr:var(--_k-fg-3);display:inline-flex;align-items:center;gap:4px;min-width:calc(7ch + 32px);height:var(--kw-line);color:var(--kw-pr);
20093
+ font:var(--_k-weight) var(--_k-type-small)/var(--_k-leading-small) var(--_k-font);white-space:nowrap}
20094
+ .knobs-ui .knobs-wt-prstate[data-state="open"]{--kw-pr:color-mix(in oklch,var(--_k-ok) 60%,var(--_k-fg-2))}
20095
+ .knobs-ui .knobs-wt-prstate[data-state="merged"]{--kw-pr:color-mix(in oklch,var(--_k-series-5) 60%,var(--_k-fg-2))}
20096
+ .knobs-wt-prstate svg{flex:none}
20097
+ .knobs-ui .knobs-wt-prchecks{display:inline-flex;margin-left:2px;color:var(--_k-fg-3)}
18910
20098
  .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr) auto}
18911
20099
  .knobs-wt-stack[data-flat] .knobs-wt-srow+.knobs-wt-srow{box-shadow:inset 0 1px 0 var(--_k-border)}
18912
20100
  @container knobs-wt-slot (max-width:520px){
18913
20101
  .knobs-wt-srow:not([data-trunk]){grid-template-columns:auto minmax(0,1fr)}
18914
20102
  .knobs-wt-stack[data-flat] .knobs-wt-srow{grid-template-columns:minmax(0,1fr)}
18915
- .knobs-wt-sstatus{grid-column:-2;justify-self:start;margin-top:4px}
20103
+ :is(.knobs-wt-sstatus,.knobs-wt-prstate){grid-column:-2;justify-self:start;margin-top:4px}
18916
20104
  }
18917
20105
 
18918
20106
  /* A table: a real one, its head stuck while it scrolls, both ways; edges fade where there's more
@@ -18920,6 +20108,12 @@ var walkthroughCss = `
18920
20108
  /* Too narrow for its columns, each row becomes a block: its primary cell titling the rest as term
18921
20109
  and value, the blocks parted by hairlines. Scrolling sideways is the last resort (stack: never). */
18922
20110
  .knobs-wt-tablebox{min-width:0}
20111
+ /* A row with a step goes there: the whole row, a wash under the pointer, no arrow, so rows with and
20112
+ without one line up. */
20113
+ .knobs-wt-tablebox [data-go]{cursor:pointer;outline:none}
20114
+ .knobs-wt-tablebox tr[data-go]>td,.knobs-wt-trow[data-go]{transition:background calc(var(--_k-hover-out) * 1ms)}
20115
+ .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)}
20116
+ .knobs-wt-tablebox [data-go]:focus-visible{box-shadow:inset 0 0 0 1px var(--_k-focus)}
18923
20117
  .knobs-wt-tablebox[data-measure] table{width:min-content}
18924
20118
  .knobs-wt-tstack{display:none;flex-direction:column;min-width:0}
18925
20119
  .knobs-wt-tablebox[data-stacked]>.knobs-wt-table{display:none}
@@ -19091,6 +20285,8 @@ async function mountCompare(host, m, look, alive, onEvent) {
19091
20285
  fit.style.setProperty("--kw-aspect", String(aspect));
19092
20286
  let lastZoom = "all";
19093
20287
  let lastLoop = "off";
20288
+ let marks = null;
20289
+ let marking = null;
19094
20290
  const c = compare(fit, {
19095
20291
  ...look,
19096
20292
  before: side(before, "Before"),
@@ -19103,6 +20299,14 @@ async function mountCompare(host, m, look, alive, onEvent) {
19103
20299
  onMode: (next) => {
19104
20300
  fit.toggleAttribute("data-overlay", next !== "side");
19105
20301
  onEvent?.({ what: "mode", detail: next });
20302
+ const layer = marks?.element;
20303
+ if (layer) {
20304
+ layer.dataset.moving = "";
20305
+ setTimeout(() => {
20306
+ delete layer.dataset.moving;
20307
+ marks?.update();
20308
+ }, MARK_GLIDE);
20309
+ }
19106
20310
  },
19107
20311
  onView: (view) => {
19108
20312
  const z = view.zoom;
@@ -19113,8 +20317,23 @@ async function mountCompare(host, m, look, alive, onEvent) {
19113
20317
  const loopKey = l ? `${formatTime(l.start)}–${formatTime(l.end)}` : "off";
19114
20318
  if (loopKey !== lastLoop)
19115
20319
  onEvent?.({ what: "loop", detail: lastLoop = loopKey });
20320
+ marking?.kick(MARK_GLIDE);
19116
20321
  }
19117
20322
  });
20323
+ const stage = fit.querySelector(".knobs-cmp-stage");
20324
+ if (m.annotations?.length && stage) {
20325
+ const shape = { w: aspect ?? DEFAULT_ASPECT, h: 1 };
20326
+ marks = annotationLayer(stage, m.annotations, () => {
20327
+ const l = compareLayout({ x: 0, y: 0, w: stage.clientWidth, h: stage.clientHeight }, shape, shape);
20328
+ return {
20329
+ rects: c.mode === "side" ? [l.side.before, l.side.after] : [l.overlay.after],
20330
+ view: c.zoom,
20331
+ time: c.time
20332
+ };
20333
+ });
20334
+ const layer = marks;
20335
+ marking = follow(doc.defaultView, () => c.playing, () => layer.update());
20336
+ }
19118
20337
  const chapters = m.chapters?.length ? m.chapters : null;
19119
20338
  if (chapters)
19120
20339
  c.timeline?.update({
@@ -19131,10 +20350,14 @@ async function mountCompare(host, m, look, alive, onEvent) {
19131
20350
  const unlisten = c.timeline?.listen({
19132
20351
  onSeek: (time, source) => {
19133
20352
  pills?.sync();
20353
+ marking?.kick();
19134
20354
  if (source !== "play")
19135
20355
  onEvent?.({ what: "seek", time: round2(time) });
19136
20356
  },
19137
- onPlay: (on) => onEvent?.({ what: on ? "play" : "pause", time: round2(c.time) })
20357
+ onPlay: (on) => {
20358
+ marking?.kick();
20359
+ onEvent?.({ what: on ? "play" : "pause", time: round2(c.time) });
20360
+ }
19138
20361
  });
19139
20362
  requestAnimationFrame(() => requestAnimationFrame(() => fit.dataset.settled = ""));
19140
20363
  return {
@@ -19161,6 +20384,8 @@ async function mountCompare(host, m, look, alive, onEvent) {
19161
20384
  };
19162
20385
  },
19163
20386
  dispose: () => {
20387
+ marking?.stop();
20388
+ marks?.dispose();
19164
20389
  unlisten?.();
19165
20390
  pills?.dispose();
19166
20391
  c.dispose();
@@ -19169,6 +20394,7 @@ async function mountCompare(host, m, look, alive, onEvent) {
19169
20394
  };
19170
20395
  }
19171
20396
  var round2 = (v) => Math.round(v * 100) / 100;
20397
+ var MARK_GLIDE = 450;
19172
20398
  var zoomedIn = (z) => z.w < 0.999 || z.h < 0.999;
19173
20399
  function mountVideo(host, m, onEvent) {
19174
20400
  const doc = host.ownerDocument;
@@ -19216,6 +20442,16 @@ function mountVideo(host, m, onEvent) {
19216
20442
  if (pills)
19217
20443
  for (const type of ["timeupdate", "seeking", "seeked"])
19218
20444
  v.addEventListener(type, sync);
20445
+ const marks = m.annotations?.length ? annotationLayer(frame, m.annotations, () => ({
20446
+ rects: [
20447
+ contain({ w: frame.clientWidth, h: frame.clientHeight }, { w: v.videoWidth, h: v.videoHeight })
20448
+ ],
20449
+ time: v.currentTime
20450
+ })) : null;
20451
+ const marking = marks ? follow(doc.defaultView, () => !v.paused, () => marks.update()) : null;
20452
+ if (marking)
20453
+ for (const type of ["play", "seeked", "loadedmetadata", "timeupdate"])
20454
+ v.addEventListener(type, () => marking.kick());
19219
20455
  return {
19220
20456
  pause: () => v.pause(),
19221
20457
  seekBy: (d) => {
@@ -19229,6 +20465,8 @@ function mountVideo(host, m, onEvent) {
19229
20465
  ...pills?.current ? { chapter: pills.current.label } : {}
19230
20466
  }),
19231
20467
  dispose: () => {
20468
+ marking?.stop();
20469
+ marks?.dispose();
19232
20470
  pills?.dispose();
19233
20471
  v.pause();
19234
20472
  v.removeAttribute("src");
@@ -19246,9 +20484,52 @@ function mountImage(host, m) {
19246
20484
  img.decoding = "async";
19247
20485
  img.alt = m.alt ?? m.label ?? "";
19248
20486
  img.src = m.src;
20487
+ if (!img.complete) {
20488
+ frame.dataset.loading = "";
20489
+ img.addEventListener("load", () => delete frame.dataset.loading, { once: true });
20490
+ img.addEventListener("error", () => delete frame.dataset.loading, { once: true });
20491
+ }
19249
20492
  frame.append(img);
19250
20493
  host.append(frame);
19251
- return { dispose: () => frame.remove() };
20494
+ const marks = m.annotations?.length ? annotationLayer(frame, m.annotations, () => ({
20495
+ rects: [{ x: 0, y: 0, w: frame.clientWidth, h: frame.clientHeight }]
20496
+ })) : null;
20497
+ if (marks)
20498
+ img.addEventListener("load", () => marks.update(), { once: true });
20499
+ return {
20500
+ dispose: () => {
20501
+ marks?.dispose();
20502
+ frame.remove();
20503
+ }
20504
+ };
20505
+ }
20506
+ function contain(box, natural) {
20507
+ if (!(natural.w > 0 && natural.h > 0))
20508
+ return { x: 0, y: 0, w: box.w, h: box.h };
20509
+ const k = Math.min(box.w / natural.w, box.h / natural.h);
20510
+ const w = natural.w * k;
20511
+ const h = natural.h * k;
20512
+ return { x: (box.w - w) / 2, y: (box.h - h) / 2, w, h };
20513
+ }
20514
+ function follow(win, on, fn) {
20515
+ let raf = 0;
20516
+ let until = 0;
20517
+ const tick = () => {
20518
+ fn();
20519
+ raf = on() || performance.now() < until ? win.requestAnimationFrame(tick) : 0;
20520
+ };
20521
+ return {
20522
+ kick(ms = 0) {
20523
+ until = Math.max(until, performance.now() + ms);
20524
+ if (!raf)
20525
+ raf = win.requestAnimationFrame(tick);
20526
+ },
20527
+ stop() {
20528
+ if (raf)
20529
+ win.cancelAnimationFrame(raf);
20530
+ raf = 0;
20531
+ }
20532
+ };
19252
20533
  }
19253
20534
  async function mountDiff(host, m, look, alive, onEvent) {
19254
20535
  const lib = await Promise.resolve().then(() => (init_diff(), {}));
@@ -19402,6 +20683,14 @@ function mediaSlot(container, options) {
19402
20683
  let pending = false;
19403
20684
  let disposed = false;
19404
20685
  let generation = 0;
20686
+ const born = performance.now();
20687
+ const enter = () => {
20688
+ const win = doc.defaultView;
20689
+ if (!win || performance.now() - born < 120 || reducedMotion(win))
20690
+ return;
20691
+ host.dataset.entering = "";
20692
+ win.requestAnimationFrame(() => win.requestAnimationFrame(() => delete host.dataset.entering));
20693
+ };
19405
20694
  const hold = (px) => {
19406
20695
  host.style.minHeight = px === null ? "" : `${px}px`;
19407
20696
  host.classList.toggle("knobs-wt-hold", px !== null);
@@ -19421,6 +20710,7 @@ function mediaSlot(container, options) {
19421
20710
  if (!alive())
19422
20711
  return r.dispose();
19423
20712
  mounted = r;
20713
+ enter();
19424
20714
  if (filled)
19425
20715
  r.fill?.(true);
19426
20716
  if (r.ready)
@@ -19456,7 +20746,7 @@ function mediaSlot(container, options) {
19456
20746
  case "history":
19457
20747
  return done(mountHistory(host, m, options.steps));
19458
20748
  case "stack":
19459
- return done(mountStack(host, m, options.steps));
20749
+ return done(mountStack(host, m, options.steps, options.findPR));
19460
20750
  case "table":
19461
20751
  return done(mountTable(host, m, options.steps));
19462
20752
  case "mount": {
@@ -19510,17 +20800,17 @@ function mediaSlot(container, options) {
19510
20800
  }
19511
20801
  };
19512
20802
  }
19513
- var CHECK3 = icon("check", { small: true });
20803
+ var CHECK4 = icon("check", { small: true });
19514
20804
 
19515
20805
  // src/ui/walkthrough.ts
19516
- var el2 = (doc, tag, cls) => {
20806
+ var el3 = (doc, tag, cls) => {
19517
20807
  const n = doc.createElement(tag);
19518
20808
  if (cls)
19519
20809
  n.className = cls;
19520
20810
  return n;
19521
20811
  };
19522
20812
  var prose2 = (doc, tag, cls, text) => {
19523
- const n = el2(doc, tag, cls);
20813
+ const n = el3(doc, tag, cls);
19524
20814
  n.innerHTML = inlineMarkdown(text);
19525
20815
  return n;
19526
20816
  };
@@ -19559,7 +20849,7 @@ function tasteTest(container, options) {
19559
20849
  });
19560
20850
  const pickable = options.pickable !== false;
19561
20851
  let picked = pickable ? options.picked ?? null : null;
19562
- const root = el2(doc, "div", "knobs-wt-options");
20852
+ const root = el3(doc, "div", "knobs-wt-options");
19563
20853
  if (pickable) {
19564
20854
  root.setAttribute("role", "radiogroup");
19565
20855
  root.setAttribute("aria-label", "Options");
@@ -19570,34 +20860,34 @@ function tasteTest(container, options) {
19570
20860
  applyLook2(root, options);
19571
20861
  const slots = [];
19572
20862
  const tiles = list.map((o, i) => {
19573
- const tile = el2(doc, "div", "knobs-wt-option");
20863
+ const tile = el3(doc, "div", "knobs-wt-option");
19574
20864
  if (!pickable) {
19575
- const name = el2(doc, "span", "knobs-wt-slot-label");
20865
+ const name = el3(doc, "span", "knobs-wt-slot-label");
19576
20866
  name.textContent = o.label;
19577
20867
  tile.append(name);
19578
20868
  }
19579
- const pick = el2(doc, "button", "knobs-wt-pick");
20869
+ const pick = el3(doc, "button", "knobs-wt-pick");
19580
20870
  pick.type = "button";
19581
20871
  pick.setAttribute("role", "radio");
19582
20872
  if (i < 9)
19583
20873
  pick.setAttribute("aria-keyshortcuts", String(i + 1));
19584
- const letter = el2(doc, "span", "knobs-wt-letter");
20874
+ const letter = el3(doc, "span", "knobs-wt-letter");
19585
20875
  letter.textContent = o.letter;
19586
- const name = el2(doc, "span", "knobs-wt-olabel");
20876
+ const name = el3(doc, "span", "knobs-wt-olabel");
19587
20877
  name.textContent = o.label;
19588
20878
  pick.append(letter, name);
19589
20879
  if (o.suggested) {
19590
- const s = el2(doc, "span", "knobs-wt-suggested");
20880
+ const s = el3(doc, "span", "knobs-wt-suggested");
19591
20881
  s.textContent = "Suggested";
19592
20882
  pick.append(s);
19593
20883
  }
19594
20884
  if (i < 9) {
19595
- const key = el2(doc, "span", "knobs-wt-key");
20885
+ const key = el3(doc, "span", "knobs-wt-key");
19596
20886
  key.textContent = String(i + 1);
19597
20887
  pick.append(key);
19598
20888
  }
19599
- const check = el2(doc, "span", "knobs-wt-check");
19600
- check.innerHTML = CHECK3;
20889
+ const check = el3(doc, "span", "knobs-wt-check");
20890
+ check.innerHTML = CHECK4;
19601
20891
  pick.append(check);
19602
20892
  pick.addEventListener("click", () => {
19603
20893
  const next = picked === o.id ? null : o.id;
@@ -19662,7 +20952,7 @@ function walkthrough(container, options) {
19662
20952
  injectWalkthroughStyles(doc);
19663
20953
  const look = lookOf2(options);
19664
20954
  const storage = options.storage === undefined ? browserStorage() : options.storage;
19665
- const root = el2(doc, "div", "knobs-walkthrough");
20955
+ const root = el3(doc, "div", "knobs-walkthrough");
19666
20956
  applyLook2(root, options);
19667
20957
  container.append(root);
19668
20958
  let page = normalizeWalkthrough(options.doc);
@@ -19875,7 +21165,7 @@ function walkthrough(container, options) {
19875
21165
  return true;
19876
21166
  return false;
19877
21167
  };
19878
- const scrim = el2(doc, "div", "knobs-wt-scrim");
21168
+ const scrim = el3(doc, "div", "knobs-wt-scrim");
19879
21169
  scrim.addEventListener("click", () => setFull(false));
19880
21170
  const fullRect = () => ({
19881
21171
  x: DOCK_INSET,
@@ -19925,7 +21215,7 @@ function walkthrough(container, options) {
19925
21215
  }
19926
21216
  if (!full) {
19927
21217
  const r = v.card.getBoundingClientRect();
19928
- const hold = el2(doc, "div", "knobs-wt-hold-card");
21218
+ const hold = el3(doc, "div", "knobs-wt-hold-card");
19929
21219
  hold.style.height = `${r.height}px`;
19930
21220
  v.card.before(hold);
19931
21221
  v.card.dataset.full = "";
@@ -20075,7 +21365,7 @@ function walkthrough(container, options) {
20075
21365
  toast("Couldn't reach the microphone", { tone: "warn", look, doc });
20076
21366
  }
20077
21367
  };
20078
- const backPill = el2(doc, "button", "knobs-wt-back");
21368
+ const backPill = el3(doc, "button", "knobs-wt-back");
20079
21369
  backPill.type = "button";
20080
21370
  let backTo = null;
20081
21371
  let backTimer = 0;
@@ -20095,7 +21385,7 @@ function walkthrough(container, options) {
20095
21385
  return;
20096
21386
  backTo = key;
20097
21387
  backPill.innerHTML = ICON.back;
20098
- const words = el2(doc, "span", "knobs-wt-back-label");
21388
+ const words = el3(doc, "span", "knobs-wt-back-label");
20099
21389
  words.textContent = `Back to ${plainMarkdown(v.step.title)}`;
20100
21390
  backPill.append(words);
20101
21391
  backPill.dataset.on = "";
@@ -20103,16 +21393,16 @@ function walkthrough(container, options) {
20103
21393
  backTimer = win.setTimeout(hideBack, BACK_MS);
20104
21394
  };
20105
21395
  const bar = (() => {
20106
- const dock = el2(doc, "div", "knobs-wt-dock");
20107
- const box = el2(doc, "div", "knobs-wt-notebar");
20108
- const on = el2(doc, "div", "knobs-wt-on");
20109
- const row = el2(doc, "div", "knobs-wt-row");
20110
- const input = el2(doc, "textarea", "knobs-wt-input");
21396
+ const dock = el3(doc, "div", "knobs-wt-dock");
21397
+ const box = el3(doc, "div", "knobs-wt-notebar");
21398
+ const on = el3(doc, "div", "knobs-wt-on");
21399
+ const row = el3(doc, "div", "knobs-wt-row");
21400
+ const input = el3(doc, "textarea", "knobs-wt-input");
20111
21401
  input.rows = 1;
20112
21402
  input.setAttribute("aria-label", "Note");
20113
- const attach = el2(doc, "div", "knobs-wt-attach");
21403
+ const attach = el3(doc, "div", "knobs-wt-attach");
20114
21404
  const btn = (icon, label, run) => {
20115
- const b = el2(doc, "button", "knobs-wt-tool");
21405
+ const b = el3(doc, "button", "knobs-wt-tool");
20116
21406
  b.type = "button";
20117
21407
  b.innerHTML = icon;
20118
21408
  b.setAttribute("aria-label", label);
@@ -20123,17 +21413,17 @@ function walkthrough(container, options) {
20123
21413
  const fullBtn = btn(ICON.full, "Full window · f", () => setFull(!full));
20124
21414
  const penBtn = btn(ICON.pen, "Draw on the media", () => setInk(!ink.on));
20125
21415
  const micBtn = btn(ICON.mic, "Record a voice note", () => void toggleVoice());
20126
- const micTime = el2(doc, "span", "knobs-wt-rec");
21416
+ const micTime = el3(doc, "span", "knobs-wt-rec");
20127
21417
  micBtn.append(micTime);
20128
21418
  const sendBtn = btn(ICON.send, "Send · Enter", () => send());
20129
21419
  sendBtn.dataset.send = "";
20130
21420
  row.append(input, attach, fullBtn, penBtn, micBtn, sendBtn);
20131
21421
  box.append(on, row);
20132
- const launch = el2(doc, "button", "knobs-wt-launch");
21422
+ const launch = el3(doc, "button", "knobs-wt-launch");
20133
21423
  launch.type = "button";
20134
21424
  launch.innerHTML = ICON.pen;
20135
21425
  launch.append("Note");
20136
- const kbd = el2(doc, "kbd");
21426
+ const kbd = el3(doc, "kbd");
20137
21427
  kbd.textContent = "n";
20138
21428
  launch.append(kbd);
20139
21429
  launch.setAttribute("aria-label", "Write a note (n)");
@@ -20185,9 +21475,9 @@ function walkthrough(container, options) {
20185
21475
  }
20186
21476
  });
20187
21477
  const chip = (text, clear) => {
20188
- const c = el2(doc, "span", "knobs-wt-chip-a");
21478
+ const c = el3(doc, "span", "knobs-wt-chip-a");
20189
21479
  c.append(text);
20190
- const x = el2(doc, "button", "knobs-wt-x");
21480
+ const x = el3(doc, "button", "knobs-wt-x");
20191
21481
  x.type = "button";
20192
21482
  x.innerHTML = ICON.close;
20193
21483
  x.setAttribute("aria-label", `Remove ${text}`);
@@ -20336,9 +21626,9 @@ function walkthrough(container, options) {
20336
21626
  }
20337
21627
  };
20338
21628
  const renderNote = (v, n, entering = false) => {
20339
- const row = el2(doc, "div", "knobs-wt-n");
21629
+ const row = el3(doc, "div", "knobs-wt-n");
20340
21630
  row.dataset.id = n.id;
20341
- const meta = el2(doc, "span", "knobs-wt-n-meta");
21631
+ const meta = el3(doc, "span", "knobs-wt-n-meta");
20342
21632
  const m = n.context.media;
20343
21633
  meta.textContent = m?.time !== undefined ? formatTime(m.time) : m?.file ? m.file.split("/").pop() : "·";
20344
21634
  if (m?.time !== undefined) {
@@ -20354,11 +21644,11 @@ function walkthrough(container, options) {
20354
21644
  });
20355
21645
  }
20356
21646
  row.append(meta);
20357
- const body = el2(doc, "div", "knobs-wt-n-body");
21647
+ const body = el3(doc, "div", "knobs-wt-n-body");
20358
21648
  if (n.text)
20359
21649
  body.append(prose2(doc, "p", "knobs-wt-n-text", n.text));
20360
21650
  if (n.voice) {
20361
- const a = el2(doc, "audio", "knobs-wt-n-voice");
21651
+ const a = el3(doc, "audio", "knobs-wt-n-voice");
20362
21652
  a.controls = true;
20363
21653
  a.preload = "none";
20364
21654
  a.src = n.voice.url;
@@ -20378,7 +21668,7 @@ function walkthrough(container, options) {
20378
21668
  body.append(thumb);
20379
21669
  }
20380
21670
  row.append(body);
20381
- const x = el2(doc, "button", "knobs-wt-x");
21671
+ const x = el3(doc, "button", "knobs-wt-x");
20382
21672
  x.type = "button";
20383
21673
  x.innerHTML = ICON.close;
20384
21674
  x.setAttribute("aria-label", "Delete this note");
@@ -20418,9 +21708,9 @@ function walkthrough(container, options) {
20418
21708
  row.addEventListener("transitionend", done, { once: true });
20419
21709
  win.setTimeout(done, 600);
20420
21710
  };
20421
- const metaLine = (cls) => el2(doc, "div", `knobs-wt-metaline ${cls}`);
21711
+ const metaLine = (cls) => el3(doc, "div", `knobs-wt-metaline ${cls}`);
20422
21712
  const metaText = (line, text, cls) => {
20423
- const s = el2(doc, "span", cls);
21713
+ const s = el3(doc, "span", cls);
20424
21714
  s.textContent = text;
20425
21715
  line.append(s);
20426
21716
  return s;
@@ -20433,7 +21723,7 @@ function walkthrough(container, options) {
20433
21723
  line.append(chip);
20434
21724
  return chip;
20435
21725
  }
20436
- const a = el2(doc, "a");
21726
+ const a = el3(doc, "a");
20437
21727
  a.href = href;
20438
21728
  a.target = "_blank";
20439
21729
  a.rel = "noreferrer";
@@ -20443,58 +21733,58 @@ function walkthrough(container, options) {
20443
21733
  return a;
20444
21734
  };
20445
21735
  const feedItem = (f, now) => {
20446
- const li = el2(doc, "li", "knobs-wt-fi");
21736
+ const li = el3(doc, "li", "knobs-wt-fi");
20447
21737
  li.dataset.kind = f.kind;
20448
- const q = el2(doc, "div", "knobs-wt-fi-q");
21738
+ const q = el3(doc, "div", "knobs-wt-fi-q");
20449
21739
  const meta = metaLine("knobs-wt-fi-meta");
20450
21740
  if (f.at !== null) {
20451
21741
  const when = metaText(meta, ago(f.at, now));
20452
21742
  when.title = new Date(f.at).toLocaleString();
20453
- } else
20454
- metaText(meta, f.kind === "qa" ? "Answer" : "Update");
21743
+ }
20455
21744
  const step = f.step ? page.steps.find((s) => s.key === f.step) : undefined;
20456
21745
  if (step) {
20457
21746
  const section = page.sections.find((s) => s.id === step.section);
20458
- const b = el2(doc, "button", "knobs-wt-fi-step");
21747
+ const b = el3(doc, "button", "knobs-wt-fi-step");
20459
21748
  b.type = "button";
20460
21749
  b.textContent = section?.link?.label ?? plainMarkdown(step.title);
20461
21750
  b.title = `Go to “${plainMarkdown(step.title)}”`;
20462
21751
  b.addEventListener("click", () => go(step.key));
20463
21752
  meta.append(b);
20464
21753
  }
20465
- if (f.status) {
21754
+ if (f.status && review) {
20466
21755
  const id = f.status.toLowerCase();
20467
21756
  const shipped = /^(ship|shipped|merged|done)$/.test(id);
20468
21757
  const changes = /^(changes?|changes queued)$/.test(id);
20469
21758
  const s = metaText(meta, shipped ? "Shipped" : changes ? "Changes queued" : f.status);
20470
21759
  s.dataset.status = shipped ? "on" : changes ? "warn" : "";
20471
21760
  }
20472
- q.append(meta);
21761
+ if (meta.childNodes.length)
21762
+ q.append(meta);
20473
21763
  if (f.q)
20474
21764
  q.append(prose2(doc, "p", "knobs-wt-fi-ask", f.q));
20475
21765
  else
20476
21766
  q.append(prose2(doc, "p", "knobs-wt-fi-text", f.text));
20477
21767
  li.append(q);
20478
21768
  if (f.q || f.trail.length) {
20479
- const a = el2(doc, "div", "knobs-wt-fi-a");
21769
+ const a = el3(doc, "div", "knobs-wt-fi-a");
20480
21770
  if (f.q)
20481
21771
  a.append(prose2(doc, "div", "knobs-wt-fi-answer", f.text));
20482
21772
  if (f.trail.length) {
20483
- const list = el2(doc, "ol", "knobs-wt-trail");
21773
+ const list = el3(doc, "ol", "knobs-wt-trail");
20484
21774
  for (const line of f.trail) {
20485
21775
  const i = line.indexOf(" · ");
20486
- const row = el2(doc, "li");
20487
- const words = el2(doc, "span", "knobs-wt-trail-text");
21776
+ const row = el3(doc, "li");
21777
+ const words = el3(doc, "span", "knobs-wt-trail-text");
20488
21778
  words.textContent = i < 0 ? line : line.slice(i + 3);
20489
21779
  row.append(words);
20490
21780
  if (i >= 0) {
20491
- const at = el2(doc, "span", "knobs-wt-trail-at");
21781
+ const at = el3(doc, "span", "knobs-wt-trail-at");
20492
21782
  at.textContent = line.slice(0, i);
20493
21783
  row.append(at);
20494
21784
  }
20495
21785
  list.append(row);
20496
21786
  }
20497
- discs.push(disclosure(a, { label: "Context", count: f.trail.length, content: list }));
21787
+ discs.push(disclosure(a, { label: "Context", content: list }));
20498
21788
  }
20499
21789
  li.append(a);
20500
21790
  }
@@ -20522,11 +21812,11 @@ function walkthrough(container, options) {
20522
21812
  root.dataset.rail = options.rail === false ? "none" : options.rail === "right" ? "right" : "left";
20523
21813
  root.toggleAttribute("data-cards", options.cards === true);
20524
21814
  root.toggleAttribute("data-review", review);
20525
- const layout = el2(doc, "div", "knobs-wt-layout");
20526
- const main = el2(doc, "div", "knobs-wt-main");
20527
- const flow = el2(doc, "div", "knobs-wt-flow");
20528
- const head = el2(doc, "header", "knobs-wt-head");
20529
- const h1 = el2(doc, "h1", "knobs-wt-title");
21815
+ const layout = el3(doc, "div", "knobs-wt-layout");
21816
+ const main = el3(doc, "div", "knobs-wt-main");
21817
+ const flow = el3(doc, "div", "knobs-wt-flow");
21818
+ const head = el3(doc, "header", "knobs-wt-head");
21819
+ const h1 = el3(doc, "h1", "knobs-wt-title");
20530
21820
  h1.textContent = page.title;
20531
21821
  head.append(h1);
20532
21822
  if (page.summary)
@@ -20534,13 +21824,13 @@ function walkthrough(container, options) {
20534
21824
  if (page.more)
20535
21825
  head.append(more(page.more));
20536
21826
  if (page.meta.length) {
20537
- const meta = el2(doc, "div", "knobs-wt-meta");
21827
+ const meta = el3(doc, "div", "knobs-wt-meta");
20538
21828
  for (const m of page.meta) {
20539
- const s = el2(doc, "span");
21829
+ const s = el3(doc, "span");
20540
21830
  s.append(m.label);
20541
- const b = el2(doc, "b");
21831
+ const b = el3(doc, "b");
20542
21832
  if (m.href) {
20543
- const a = el2(doc, "a");
21833
+ const a = el3(doc, "a");
20544
21834
  a.href = m.href;
20545
21835
  a.target = "_blank";
20546
21836
  a.rel = "noreferrer";
@@ -20556,16 +21846,14 @@ function walkthrough(container, options) {
20556
21846
  flow.append(head);
20557
21847
  let feedEl = null;
20558
21848
  if (page.feed.length) {
20559
- feedEl = el2(doc, "section", "knobs-wt-feed");
21849
+ feedEl = el3(doc, "section", "knobs-wt-feed");
20560
21850
  feedEl.id = "wt-latest";
20561
21851
  feedEl.setAttribute("aria-label", "Updates and answers");
20562
- const fh = el2(doc, "header", "knobs-wt-feed-head");
20563
- const h = el2(doc, "h2", "knobs-wt-feed-title");
21852
+ const fh = el3(doc, "header", "knobs-wt-feed-head");
21853
+ const h = el3(doc, "h2", "knobs-wt-feed-title");
20564
21854
  h.textContent = page.feed.some((f) => f.kind === "qa") ? "Updates & Q&A" : "Updates";
20565
- const n = el2(doc, "span", "knobs-wt-feed-count");
20566
- n.textContent = String(page.feed.length);
20567
- fh.append(h, n);
20568
- const list = el2(doc, "ol", "knobs-wt-feed-list");
21855
+ fh.append(h);
21856
+ const list = el3(doc, "ol", "knobs-wt-feed-list");
20569
21857
  const now = Date.now();
20570
21858
  for (const f of page.feed)
20571
21859
  list.append(feedItem(f, now));
@@ -20575,21 +21863,21 @@ function walkthrough(container, options) {
20575
21863
  let rail = null;
20576
21864
  count = fill = tocEl = null;
20577
21865
  if (options.rail !== false) {
20578
- rail = el2(doc, "aside", "knobs-wt-rail");
21866
+ rail = el3(doc, "aside", "knobs-wt-rail");
20579
21867
  rail.setAttribute("aria-label", "Contents");
20580
- tocEl = el2(doc, "nav", "knobs-wt-toc");
21868
+ tocEl = el3(doc, "nav", "knobs-wt-toc");
20581
21869
  tocEl.setAttribute("aria-label", "Contents");
20582
21870
  if (feedEl) {
20583
- const group = el2(doc, "div", "knobs-wt-toc-group");
21871
+ const group = el3(doc, "div", "knobs-wt-toc-group");
20584
21872
  const target = feedEl;
20585
21873
  group.append(tocRow("Latest", false, () => target.scrollIntoView({ block: "start", behavior: reduced() ? "auto" : "smooth" })));
20586
21874
  tocEl.append(group);
20587
21875
  }
20588
21876
  if (review) {
20589
- const progress = el2(doc, "div", "knobs-wt-progress");
20590
- count = el2(doc, "div", "knobs-wt-count");
20591
- const meter = el2(doc, "div", "knobs-wt-bar");
20592
- fill = el2(doc, "div", "knobs-wt-fill");
21877
+ const progress = el3(doc, "div", "knobs-wt-progress");
21878
+ count = el3(doc, "div", "knobs-wt-count");
21879
+ const meter = el3(doc, "div", "knobs-wt-bar");
21880
+ fill = el3(doc, "div", "knobs-wt-fill");
20593
21881
  meter.append(fill);
20594
21882
  progress.append(count, meter);
20595
21883
  rail.append(progress);
@@ -20598,15 +21886,15 @@ function walkthrough(container, options) {
20598
21886
  if (review)
20599
21887
  rail.append(actions(true));
20600
21888
  if (review && options.keys !== false) {
20601
- const keys = el2(doc, "div", "knobs-wt-keys");
21889
+ const keys = el3(doc, "div", "knobs-wt-keys");
20602
21890
  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" : "");
20603
21891
  rail.append(keys);
20604
21892
  }
20605
21893
  }
20606
21894
  for (const section of page.sections) {
20607
- const sec = el2(doc, "section", "knobs-wt-section");
21895
+ const sec = el3(doc, "section", "knobs-wt-section");
20608
21896
  sec.id = `wt-${section.id}`;
20609
- const sh = el2(doc, "header", "knobs-wt-shead");
21897
+ const sh = el3(doc, "header", "knobs-wt-shead");
20610
21898
  const status = review ? section.status : undefined;
20611
21899
  if (section.kicker || section.link || status) {
20612
21900
  const meta = metaLine("knobs-wt-kicker");
@@ -20620,10 +21908,10 @@ function walkthrough(container, options) {
20620
21908
  }
20621
21909
  sh.append(meta);
20622
21910
  }
20623
- const h2 = el2(doc, "h2", "knobs-wt-heading");
21911
+ const h2 = el3(doc, "h2", "knobs-wt-heading");
20624
21912
  h2.innerHTML = inlineMarkdown(section.title);
20625
21913
  sh.append(h2);
20626
- const intro = el2(doc, "div", "knobs-wt-sintro");
21914
+ const intro = el3(doc, "div", "knobs-wt-sintro");
20627
21915
  intro.append(sh);
20628
21916
  if (section.summary)
20629
21917
  intro.append(prose2(doc, "p", "knobs-wt-ssum", section.summary));
@@ -20632,7 +21920,7 @@ function walkthrough(container, options) {
20632
21920
  sec.append(intro);
20633
21921
  let group = null;
20634
21922
  if (tocEl) {
20635
- group = el2(doc, "div", "knobs-wt-toc-group");
21923
+ group = el3(doc, "div", "knobs-wt-toc-group");
20636
21924
  const first = section.steps[0]?.key;
20637
21925
  group.append(tocRow(plainMarkdown(section.title), false, () => first ? go(first) : undefined, section.id));
20638
21926
  tocEl.append(group);
@@ -20674,8 +21962,8 @@ function walkthrough(container, options) {
20674
21962
  }));
20675
21963
  };
20676
21964
  const more = (text) => {
20677
- const box = el2(doc, "div", "knobs-wt-more");
20678
- const body = el2(doc, "div", "knobs-wt-more-text");
21965
+ const box = el3(doc, "div", "knobs-wt-more");
21966
+ const body = el3(doc, "div", "knobs-wt-more-text");
20679
21967
  for (const para of text.split(/\n\s*\n/))
20680
21968
  if (para.trim())
20681
21969
  body.append(prose2(doc, "p", "", para.trim()));
@@ -20683,24 +21971,25 @@ function walkthrough(container, options) {
20683
21971
  return box;
20684
21972
  };
20685
21973
  const tocRow = (label, sub, run, section) => {
20686
- const b = el2(doc, "button", sub ? "knobs-wt-toc-row knobs-wt-toc-sub" : "knobs-wt-toc-row");
21974
+ const b = el3(doc, "button", sub ? "knobs-wt-toc-row knobs-wt-toc-sub" : "knobs-wt-toc-row");
20687
21975
  b.type = "button";
20688
21976
  if (section)
20689
21977
  b.dataset.section = section;
20690
- const t = el2(doc, "span", "knobs-wt-toc-label");
21978
+ const t = el3(doc, "span", "knobs-wt-toc-label");
20691
21979
  t.textContent = label;
20692
- const e = el2(doc, "span", "knobs-wt-toc-end");
20693
21980
  b.title = label;
20694
- b.append(t, e);
21981
+ b.append(t);
21982
+ if (review)
21983
+ b.append(el3(doc, "span", "knobs-wt-toc-end"));
20695
21984
  b.addEventListener("mousedown", (ev) => ev.preventDefault());
20696
21985
  b.addEventListener("click", run);
20697
21986
  tocRows.push(b);
20698
21987
  return b;
20699
21988
  };
20700
21989
  const actions = (rail) => {
20701
- const row = el2(doc, "div", "knobs-wt-actions");
21990
+ const row = el3(doc, "div", "knobs-wt-actions");
20702
21991
  const btn = (text, run, primary = false) => {
20703
- const b = el2(doc, "button", "knobs-wt-btn");
21992
+ const b = el3(doc, "button", "knobs-wt-btn");
20704
21993
  b.type = "button";
20705
21994
  b.textContent = text;
20706
21995
  if (primary)
@@ -20738,13 +22027,13 @@ function walkthrough(container, options) {
20738
22027
  }
20739
22028
  };
20740
22029
  const stepView = (step, group) => {
20741
- const card = el2(doc, "article", options.cards ? "knobs-wt-step knobs-surface" : "knobs-wt-step");
22030
+ const card = el3(doc, "article", options.cards ? "knobs-wt-step knobs-surface" : "knobs-wt-step");
20742
22031
  card.id = `wt-${step.key.replace("/", "--")}`;
20743
22032
  card.tabIndex = -1;
20744
22033
  card.dataset.key = step.key;
20745
22034
  card.setAttribute("aria-label", plainMarkdown(step.title));
20746
- const head = el2(doc, "header", "knobs-wt-step-head");
20747
- const h3 = el2(doc, "h3", "knobs-wt-stepTitle");
22035
+ const head = el3(doc, "header", "knobs-wt-step-head");
22036
+ const h3 = el3(doc, "h3", "knobs-wt-stepTitle");
20748
22037
  h3.innerHTML = inlineMarkdown(step.title);
20749
22038
  head.append(h3);
20750
22039
  if (step.caption)
@@ -20756,10 +22045,10 @@ function walkthrough(container, options) {
20756
22045
  const onEvent = (e) => record({ ...e, step: step.key });
20757
22046
  let media = null;
20758
22047
  if (step.media.length) {
20759
- media = el2(doc, "div", "knobs-wt-media");
22048
+ media = el3(doc, "div", "knobs-wt-media");
20760
22049
  media.style.setProperty("--kw-n", String(Math.min(step.media.length, step.columns)));
20761
22050
  for (const m of step.media)
20762
- slots.push(mediaSlot(media, { ...look, media: m, onEvent, steps: stepLinks }));
22051
+ slots.push(mediaSlot(media, { ...look, media: m, onEvent, steps: stepLinks, findPR: findPRIn }));
20763
22052
  card.append(media);
20764
22053
  }
20765
22054
  let taste = null;
@@ -20776,7 +22065,7 @@ function walkthrough(container, options) {
20776
22065
  });
20777
22066
  media ??= taste.element;
20778
22067
  }
20779
- const notes = el2(doc, "div", "knobs-wt-notes");
22068
+ const notes = el3(doc, "div", "knobs-wt-notes");
20780
22069
  if (review)
20781
22070
  card.append(notes);
20782
22071
  card.addEventListener("focusin", () => {
@@ -20790,7 +22079,7 @@ function walkthrough(container, options) {
20790
22079
  if (group) {
20791
22080
  toc = tocRow(plainMarkdown(step.title), true, () => go(step.key));
20792
22081
  if (review) {
20793
- glyph = el2(doc, "span", "knobs-wt-toc-glyph");
22082
+ glyph = el3(doc, "span", "knobs-wt-toc-glyph");
20794
22083
  toc.querySelector(".knobs-wt-toc-end").append(glyph);
20795
22084
  }
20796
22085
  group.append(toc);
@@ -20864,6 +22153,10 @@ function walkthrough(container, options) {
20864
22153
  claim();
20865
22154
  return;
20866
22155
  }
22156
+ if (key === "a" && !e.shiftKey && root.querySelector(".knobs-wt-anno")) {
22157
+ claim();
22158
+ return setAnnotations(root, !annotationsOn(root));
22159
+ }
20867
22160
  if (key === "n" && noteMode !== "hidden") {
20868
22161
  claim();
20869
22162
  bar.setOpen(true, true);
@@ -20962,7 +22255,7 @@ function walkthrough(container, options) {
20962
22255
  }
20963
22256
 
20964
22257
  // src/ui/solid.ts
20965
- var follow = (read, apply) => {
22258
+ var follow2 = (read, apply) => {
20966
22259
  const changes = propChanges(untrack(read));
20967
22260
  createEffect(() => {
20968
22261
  const changed = changes(read());
@@ -21010,7 +22303,7 @@ function Tree(props) {
21010
22303
  }
21011
22304
  }));
21012
22305
  untrack(() => props.onReady?.(t));
21013
- follow(() => ({ items: props.items, filter: props.filter, selection: props.selection }), (c) => {
22306
+ follow2(() => ({ items: props.items, filter: props.filter, selection: props.selection }), (c) => {
21014
22307
  if ("items" in c)
21015
22308
  t.update(c.items);
21016
22309
  if ("filter" in c)
@@ -21046,7 +22339,7 @@ function CodeDiff(props) {
21046
22339
  onViewed: props.onViewed && ((viewed, file) => props.onViewed?.(viewed, file))
21047
22340
  }));
21048
22341
  untrack(() => props.onReady?.(d));
21049
- follow(diffProps(props), (c) => {
22342
+ follow2(diffProps(props), (c) => {
21050
22343
  if (c.height)
21051
22344
  host.style.height = c.height === "auto" ? "" : "100%";
21052
22345
  d.update(c);
@@ -21069,7 +22362,7 @@ function DiffReview(props) {
21069
22362
  onViewed: (viewed, file) => props.onViewed?.(viewed, file)
21070
22363
  }));
21071
22364
  untrack(() => props.onReady?.(r));
21072
- follow(diffProps(props), (c) => {
22365
+ follow2(diffProps(props), (c) => {
21073
22366
  if (c.height)
21074
22367
  host.style.height = c.height === "auto" ? "" : "100%";
21075
22368
  r.update(c);
@@ -21091,7 +22384,7 @@ function Timeline(props) {
21091
22384
  onCross: (m) => props.onCross?.(m)
21092
22385
  }));
21093
22386
  untrack(() => props.onReady?.(t));
21094
- follow(() => ({
22387
+ follow2(() => ({
21095
22388
  duration: props.duration,
21096
22389
  fps: props.fps,
21097
22390
  units: props.units,
@@ -21120,7 +22413,7 @@ function Compare(props) {
21120
22413
  onView: (view) => props.onView?.(view)
21121
22414
  }));
21122
22415
  untrack(() => props.onReady?.(c));
21123
- follow(() => ({
22416
+ follow2(() => ({
21124
22417
  before: props.before,
21125
22418
  after: props.after,
21126
22419
  mode: props.mode,
@@ -21143,7 +22436,7 @@ function CompareStrip(props) {
21143
22436
  host.style.height = "100%";
21144
22437
  const s = untrack(() => compareStrip(host, { ...props, onPick: (time) => props.onPick?.(time) }));
21145
22438
  untrack(() => props.onReady?.(s));
21146
- follow(() => ({ before: props.before.src, after: props.after.src }), (changed) => s.update({
22439
+ follow2(() => ({ before: props.before.src, after: props.after.src }), (changed) => s.update({
21147
22440
  ..."before" in changed ? { before: props.before } : {},
21148
22441
  ..."after" in changed ? { after: props.after } : {}
21149
22442
  }));
@@ -21163,7 +22456,7 @@ function Bento(props) {
21163
22456
  onActive: (id) => props.onActive?.(id)
21164
22457
  }));
21165
22458
  untrack(() => props.onReady?.(b));
21166
- follow(() => ({ items: props.items, picks: props.picks }), (c) => {
22459
+ follow2(() => ({ items: props.items, picks: props.picks }), (c) => {
21167
22460
  if ("items" in c)
21168
22461
  b.update(c.items);
21169
22462
  if (c.picks)
@@ -21185,7 +22478,7 @@ function Activity(props) {
21185
22478
  onPlay: props.onPlay && ((id, event) => props.onPlay?.(id, event))
21186
22479
  }));
21187
22480
  untrack(() => props.onReady?.(a));
21188
- follow(() => ({ events: props.events, newest: props.newest, selected: props.selected }), (c) => {
22481
+ follow2(() => ({ events: props.events, newest: props.newest, selected: props.selected }), (c) => {
21189
22482
  if (c.events || c.newest)
21190
22483
  a.update({ events: c.events, newest: c.newest });
21191
22484
  if (c.selected !== undefined && c.selected !== a.selected)
@@ -21214,7 +22507,7 @@ function OptionPicker(props) {
21214
22507
  }
21215
22508
  }));
21216
22509
  untrack(() => props.onReady?.(p));
21217
- follow(() => ({ options: props.options, selected: props.selected, chosen: props.chosen }), (c) => {
22510
+ follow2(() => ({ options: props.options, selected: props.selected, chosen: props.chosen }), (c) => {
21218
22511
  if ("options" in c)
21219
22512
  p.set(c.options);
21220
22513
  if (c.selected != null)
@@ -21231,7 +22524,7 @@ function Walkthrough(props) {
21231
22524
  const host = document.createElement("div");
21232
22525
  const w = untrack(() => walkthrough(host, { ...props, onFeedback: (feedback) => props.onFeedback?.(feedback) }));
21233
22526
  untrack(() => props.onReady?.(w));
21234
- follow(() => ({ doc: props.doc }), (c) => c.doc && w.set(c.doc));
22527
+ follow2(() => ({ doc: props.doc }), (c) => c.doc && w.set(c.doc));
21235
22528
  onCleanup(() => w.dispose());
21236
22529
  return host;
21237
22530
  }
@@ -21241,7 +22534,7 @@ function TasteTest(props) {
21241
22534
  const host = document.createElement("div");
21242
22535
  const t = untrack(() => tasteTest(host, { ...props, onPick: (id) => props.onPick?.(id) }));
21243
22536
  untrack(() => props.onReady?.(t));
21244
- follow(() => ({ picked: props.picked }), (c) => ("picked" in c) && t.pick(c.picked ?? null));
22537
+ follow2(() => ({ picked: props.picked }), (c) => ("picked" in c) && t.pick(c.picked ?? null));
21245
22538
  onCleanup(() => t.dispose());
21246
22539
  return host;
21247
22540
  }