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

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.js CHANGED
@@ -595,6 +595,8 @@ var compareCss = `
595
595
  .knobs-cmp-stage{position:relative;flex:1;min-height:96px;overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_ku-cmp-stage);touch-action:pan-y;outline:none}
596
596
  .knobs-compare[data-frame="flat"] .knobs-cmp-stage{border-radius:0}
597
597
  .knobs-compare[data-mode="wipe"] .knobs-cmp-stage{cursor:ew-resize}
598
+ /* Videos: a click on the stage plays and pauses. */
599
+ .knobs-compare[data-video]:not([data-mode="wipe"],[data-zoomed]) .knobs-cmp-stage{cursor:pointer}
598
600
  /* Unzoomed, a finger still scrolls the page up and down; zoomed, the stage takes every gesture. */
599
601
  .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:grab;touch-action:none}
600
602
  .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:grabbing!important}
@@ -604,6 +606,9 @@ var compareCss = `
604
606
  .knobs-cmp-canvas{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
605
607
  .knobs-cmp-media{position:absolute;left:0;top:0;display:block;max-width:none;margin:0;padding:0;border:0;object-fit:fill;transform-origin:0 0;pointer-events:none;will-change:transform}
606
608
  .knobs-compare[data-renderer="gpu"] .knobs-cmp-media{opacity:0}
609
+ /* The media comes in with its first frames: a fade and a 4px blur on the default spring. */
610
+ .knobs-cmp-canvas,.knobs-compare[data-renderer="dom"] .knobs-cmp-media{transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
611
+ .knobs-compare:not([data-ready]) :is(.knobs-cmp-canvas,.knobs-cmp-media){opacity:0;filter:blur(4px)}
607
612
  /* Glass: what's behind shows through, blurred, on the knobs tokens. */
608
613
  .knobs-cmp-label,.knobs-cmp-zoom{background:color-mix(in srgb,var(--_k-bg) 52%,transparent);-webkit-backdrop-filter:blur(14px) saturate(1.6);backdrop-filter:blur(14px) saturate(1.6);
609
614
  box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim),0 2px 10px rgba(0,0,0,calc(.18 * var(--_k-shadow-k,1)))}
@@ -612,11 +617,17 @@ var compareCss = `
612
617
  white-space:nowrap;pointer-events:none;will-change:transform}
613
618
  .knobs-cmp-label[data-side="before"]{color:var(--_k-fg-3)}
614
619
  .knobs-cmp-label[data-side="after"]{color:var(--_k-fg)}
615
- .knobs-cmp-mark svg{display:block;width:12px;height:12px}
616
- /* The wipe: a hairline down the line with a raised grip at the pointer's height; a wider strip
617
- takes the hand and the keys (a slider). It fades with the mode; wiping, line and grip brighten. */
618
- .knobs-cmp-handle{cursor:ew-resize;position:absolute;left:-12px;top:0;width:24px;height:0;opacity:0;pointer-events:none;outline:none;will-change:transform;transition:opacity .18s cubic-bezier(.4,0,1,1)}
619
- .knobs-cmp-handle[data-on]{opacity:1;pointer-events:auto;transition:opacity .22s cubic-bezier(.2,.8,.2,1)}
620
+ /* Fusing, the mark folds away and the label is its name. */
621
+ .knobs-cmp-mark{flex:none;display:flex;justify-content:flex-end;overflow:hidden}
622
+ .knobs-cmp-mark svg{display:block;flex:none;width:12px;height:12px}
623
+ /* The fuse's seam: a blurred strip over where the sides meet, soft at both sides. */
624
+ .knobs-cmp-seam{position:absolute;left:0;top:0;width:8px;height:0;opacity:0;pointer-events:none;will-change:transform,opacity;
625
+ -webkit-mask-image:linear-gradient(to right,transparent,#000 35%,#000 65%,transparent);mask-image:linear-gradient(to right,transparent,#000 35%,#000 65%,transparent)}
626
+ /* The wipe: a hairline down the line with a raised grip in its middle (at the pointer's height
627
+ while dragged); a wider strip takes the hand and the keys (a slider). It fades in with the
628
+ fuse; wiping, line and grip brighten. */
629
+ .knobs-cmp-handle{cursor:ew-resize;position:absolute;left:-12px;top:0;width:24px;height:0;opacity:0;pointer-events:none;outline:none;will-change:transform,opacity}
630
+ .knobs-cmp-handle[data-on]{pointer-events:auto}
620
631
  .knobs-cmp-handle::before{content:"";position:absolute;top:0;bottom:0;left:11.5px;width:1px;background:color-mix(in srgb,var(--_ku-cmp-line) 72%,transparent);box-shadow:0 0 0 .5px rgba(0,0,0,.28),0 0 6px rgba(0,0,0,.25)}
621
632
  .knobs-compare[data-dragging] .knobs-cmp-handle::before{background:var(--_ku-cmp-line)}
622
633
  .knobs-cmp-grip{position:absolute;top:0;left:4px;width:16px;height:28px;border-radius:min(8px,calc(var(--_k-radius) * 999));background:var(--_k-bg-overlay);
@@ -648,6 +659,27 @@ var compareCss = `
648
659
  @media(prefers-reduced-motion:reduce){:is(.knobs-compare,.knobs-cmp-strip) :where(*,::before,::after){transition-duration:.12s!important}}
649
660
  `;
650
661
 
662
+ // src/ui/player-css.ts
663
+ var playerCss = `
664
+ :where(.knobs-player,.knobs-player *){box-sizing:border-box}
665
+ .knobs-ui.knobs-player{--_kp-stage:var(--_k-compare-stage);display:flex;flex-direction:column;gap:6px;width:100%;outline:none;
666
+ font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
667
+ :where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-ui.knobs-player,.knobs-ui.knobs-player[data-nested]{--_kp-stage:var(--k-compare-stage,transparent)}
668
+ .knobs-player-frame{position:relative;width:100%;aspect-ratio:var(--kp-aspect,16/9);overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_kp-stage);cursor:pointer}
669
+ .knobs-player[data-frame="flat"] .knobs-player-frame{border-radius:0}
670
+ .knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
671
+ opacity:0;filter:blur(4px);transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
672
+ .knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}
673
+ .knobs-player-glyph{position:absolute;left:50%;top:50%;display:grid;place-items:center;width:44px;height:44px;margin:-22px 0 0 -22px;border-radius:50%;color:var(--_k-fg);pointer-events:none;
674
+ opacity:0;filter:blur(4px);scale:.96;
675
+ background:color-mix(in srgb,var(--_k-bg) 52%,transparent);-webkit-backdrop-filter:blur(14px) saturate(1.6);backdrop-filter:blur(14px) saturate(1.6);
676
+ box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim);
677
+ transition:opacity calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy),filter calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy),scale calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy)}
678
+ .knobs-player-glyph svg{width:16px;height:16px}
679
+ .knobs-player-glyph[data-flash]{opacity:1;filter:none;scale:1}
680
+ .knobs-player-line{flex:none;min-height:0}
681
+ `;
682
+
651
683
  // src/ui/overlay-css.ts
652
684
  var overlayCss = `
653
685
  .knobs-layers{position:fixed;inset:0;z-index:2147480005;pointer-events:none}
@@ -1512,7 +1544,7 @@ var init_schema = __esm(() => {
1512
1544
  type: "length",
1513
1545
  group: "Type",
1514
1546
  source: value("20px"),
1515
- doc: "Section headings (a PR's title, under its kicker), weight 500."
1547
+ doc: "A page's title, weight 500."
1516
1548
  },
1517
1549
  {
1518
1550
  name: "leading-heading",
@@ -1528,7 +1560,7 @@ var init_schema = __esm(() => {
1528
1560
  type: "length",
1529
1561
  group: "Type",
1530
1562
  source: value("16px"),
1531
- doc: "A page's title, weight 500."
1563
+ doc: "Section titles (under their kicker), weight 500."
1532
1564
  },
1533
1565
  {
1534
1566
  name: "leading-title",
@@ -1560,7 +1592,7 @@ var init_schema = __esm(() => {
1560
1592
  type: "length",
1561
1593
  group: "Type",
1562
1594
  source: value("12px"),
1563
- doc: "Labels over media, weight 500; mono inside body text."
1595
+ doc: "Mono inside body text: code and paths."
1564
1596
  },
1565
1597
  {
1566
1598
  name: "leading-label",
@@ -3082,8 +3114,6 @@ var css2 = `
3082
3114
  .knobs-tl-hover{position:absolute;left:0;top:0;bottom:0;width:1px;margin-left:-.5px;background:var(--_k-border-strong);transform:translateX(-100px);pointer-events:none;opacity:0;transition:opacity .15s}
3083
3115
  .knobs-tl-track:hover .knobs-tl-hover{opacity:1}
3084
3116
  .knobs-timeline[data-scrubbing] .knobs-tl-hover{opacity:0}
3085
- @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
3086
- @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
3087
3117
  .knobs-tl-tag{position:absolute;left:0;bottom:2px;font-size:var(--_k-text-meta);line-height:16px;color:var(--_k-fg);font-variant-numeric:tabular-nums;font-feature-settings:"tnum";white-space:nowrap;pointer-events:none;opacity:0}
3088
3118
  .knobs-tl-tag[data-on]{opacity:1}
3089
3119
  /* The lane (variant: "lane"): one slim track of ticks under a compare or a video; markers are
@@ -3099,7 +3129,26 @@ var css2 = `
3099
3129
  .knobs-timeline[data-variant="lane"] .knobs-tl-head::before{top:0}
3100
3130
  .knobs-timeline[data-variant="lane"] .knobs-tl-head::after{content:none}
3101
3131
  .knobs-timeline[data-variant="lane"] .knobs-tl-tag{top:2px;bottom:auto}
3102
- `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + selectionPillCss) + rolling_css_default, RESTYLED = ".knobs-ui,.knobs-panel,.knobs-chip,.knobs-notice", lookDefaults, tune = (s, m) => {
3132
+ /* The line (variant: "line"): a player's progress under its media. One 2px line, fg-3 with fg
3133
+ filling it, a segment per chapter; it thickens under the pointer and while scrubbing. The
3134
+ chapter and time under the pointer float over it; nothing else shows. */
3135
+ .knobs-ui.knobs-timeline[data-variant="line"]{height:auto;min-height:0;padding:0;gap:0}
3136
+ .knobs-timeline[data-variant="line"] .knobs-tl-transport{display:none}
3137
+ .knobs-ui.knobs-timeline[data-variant="line"] .knobs-tl-track{height:16px;overflow:visible;border-radius:var(--_k-radius);background:transparent;box-shadow:none;cursor:pointer}
3138
+ .knobs-timeline[data-variant="line"] :is(.knobs-tl-ruler,.knobs-tl-lane,.knobs-tl-hover,.knobs-tl-head,.knobs-tl-rlabel){display:none}
3139
+ .knobs-tl-segs{position:absolute;left:0;right:0;top:50%;height:2px;margin-top:-1px;pointer-events:none;transition:scale calc(var(--_k-hover-out) * 1ms) var(--_ku-ease)}
3140
+ .knobs-tl-seg{position:absolute;left:0;top:0;height:100%;overflow:hidden;border-radius:1px;background:var(--_k-fg-3);will-change:transform}
3141
+ .knobs-tl-seg-fill{position:absolute;inset:0;background:var(--_k-fg);transform-origin:0 0;will-change:transform}
3142
+ .knobs-timeline[data-variant="line"] .knobs-tl-track:hover .knobs-tl-segs,.knobs-timeline[data-variant="line"][data-scrubbing] .knobs-tl-segs{scale:1 2}
3143
+ .knobs-timeline[data-variant="line"] .knobs-tl-range{top:3px;bottom:3px;border-radius:3px}
3144
+ .knobs-timeline[data-variant="line"]:focus-visible .knobs-tl-track,.knobs-timeline[data-variant="line"][data-keyboard]:focus .knobs-tl-track{background:transparent;box-shadow:0 0 0 2px color-mix(in srgb,var(--_k-accent) 60%,transparent)}
3145
+ .knobs-timeline[data-variant="line"] .knobs-tl-tag{top:auto;bottom:calc(100% + 2px);display:flex;gap:6px;padding:0 6px;border-radius:calc(var(--_k-radius) * 999);background:color-mix(in srgb,var(--_k-bg) 72%,transparent);
3146
+ -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:inset 0 0 0 .5px var(--_k-border)}
3147
+ .knobs-tl-tag-label{color:var(--_k-fg-2)}
3148
+ .knobs-tl-tag-label:empty{display:none}
3149
+ @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
3150
+ @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
3151
+ `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + playerCss + selectionPillCss) + rolling_css_default, RESTYLED = ".knobs-ui,.knobs-panel,.knobs-chip,.knobs-notice", lookDefaults, tune = (s, m) => {
3103
3152
  s.omega = m.omega;
3104
3153
  s.zeta = m.zeta;
3105
3154
  return s;
@@ -5478,6 +5527,17 @@ function currentMarker(markers, t, fps) {
5478
5527
  best = m;
5479
5528
  return best;
5480
5529
  }
5530
+ function minutes(t) {
5531
+ const s = Math.max(0, Math.floor(t + 0.000001));
5532
+ const h = Math.floor(s / 3600);
5533
+ const m = Math.floor(s % 3600 / 60);
5534
+ const ss = String(s % 60).padStart(2, "0");
5535
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
5536
+ }
5537
+ function chapterBounds(times, duration) {
5538
+ const inside = times.filter((t) => t > 0.000001 && t < duration - 0.000001);
5539
+ return [0, ...[...new Set(inside)].sort((a, b) => a - b), duration];
5540
+ }
5481
5541
  var SECOND_STEPS, FRAME_STEPS, toFrame = (t, fps) => Math.round(t * fps), frameTime = (t, fps) => Math.round(t * fps) / fps, round = (t) => Math.round(t * 1e6) / 1e6, CATCH_UP;
5482
5542
  var init_timeline_model = __esm(() => {
5483
5543
  SECOND_STEPS = [0.1, 0.2, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600];
@@ -5495,7 +5555,8 @@ function timeline(container, options) {
5495
5555
  let speed = options.speed ?? 1;
5496
5556
  let repeat = options.repeat ?? true;
5497
5557
  const arrows = options.arrows ?? "frames";
5498
- const lane = options.variant === "lane";
5558
+ const line = options.variant === "line";
5559
+ const lane = line || options.variant === "lane";
5499
5560
  let time = Math.min(duration, Math.max(0, options.time ?? 0));
5500
5561
  let playing = false;
5501
5562
  let markers = [...options.markers ?? []];
@@ -5530,7 +5591,7 @@ function timeline(container, options) {
5530
5591
  root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight , . Home End [ ] I O M");
5531
5592
  applyLook2(root, options);
5532
5593
  if (lane)
5533
- root.dataset.variant = "lane";
5594
+ root.dataset.variant = line ? "line" : "lane";
5534
5595
  const transport = el("div", "knobs-tl-transport");
5535
5596
  const playBtn = el("button", "knobs-tl-play");
5536
5597
  playBtn.type = "button";
@@ -5543,6 +5604,10 @@ function timeline(container, options) {
5543
5604
  const hover = el("div", "knobs-tl-hover");
5544
5605
  const tag = el("span", "knobs-tl-tag");
5545
5606
  tag.setAttribute("aria-hidden", "true");
5607
+ const tagLabel = el("span", "knobs-tl-tag-label");
5608
+ const tagTime = el("span", "knobs-tl-tag-time");
5609
+ tag.append(tagLabel, tagTime);
5610
+ const segs = el("div", "knobs-tl-segs");
5546
5611
  const head = el("div", "knobs-tl-head");
5547
5612
  head.id = `${id}-head`;
5548
5613
  head.setAttribute("role", "slider");
@@ -5550,11 +5615,13 @@ function timeline(container, options) {
5550
5615
  head.setAttribute("aria-valuemin", "0");
5551
5616
  head.setAttribute("aria-valuemax", String(duration));
5552
5617
  track.append(canvas, markerLane, hover, head, tag);
5618
+ if (line)
5619
+ track.insertBefore(segs, markerLane);
5553
5620
  root.append(transport, track);
5554
5621
  container.append(root);
5555
5622
  let width = 1;
5556
5623
  let height = 1;
5557
- const fitView = () => clampView({ start: 0, span: duration }, duration, { pad: 0.02 });
5624
+ const fitView = () => clampView({ start: 0, span: duration }, duration, { pad: line ? 0 : 0.02 });
5558
5625
  let target = fitView();
5559
5626
  const vStart = new Spring(target.start, INSTANT.omega, 1, 0.00001);
5560
5627
  const vSpan = new Spring(target.span, INSTANT.omega, 1, 0.00001);
@@ -5563,13 +5630,13 @@ function timeline(container, options) {
5563
5630
  const xOf = (t) => (t - vStart.x) * pxPerSec();
5564
5631
  const tOf = (x) => vStart.x + x / pxPerSec();
5565
5632
  const minSpan = () => Math.max(4 / fps, 0.05);
5566
- const retarget = (next, motion = INSTANT) => {
5567
- target = clampView(next, duration, { pad: 0.04, minSpan: minSpan() });
5633
+ const retarget = (next, motion = INSTANT, hand = false) => {
5634
+ target = clampView(next, duration, { pad: line ? 0 : 0.04, minSpan: minSpan() });
5568
5635
  for (const s of [vStart, vSpan]) {
5569
5636
  s.omega = motion.omega;
5570
5637
  s.zeta = motion.zeta;
5571
5638
  }
5572
- if (reducedMotion(win) && motion === CALM) {
5639
+ if (hand || reducedMotion(win) && motion === CALM) {
5573
5640
  vStart.snap(target.start);
5574
5641
  vSpan.snap(target.span);
5575
5642
  } else {
@@ -5595,6 +5662,8 @@ function timeline(container, options) {
5595
5662
  };
5596
5663
  let drawn = "";
5597
5664
  const drawRuler = () => {
5665
+ if (line)
5666
+ return;
5598
5667
  const v = view();
5599
5668
  const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${height}:${units}:${fps}`;
5600
5669
  if (key === drawn)
@@ -5682,6 +5751,8 @@ function timeline(container, options) {
5682
5751
  n.title = `${m.label ? `${m.label} · ` : ""}${clock(m.time)}`;
5683
5752
  }
5684
5753
  placeMarkers();
5754
+ if (line)
5755
+ syncSegments();
5685
5756
  };
5686
5757
  const placeMarkers = () => {
5687
5758
  for (const m of markers) {
@@ -5690,6 +5761,36 @@ function timeline(container, options) {
5690
5761
  n.style.transform = `translateX(${xOf(m.time).toFixed(2)}px)`;
5691
5762
  }
5692
5763
  };
5764
+ let bounds = [];
5765
+ const segEls = [];
5766
+ const syncSegments = () => {
5767
+ bounds = chapterBounds(markers.map((m) => m.time), duration);
5768
+ while (segEls.length > bounds.length - 1)
5769
+ segEls.pop().el.remove();
5770
+ while (segEls.length < bounds.length - 1) {
5771
+ const n = el("i", "knobs-tl-seg");
5772
+ const fill = el("b", "knobs-tl-seg-fill");
5773
+ n.append(fill);
5774
+ segs.append(n);
5775
+ segEls.push({ el: n, fill });
5776
+ }
5777
+ placeSegments();
5778
+ };
5779
+ const placeSegments = () => {
5780
+ if (!line)
5781
+ return;
5782
+ const last = segEls.length - 1;
5783
+ segEls.forEach((s, i) => {
5784
+ const a = bounds[i];
5785
+ const b = bounds[i + 1];
5786
+ const x0 = xOf(a) + (i > 0 ? SEG_GAP / 2 : 0);
5787
+ const x1 = xOf(b) - (i < last ? SEG_GAP / 2 : 0);
5788
+ s.el.style.transform = `translateX(${x0.toFixed(2)}px)`;
5789
+ s.el.style.width = `${Math.max(0, x1 - x0).toFixed(2)}px`;
5790
+ const k = Math.min(1, Math.max(0, (shownHead - a) / Math.max(0.000000001, b - a)));
5791
+ s.fill.style.transform = `scaleX(${k.toFixed(4)})`;
5792
+ });
5793
+ };
5693
5794
  const syncRanges = () => {
5694
5795
  const keep = new Set(ranges.map((r) => r.id));
5695
5796
  for (const [k, n] of rangeEls)
@@ -5706,7 +5807,7 @@ function timeline(container, options) {
5706
5807
  b.dataset.edge = "end";
5707
5808
  const label = el("span", "knobs-tl-rlabel");
5708
5809
  n.append(label, a, b);
5709
- track.insertBefore(n, markerLane);
5810
+ track.insertBefore(n, line ? segs : markerLane);
5710
5811
  rangeEls.set(r.id, n);
5711
5812
  if (built)
5712
5813
  enter(n);
@@ -5758,6 +5859,7 @@ function timeline(container, options) {
5758
5859
  let currentId = null;
5759
5860
  const renderHead = () => {
5760
5861
  head.style.transform = `translateX(${xOf(shownHead).toFixed(2)}px)`;
5862
+ placeSegments();
5761
5863
  head.setAttribute("aria-valuenow", time.toFixed(3));
5762
5864
  head.setAttribute("aria-valuetext", `${time.toFixed(2)} seconds, frame ${toFrame(time, fps)}`);
5763
5865
  const cur = currentMarker(markers, time, fps)?.id ?? null;
@@ -6021,14 +6123,25 @@ function timeline(container, options) {
6021
6123
  setTime(s.t, "scrub", s.shown, press);
6022
6124
  root.toggleAttribute("data-stuck", s.stop !== null);
6023
6125
  };
6126
+ let pointerX = null;
6127
+ const zoomAtHand = (factor) => zoom(factor, pointerX === null ? time : target.start + pointerX / width * target.span);
6024
6128
  const showTag = (x) => {
6025
6129
  const at = Math.min(duration, Math.max(0, tOf(x)));
6026
- const text = `${at.toFixed(2)} s`;
6027
- if (tag.textContent !== text)
6028
- tag.textContent = text;
6130
+ const text = line ? minutes(at) : `${at.toFixed(2)} s`;
6131
+ if (tagTime.textContent !== text)
6132
+ tagTime.textContent = text;
6133
+ const label = line ? currentMarker(markers, at, fps)?.label ?? "" : "";
6134
+ if (tagLabel.textContent !== label)
6135
+ tagLabel.textContent = label;
6029
6136
  const cx = Math.min(width, Math.max(0, x));
6030
- const flip = cx > width - tag.offsetWidth - 12;
6031
- tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
6137
+ if (line) {
6138
+ const w = tag.offsetWidth;
6139
+ const left = Math.min(Math.max(0, width - w), Math.max(0, cx - w / 2));
6140
+ tag.style.transform = `translateX(${left.toFixed(1)}px)`;
6141
+ } else {
6142
+ const flip = cx > width - tag.offsetWidth - 12;
6143
+ tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
6144
+ }
6032
6145
  tag.dataset.on = "";
6033
6146
  };
6034
6147
  const hideTag = () => delete tag.dataset.on;
@@ -6089,6 +6202,7 @@ function timeline(container, options) {
6089
6202
  });
6090
6203
  track.addEventListener("pointermove", (e) => {
6091
6204
  const x = localX(e);
6205
+ pointerX = x;
6092
6206
  hover.style.transform = `translateX(${x.toFixed(1)}px)`;
6093
6207
  if (!drag || drag.kind === "scrub")
6094
6208
  showTag(x);
@@ -6162,6 +6276,7 @@ function timeline(container, options) {
6162
6276
  track.addEventListener("pointercancel", endDrag);
6163
6277
  track.addEventListener("lostpointercapture", endDrag);
6164
6278
  track.addEventListener("pointerleave", () => {
6279
+ pointerX = null;
6165
6280
  if (drag)
6166
6281
  return;
6167
6282
  hover.style.transform = "translateX(-100px)";
@@ -6169,7 +6284,7 @@ function timeline(container, options) {
6169
6284
  });
6170
6285
  track.addEventListener("dblclick", (e) => {
6171
6286
  if (lane)
6172
- return;
6287
+ return fit();
6173
6288
  const markerEl = e.target.closest(".knobs-tl-marker");
6174
6289
  if (markerEl)
6175
6290
  return rename(markerEl.dataset.id);
@@ -6180,18 +6295,40 @@ function timeline(container, options) {
6180
6295
  rename(m.id);
6181
6296
  });
6182
6297
  track.addEventListener("wheel", (e) => {
6183
- if (lane)
6298
+ const zooming = e.ctrlKey || e.metaKey;
6299
+ if (zooming && pinching)
6300
+ return e.preventDefault();
6301
+ const sideways = Math.abs(e.deltaX) > Math.abs(e.deltaY);
6302
+ if (lane && !zooming && !sideways)
6184
6303
  return;
6185
6304
  e.preventDefault();
6186
6305
  const scale = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? height : 1;
6187
- if (e.ctrlKey || e.metaKey) {
6306
+ if (zooming) {
6188
6307
  const at = target.start + localX(e) / width * target.span;
6189
- retarget(zoomView(target, Math.exp(-e.deltaY * scale * (e.ctrlKey ? 0.01 : 0.002)), at));
6308
+ const k = Math.exp(-e.deltaY * scale * (e.ctrlKey ? 0.01 : 0.002));
6309
+ retarget(zoomView(target, k, at), INSTANT, lane);
6190
6310
  } else {
6191
- const d = (Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY) * scale;
6192
- retarget({ ...target, start: target.start + d / width * target.span });
6311
+ const d = (sideways ? e.deltaX : e.deltaY) * scale;
6312
+ retarget({ ...target, start: target.start + d / width * target.span }, INSTANT, lane);
6193
6313
  }
6194
6314
  }, { passive: false });
6315
+ let pinchScale = 1;
6316
+ let pinching = false;
6317
+ const onGesture = (e) => {
6318
+ const g = e;
6319
+ e.preventDefault();
6320
+ if (e.type === "gesturestart")
6321
+ pinchScale = 1;
6322
+ pinching = e.type !== "gestureend";
6323
+ if (e.type !== "gesturechange")
6324
+ return;
6325
+ const s = g.scale ?? 1;
6326
+ const x = g.clientX !== undefined ? localX({ clientX: g.clientX }) : width / 2;
6327
+ retarget(zoomView(target, s / pinchScale, target.start + x / width * target.span), INSTANT, true);
6328
+ pinchScale = s;
6329
+ };
6330
+ for (const type of ["gesturestart", "gesturechange", "gestureend"])
6331
+ track.addEventListener(type, onGesture);
6195
6332
  root.addEventListener("keydown", (e) => {
6196
6333
  if (e.target !== root || e.metaKey || e.ctrlKey) {
6197
6334
  if ((e.metaKey || e.ctrlKey) && (e.key === "=" || e.key === "-" || e.key === "0") && e.target === root) {
@@ -6199,7 +6336,7 @@ function timeline(container, options) {
6199
6336
  if (e.key === "0")
6200
6337
  fit();
6201
6338
  else
6202
- zoom(e.key === "=" ? 1.5 : 1 / 1.5);
6339
+ zoomAtHand(e.key === "=" ? 1.5 : 1 / 1.5);
6203
6340
  }
6204
6341
  return;
6205
6342
  }
@@ -6281,18 +6418,12 @@ function timeline(container, options) {
6281
6418
  return;
6282
6419
  case "=":
6283
6420
  case "+":
6284
- if (lane)
6285
- return;
6286
6421
  claim();
6287
- return zoom(1.5);
6422
+ return zoomAtHand(1.5);
6288
6423
  case "-":
6289
- if (lane)
6290
- return;
6291
6424
  claim();
6292
- return zoom(1 / 1.5);
6425
+ return zoomAtHand(1 / 1.5);
6293
6426
  case "0":
6294
- if (lane)
6295
- return;
6296
6427
  claim();
6297
6428
  return fit();
6298
6429
  }
@@ -6508,7 +6639,7 @@ function timeline(container, options) {
6508
6639
  };
6509
6640
  return api;
6510
6641
  }
6511
- var RULER = 22, INSTANT, CALM, GLIDE, uid2 = 0, PLAY = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.6v8.8c0 .5.5.8.9.5l6.6-4.4c.4-.3.4-.8 0-1L5.9 3.1c-.4-.3-.9 0-.9.5Z" fill="currentColor"/></svg>', PAUSE = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="4" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/><rect x="9.2" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/></svg>';
6642
+ var RULER = 22, SEG_GAP = 3, INSTANT, CALM, GLIDE, uid2 = 0, PLAY = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.6v8.8c0 .5.5.8.9.5l6.6-4.4c.4-.3.4-.8 0-1L5.9 3.1c-.4-.3-.9 0-.9.5Z" fill="currentColor"/></svg>', PAUSE = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="4" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/><rect x="9.2" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/></svg>';
6512
6643
  var init_timeline = __esm(() => {
6513
6644
  init_rgba();
6514
6645
  init_motion();
@@ -6580,15 +6711,28 @@ function compareLayout(box, before, after) {
6580
6711
  const shape = after.w > 0 && after.h > 0 ? after : before;
6581
6712
  const pair = letterbox({ w: shape.w * 2, h: shape.h }, box);
6582
6713
  const half = pair.w / 2;
6714
+ const fused = { x: pair.x + half / 2, y: pair.y, w: half, h: pair.h };
6583
6715
  return {
6584
6716
  side: {
6585
6717
  before: letterbox(before, { x: pair.x, y: pair.y, w: half, h: pair.h }),
6586
6718
  after: letterbox(after, { x: pair.x + half, y: pair.y, w: half, h: pair.h }),
6587
6719
  split: pair.x + half
6588
6720
  },
6589
- overlay: { before: letterbox(before, box), after: letterbox(after, box) }
6721
+ overlay: { before: letterbox(before, fused), after: letterbox(after, fused) }
6590
6722
  };
6591
6723
  }
6724
+ function fuseFrame(L, before, after, k, wipe) {
6725
+ const t = Math.min(1, Math.max(0, k));
6726
+ const o = L.overlay;
6727
+ const left = Math.min(o.before.x, o.after.x);
6728
+ const line = wipeX({ x: left, y: 0, w: Math.max(o.before.x + o.before.w, o.after.x + o.after.w) - left, h: 0 }, wipe);
6729
+ const cut = L.side.split + (line - L.side.split) * t;
6730
+ const contact = Math.max(0, Math.min(cut - after.x, before.x + before.w - cut));
6731
+ const feather = Math.min(contact, 1 + FUSE.feather * Math.sin(Math.PI * t));
6732
+ const overlap = before.x + before.w - after.x;
6733
+ const seam = t > 0.001 && overlap >= 0 ? Math.min(1, Math.max(0, overlap / FUSE.seamFade)) * Math.min(1, t * 4) : 0;
6734
+ return { cut, feather, seam, blur: t < 0.995 ? FUSE.blur * (1 - t) : 0 };
6735
+ }
6592
6736
  function clampZoom(r) {
6593
6737
  const w = Math.min(1, Math.max(1 / ZOOM_MAX, r.w));
6594
6738
  const u = Math.min(1 - w, Math.max(0, r.u));
@@ -6674,7 +6818,15 @@ function stripPeaks(diffs, o = {}) {
6674
6818
  });
6675
6819
  return out;
6676
6820
  }
6677
- 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), seekTime = (t, fps, duration) => frameMidpoint(frameOf(t, fps), fps, duration), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, splitHeight = (w, media) => media.w > 0 && media.h > 0 ? w / 2 * (media.h / media.w) : w / 2 * (9 / 16), ZOOM_FIT, ZOOM_MAX = 16, panBy = (r, dx, dy, base) => clampZoom({
6821
+ 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), seekTime = (t, fps, duration) => frameMidpoint(frameOf(t, fps), fps, duration), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, FUSE, labelFold = (fold, mark, blur = 3) => {
6822
+ const f = Math.min(1, Math.max(0, fold));
6823
+ const open = 1 - f;
6824
+ return {
6825
+ width: mark * open,
6826
+ opacity: Math.min(1, Math.max(0, (open - 0.2) / 0.8)),
6827
+ blur: blur * Math.min(1, Math.max(0, (f - 0.25) / 0.75))
6828
+ };
6829
+ }, splitHeight = (w, media) => media.w > 0 && media.h > 0 ? w / 2 * (media.h / media.w) : w / 2 * (9 / 16), ZOOM_FIT, ZOOM_MAX = 16, panBy = (r, dx, dy, base) => clampZoom({
6678
6830
  u: r.u - dx / Math.max(1, base.w) * r.w,
6679
6831
  v: r.v - dy / Math.max(1, base.h) * r.h,
6680
6832
  w: r.w
@@ -6697,6 +6849,7 @@ var init_compare_model = __esm(() => {
6697
6849
  MODE_KEYS = { side: "s", wipe: "w", difference: "d" };
6698
6850
  VIDEO = /\.(mp4|m4v|mov|webm|ogv|mkv)$/i;
6699
6851
  SEEK_IDLE = { seeking: false, pending: null };
6852
+ FUSE = { feather: 24, blur: 4, seamFade: 48 };
6700
6853
  ZOOM_FIT = { u: 0, v: 0, w: 1, h: 1 };
6701
6854
  });
6702
6855
 
@@ -7591,11 +7744,6 @@ fn pm(c: vec4f) -> vec4f { return vec4f(c.rgb * c.a, c.a); }
7591
7744
 
7592
7745
  fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
7593
7746
  switch mode {
7594
- case 1u: {
7595
- let s = max(u.wipe.y, 0.5);
7596
- let m = smoothstep(u.wipe.x - s, u.wipe.x + s, p.x);
7597
- return pm(vec4f(mix(a.rgb, b.rgb, m), mix(a.a, b.a, m)));
7598
- }
7599
7747
  case 2u: {
7600
7748
  let d = abs(a.rgb - b.rgb);
7601
7749
  let h = smoothstep(u.diff.x, u.diff.x + 0.06, max(d.r, max(d.g, d.b)) * u.diff.y) * min(a.a, b.a);
@@ -7604,8 +7752,10 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
7604
7752
  }
7605
7753
  default: {}
7606
7754
  }
7607
- // Side by side: each side in its own place, Before over After where they meet.
7608
- return vec4f(a.rgb * a.a + b.rgb * b.a * (1.0 - a.a), a.a + b.a * (1.0 - a.a));
7755
+ // Side by side, fusing and wiping: Before over After, cut at the seam with its feather.
7756
+ let s = max(u.wipe.y, 0.5);
7757
+ let ka = a.a * (1.0 - smoothstep(u.wipe.x - s, u.wipe.x + s, p.x));
7758
+ return vec4f(a.rgb * ka + b.rgb * b.a * (1.0 - ka), ka + b.a * (1.0 - ka));
7609
7759
  }
7610
7760
 
7611
7761
  @fragment fn fs(@builtin(position) q: vec4f) -> @location(0) vec4f {
@@ -8007,6 +8157,196 @@ var init_compare_strip = __esm(() => {
8007
8157
  DIFF = { w: 32, h: 18 };
8008
8158
  });
8009
8159
 
8160
+ // src/ui/player.ts
8161
+ function player(container, options) {
8162
+ const doc = container.ownerDocument;
8163
+ const el = (tag, cls) => {
8164
+ const n = doc.createElement(tag);
8165
+ n.className = cls;
8166
+ return n;
8167
+ };
8168
+ const look = {
8169
+ ...options.preset ? { preset: options.preset } : {},
8170
+ ...options.theme ? { theme: options.theme } : {},
8171
+ ...options.appearance ? { appearance: options.appearance } : {}
8172
+ };
8173
+ const root = el("div", "knobs-player");
8174
+ root.setAttribute("role", "group");
8175
+ root.setAttribute("aria-label", options.label ?? "Video");
8176
+ applyLook2(root, options);
8177
+ const frame = el("div", "knobs-player-frame");
8178
+ frame.style.setProperty("--kp-aspect", String(options.aspect ?? 16 / 9));
8179
+ const v = el("video", "knobs-player-media");
8180
+ v.muted = options.muted ?? true;
8181
+ v.defaultMuted = v.muted;
8182
+ v.playsInline = true;
8183
+ v.preload = "metadata";
8184
+ v.setAttribute("playsinline", "");
8185
+ if (options.poster)
8186
+ v.poster = options.poster;
8187
+ if (options.captions) {
8188
+ const track = doc.createElement("track");
8189
+ track.kind = "captions";
8190
+ track.label = "Captions";
8191
+ track.srclang = doc.documentElement.lang || "en";
8192
+ track.default = true;
8193
+ track.src = options.captions;
8194
+ v.append(track);
8195
+ }
8196
+ const glyph = playGlyph(doc);
8197
+ frame.append(v, glyph.element);
8198
+ const host = el("div", "knobs-player-line");
8199
+ root.append(frame, host);
8200
+ container.append(root);
8201
+ const markers = (options.chapters ?? []).map((c, i) => ({
8202
+ id: c.id ?? `c${i}`,
8203
+ time: c.start,
8204
+ label: c.label
8205
+ }));
8206
+ const tl = timeline(host, {
8207
+ ...look,
8208
+ variant: "line",
8209
+ duration: 1,
8210
+ fps: options.fps ?? 30,
8211
+ markers,
8212
+ repeat: options.loop ?? true,
8213
+ arrows: { step: 5, shift: 10 },
8214
+ label: options.label ? `${options.label} time` : "Video time",
8215
+ ...options.hints === false ? { hints: false } : {}
8216
+ });
8217
+ const onFrame = () => root.setAttribute("data-ready", "");
8218
+ let pending = null;
8219
+ const seekVideo = (t) => {
8220
+ if (v.readyState < 1)
8221
+ return;
8222
+ if (v.seeking)
8223
+ pending = t;
8224
+ else
8225
+ v.currentTime = t;
8226
+ };
8227
+ let reported = 0;
8228
+ const onSeeked = () => {
8229
+ if (pending !== null) {
8230
+ const t = pending;
8231
+ pending = null;
8232
+ v.currentTime = t;
8233
+ return;
8234
+ }
8235
+ if (!v.paused || tl.playing || Math.abs(v.currentTime - reported) < 0.1)
8236
+ return;
8237
+ reported = v.currentTime;
8238
+ options.onSeek?.(reported);
8239
+ };
8240
+ const onMeta = () => {
8241
+ onFrame();
8242
+ if (v.videoWidth)
8243
+ frame.style.setProperty("--kp-aspect", String(v.videoWidth / v.videoHeight));
8244
+ if (Number.isFinite(v.duration))
8245
+ tl.update({ duration: v.duration });
8246
+ };
8247
+ v.addEventListener("loadedmetadata", onMeta);
8248
+ v.addEventListener("loadeddata", onFrame);
8249
+ v.addEventListener("seeked", onSeeked);
8250
+ const unlisten = tl.listen({
8251
+ onSeek(t, source) {
8252
+ if (source === "play") {
8253
+ if (Math.abs(v.currentTime - t) > 0.25)
8254
+ v.currentTime = t;
8255
+ return;
8256
+ }
8257
+ if (Math.abs(v.currentTime - t) < 1 / (options.fps ?? 30))
8258
+ return;
8259
+ seekVideo(t);
8260
+ },
8261
+ onPlay(on) {
8262
+ if (on)
8263
+ v.play().catch(() => tl.pause());
8264
+ else {
8265
+ v.pause();
8266
+ reported = tl.time;
8267
+ }
8268
+ root.toggleAttribute("data-playing", on);
8269
+ options.onPlay?.(on, tl.time);
8270
+ },
8271
+ onScrub(on) {
8272
+ if (on)
8273
+ v.pause();
8274
+ }
8275
+ });
8276
+ tl.drive(() => {
8277
+ if (v.readyState < 1)
8278
+ return Number.NaN;
8279
+ if (v.seeking)
8280
+ return tl.time;
8281
+ return v.ended ? Number.POSITIVE_INFINITY : v.currentTime;
8282
+ });
8283
+ const onClick = (e) => {
8284
+ if (e.button !== 0)
8285
+ return;
8286
+ tl.toggle();
8287
+ glyph.flash(tl.playing);
8288
+ tl.focus();
8289
+ };
8290
+ frame.addEventListener("click", onClick);
8291
+ v.src = options.src;
8292
+ const api = {
8293
+ element: root,
8294
+ video: v,
8295
+ timeline: tl,
8296
+ get time() {
8297
+ return tl.time;
8298
+ },
8299
+ get playing() {
8300
+ return tl.playing;
8301
+ },
8302
+ get duration() {
8303
+ return Number.isFinite(v.duration) ? v.duration : 0;
8304
+ },
8305
+ play: () => tl.play(),
8306
+ pause: () => tl.pause(),
8307
+ toggle: () => tl.toggle(),
8308
+ seek: (t, o) => tl.seek(t, o?.glide ? { glide: true } : undefined),
8309
+ seekBy: (d) => tl.seek(Math.min(tl.duration, Math.max(0, tl.time + d)), { glide: true }),
8310
+ focus: () => tl.focus(),
8311
+ dispose() {
8312
+ unlisten();
8313
+ glyph.dispose();
8314
+ tl.drive(null);
8315
+ tl.dispose();
8316
+ v.pause();
8317
+ v.removeEventListener("loadedmetadata", onMeta);
8318
+ v.removeEventListener("loadeddata", onFrame);
8319
+ v.removeEventListener("seeked", onSeeked);
8320
+ v.removeAttribute("src");
8321
+ v.load();
8322
+ root.remove();
8323
+ }
8324
+ };
8325
+ return api;
8326
+ }
8327
+ function playGlyph(doc) {
8328
+ const element = doc.createElement("span");
8329
+ element.className = "knobs-player-glyph";
8330
+ element.setAttribute("aria-hidden", "true");
8331
+ let timer = 0;
8332
+ const win = doc.defaultView;
8333
+ return {
8334
+ element,
8335
+ flash(playing) {
8336
+ element.innerHTML = playing ? PLAY : PAUSE;
8337
+ element.setAttribute("data-flash", "");
8338
+ win.clearTimeout(timer);
8339
+ timer = win.setTimeout(() => element.removeAttribute("data-flash"), FLASH_MS);
8340
+ },
8341
+ dispose: () => win.clearTimeout(timer)
8342
+ };
8343
+ }
8344
+ var FLASH_MS = 450;
8345
+ var init_player = __esm(() => {
8346
+ init_look();
8347
+ init_timeline();
8348
+ });
8349
+
8010
8350
  // src/ui/compare.ts
8011
8351
  function compare(container, options) {
8012
8352
  const doc = container.ownerDocument;
@@ -8114,6 +8454,8 @@ function compare(container, options) {
8114
8454
  if (name.textContent !== text)
8115
8455
  name.textContent = text;
8116
8456
  };
8457
+ const seam = el("div", "knobs-cmp-seam");
8458
+ seam.setAttribute("aria-hidden", "true");
8117
8459
  const handle = el("div", "knobs-cmp-handle");
8118
8460
  handle.setAttribute("role", "slider");
8119
8461
  handle.setAttribute("aria-label", "Wipe");
@@ -8123,7 +8465,8 @@ function compare(container, options) {
8123
8465
  grip.setAttribute("aria-hidden", "true");
8124
8466
  handle.append(grip);
8125
8467
  const note = el("div", "knobs-cmp-note");
8126
- stage.append(canvas, split, labels[0], labels[1], handle, zoomPill, note);
8468
+ const glyph = playGlyph(doc);
8469
+ stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note, glyph.element);
8127
8470
  root.append(bar, stage);
8128
8471
  const wantsStrip = options.strip === true;
8129
8472
  let tl = bound;
@@ -8138,7 +8481,7 @@ function compare(container, options) {
8138
8481
  root.append(host);
8139
8482
  tl = timeline(host, {
8140
8483
  ...look,
8141
- variant: "lane",
8484
+ variant: "line",
8142
8485
  duration: 1,
8143
8486
  fps: fps(),
8144
8487
  arrows: { step: 0.5, shift: 2 },
@@ -8159,12 +8502,13 @@ function compare(container, options) {
8159
8502
  let toMode = mode;
8160
8503
  const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
8161
8504
  let wipeCatching = false;
8162
- let gripY = null;
8505
+ const gripAt = new Spring(0.5, LAYOUT.omega, LAYOUT.zeta, 0.0001);
8163
8506
  let loupeHeld = false;
8164
8507
  let pointer = null;
8165
8508
  let drag = null;
8166
8509
  const touches = new Map;
8167
8510
  let pinch = null;
8511
+ let press = null;
8168
8512
  const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
8169
8513
  const zv = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
8170
8514
  const zw = new Spring(1, ZOOM.omega, ZOOM.zeta, 0.00001);
@@ -8451,6 +8795,8 @@ function compare(container, options) {
8451
8795
  for (const s of sides) {
8452
8796
  const st = s.el.style;
8453
8797
  st.clipPath = "";
8798
+ st.maskImage = "";
8799
+ st.setProperty("-webkit-mask-image", "");
8454
8800
  st.mixBlendMode = "";
8455
8801
  st.opacity = "";
8456
8802
  s.shown = null;
@@ -8677,43 +9023,65 @@ function compare(container, options) {
8677
9023
  viewSent = key;
8678
9024
  options.onView({ zoom: clampZoom(z), loop: l });
8679
9025
  };
8680
- const labelsIn = (m, r, wr, wx) => {
8681
- if (m === "wipe") {
8682
- const at = seamLabels(wx, wr.x, wr.x + wr.w, labelW[0], labelW[1], INSET);
8683
- return [at.before, at.after];
8684
- }
8685
- const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * lay.x;
8686
- return [
8687
- { x: r.before.x + INSET, opacity: 1 },
8688
- { x: ax, opacity: 1 }
8689
- ];
9026
+ const differenceWeight = () => {
9027
+ const f = Math.min(1, Math.max(0, fade.x));
9028
+ return (fromMode === "difference" ? 1 - f : 0) + (toMode === "difference" ? f : 0);
8690
9029
  };
8691
9030
  const placeGrip = (wr) => {
8692
- const gy = gripTop(gripY ?? wr.y + wr.h / 2, wr, GRIP.h) - wr.y;
9031
+ const gy = gripTop(wr.y + gripAt.x * wr.h, wr, GRIP.h) - wr.y;
8693
9032
  grip.style.transform = `translate3d(0,${gy.toFixed(2)}px,0)`;
8694
9033
  };
9034
+ let folded = 0;
8695
9035
  const draw = () => {
8696
9036
  dirty = false;
9037
+ if (!root.hasAttribute("data-ready") && sides.every((x) => x.shown || x.failed))
9038
+ root.setAttribute("data-ready", "");
8697
9039
  const r = layoutNow();
8698
9040
  const wr = wipeRect(r.L.overlay);
8699
- const wx = wipeX(wr, wipeLine.x);
8700
- const k = lay.x;
8701
- const a = labelsIn(fromMode, r, wr, wx);
8702
- const b = labelsIn(toMode, r, wr, wx);
8703
- const f = Math.min(1, Math.max(0, fade.x));
9041
+ const k = Math.min(1, Math.max(0, lay.x));
9042
+ const fu = fuseFrame(r.L, r.before, r.after, k, wipeLine.x);
9043
+ const dw = differenceWeight();
9044
+ const fusing = 1 - dw;
9045
+ folded = k * fusing;
9046
+ const fold = labelFold(folded, MARK.w);
9047
+ const w = labelW.map((x) => x - (MARK.w - fold.width) - MARK.gap * folded);
9048
+ const ride = seamLabels(fu.cut, r.before.x, r.after.x + r.after.w, w[0], w[1], INSET);
9049
+ const apart = [r.before.x + INSET, r.after.x + INSET];
9050
+ const corner = [r.before.x + INSET, r.after.x + r.after.w - labelW[1] - INSET];
9051
+ const rides = [ride.before, ride.after];
8704
9052
  const ys = [r.before.y + INSET, r.after.y + INSET];
8705
9053
  for (const i of [0, 1]) {
8706
- const x = a[i].x + (b[i].x - a[i].x) * f;
8707
- labels[i].style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
8708
- labels[i].style.opacity = (a[i].opacity + (b[i].opacity - a[i].opacity) * f).toFixed(3);
9054
+ const fused = apart[i] + (rides[i].x - apart[i]) * k;
9055
+ const x = fused + (corner[i] - fused) * dw;
9056
+ const opacity = (1 - k + k * rides[i].opacity) * fusing + dw;
9057
+ const l = labels[i];
9058
+ l.style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
9059
+ l.style.opacity = opacity.toFixed(3);
9060
+ const mark = l.firstElementChild;
9061
+ if (mark) {
9062
+ mark.style.width = `${fold.width.toFixed(2)}px`;
9063
+ mark.style.marginRight = `${(-MARK.gap * folded).toFixed(2)}px`;
9064
+ mark.style.opacity = fold.opacity.toFixed(3);
9065
+ mark.style.filter = fold.blur > 0.05 ? `blur(${fold.blur.toFixed(2)}px)` : "";
9066
+ }
8709
9067
  }
8710
9068
  const top = Math.min(r.L.side.before.y, r.L.side.after.y);
8711
9069
  split.style.transform = `translate(${r.L.side.split.toFixed(2)}px,${top.toFixed(2)}px)`;
8712
9070
  split.style.height = `${Math.max(0, r.L.side.after.h).toFixed(2)}px`;
8713
9071
  split.style.opacity = String(Math.max(0, 1 - k * 2));
9072
+ const seamOn = fu.seam * fusing;
9073
+ const blur = fu.blur > 0.01 ? `blur(${fu.blur.toFixed(2)}px)` : "none";
9074
+ seam.style.transform = `translate(${fu.cut.toFixed(2)}px,${wr.y.toFixed(2)}px) translateX(-50%)`;
9075
+ seam.style.width = `${(8 + 2 * fu.feather).toFixed(2)}px`;
9076
+ seam.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
9077
+ seam.style.opacity = (blur === "none" ? 0 : seamOn).toFixed(3);
9078
+ seam.style.backdropFilter = blur;
9079
+ seam.style.setProperty("-webkit-backdrop-filter", blur);
8714
9080
  handle.toggleAttribute("data-on", mode === "wipe");
8715
- handle.style.transform = `translate(${wx.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
9081
+ handle.style.transform = `translate(${fu.cut.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
8716
9082
  handle.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
9083
+ handle.style.opacity = seamOn.toFixed(3);
9084
+ handle.style.filter = blur;
8717
9085
  placeGrip(wr);
8718
9086
  handle.setAttribute("aria-valuenow", String(Math.round(wipe * 100)));
8719
9087
  handle.setAttribute("aria-valuetext", `${Math.round(wipe * 100)}% Before`);
@@ -8726,24 +9094,27 @@ function compare(container, options) {
8726
9094
  if (pending)
8727
9095
  return;
8728
9096
  if (renderer === "dom") {
8729
- const place = (n, rect, clip, cutAt) => {
9097
+ const cut = mode === "difference" ? null : fu;
9098
+ const place = (n, rect, clip, before) => {
8730
9099
  const st = n.style;
8731
9100
  st.transform = `translate(${rect.x.toFixed(2)}px,${rect.y.toFixed(2)}px)`;
8732
9101
  st.width = `${rect.w.toFixed(2)}px`;
8733
9102
  st.height = `${rect.h.toFixed(2)}px`;
9103
+ const edge = before && cut ? cut.cut + cut.feather : Number.POSITIVE_INFINITY;
8734
9104
  const t = Math.max(0, clip.y - rect.y);
8735
9105
  const l = Math.max(0, clip.x - rect.x);
8736
9106
  const b = Math.max(0, rect.y + rect.h - (clip.y + clip.h));
8737
- const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, cutAt));
9107
+ const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, edge));
8738
9108
  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)` : "";
9109
+ 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)` : "";
9110
+ st.maskImage = mask;
9111
+ st.setProperty("-webkit-mask-image", mask);
8739
9112
  };
8740
- const cut = mode === "wipe" ? wx : Number.POSITIVE_INFINITY;
8741
9113
  for (const s of sides) {
8742
9114
  const rect = s.index === 0 ? r.zBefore : r.zAfter;
8743
9115
  const clip = s.index === 0 ? r.before : r.after;
8744
- const at = s.index === 0 ? cut : Number.POSITIVE_INFINITY;
8745
- place(s.el, rect, clip, at);
8746
- place(s.preview, rect, clip, at);
9116
+ place(s.el, rect, clip, s.index === 0);
9117
+ place(s.preview, rect, clip, s.index === 0);
8747
9118
  if (s.index !== 0)
8748
9119
  continue;
8749
9120
  for (const n of [s.el, s.preview])
@@ -8763,8 +9134,8 @@ function compare(container, options) {
8763
9134
  width: Math.max(1, Math.round(stageW * dpr)),
8764
9135
  height: Math.max(1, Math.round(stageH * dpr)),
8765
9136
  dpr,
8766
- wipeX: wx * dpr,
8767
- soft: 0.75 * dpr,
9137
+ wipeX: fu.cut * dpr,
9138
+ soft: Math.max(0.75, fu.feather) * dpr,
8768
9139
  from: MODE_INDEX[fromMode],
8769
9140
  to: MODE_INDEX[toMode],
8770
9141
  mix: Math.min(1, Math.max(0, fade.x)),
@@ -8791,6 +9162,7 @@ function compare(container, options) {
8791
9162
  zu.step(dt);
8792
9163
  zv.step(dt);
8793
9164
  zw.step(dt);
9165
+ gripAt.step(dt);
8794
9166
  const playing = isPlaying();
8795
9167
  if (playing) {
8796
9168
  const m = master();
@@ -8821,11 +9193,9 @@ function compare(container, options) {
8821
9193
  pushFrame(s);
8822
9194
  }
8823
9195
  }
8824
- const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching;
9196
+ const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching || !gripAt.resting;
8825
9197
  if (moving)
8826
9198
  dirty = true;
8827
- if (auto && !lay.resting)
8828
- fitHeight();
8829
9199
  if (dirty)
8830
9200
  draw();
8831
9201
  return playing || moving;
@@ -8851,13 +9221,7 @@ function compare(container, options) {
8851
9221
  zw.set(c.w);
8852
9222
  requestDraw();
8853
9223
  };
8854
- const autoHeight = (w) => {
8855
- const shape = sizeOf(sides[1]);
8856
- const side = splitHeight(w, shape);
8857
- const over = w * shape.h / shape.w;
8858
- const h = side + (over - side) * lay.x;
8859
- return Math.max(60, Math.min(win.innerHeight * 0.8, h));
8860
- };
9224
+ const autoHeight = (w) => Math.max(60, Math.min(win.innerHeight * 0.8, splitHeight(w, sizeOf(sides[1]))));
8861
9225
  const fitHeight = () => {
8862
9226
  const h = autoHeight(stageW);
8863
9227
  if (Math.abs(h - stageH) < 0.25)
@@ -8869,7 +9233,8 @@ function compare(container, options) {
8869
9233
  const readColors = () => {
8870
9234
  const c = tokenColor(win.getComputedStyle(root), "--_ku-cmp-diff", [0.49, 0.81, 0.61, 1]);
8871
9235
  accent = [c[0], c[1], c[2]];
8872
- labelW = [labels[0].offsetWidth, labels[1].offsetWidth];
9236
+ const unfold = MARK.w - labelFold(folded, MARK.w).width + MARK.gap * folded;
9237
+ labelW = [labels[0].offsetWidth + unfold, labels[1].offsetWidth + unfold];
8873
9238
  requestDraw();
8874
9239
  };
8875
9240
  const measure = () => {
@@ -8960,8 +9325,10 @@ function compare(container, options) {
8960
9325
  root.focus({ preventScroll: true });
8961
9326
  touches.set(e.pointerId, p);
8962
9327
  stage.setPointerCapture(e.pointerId);
9328
+ press = touches.size === 1 && mode !== "wipe" && master() ? p : null;
8963
9329
  if (touches.size >= 2) {
8964
9330
  e.preventDefault();
9331
+ press = null;
8965
9332
  return startPinch();
8966
9333
  }
8967
9334
  if (mode === "wipe") {
@@ -8971,7 +9338,7 @@ function compare(container, options) {
8971
9338
  if (inside && (grab.onLine || !zoomed())) {
8972
9339
  drag = { kind: "wipe", offset: grab.offset };
8973
9340
  root.dataset.dragging = "";
8974
- gripY = p.y;
9341
+ gripAt.snap(wr.h > 0 ? (p.y - wr.y) / wr.h : 0.5);
8975
9342
  e.preventDefault();
8976
9343
  if (!grab.onLine)
8977
9344
  setWipe(wipeAt(p.x, wr), true, true);
@@ -8989,6 +9356,8 @@ function compare(container, options) {
8989
9356
  const onMove = (e) => {
8990
9357
  const p = local(e);
8991
9358
  pointer = p;
9359
+ if (press && Math.hypot(p.x - press.x, p.y - press.y) > 4)
9360
+ press = null;
8992
9361
  if (touches.has(e.pointerId))
8993
9362
  touches.set(e.pointerId, p);
8994
9363
  if (pinch && touches.size >= 2) {
@@ -9002,14 +9371,12 @@ function compare(container, options) {
9002
9371
  pinch = { d, x: mid.x, y: mid.y };
9003
9372
  return handZoom(zoomAbout(panned, k, q.pu, q.pv));
9004
9373
  }
9005
- if (mode === "wipe" && drag?.kind !== "pan") {
9006
- gripY = p.y;
9007
- if (!drag)
9008
- placeGrip(wipeRect(layoutNow().L.overlay));
9009
- }
9010
- if (drag?.kind === "wipe")
9011
- setWipe(wipeAt(p.x, wipeRect(layoutNow().L.overlay), drag.offset));
9012
- else if (drag?.kind === "pan") {
9374
+ if (drag?.kind === "wipe") {
9375
+ const wr = wipeRect(layoutNow().L.overlay);
9376
+ gripAt.snap(wr.h > 0 ? (p.y - wr.y) / wr.h : 0.5);
9377
+ setWipe(wipeAt(p.x, wr, drag.offset));
9378
+ placeGrip(wr);
9379
+ } else if (drag?.kind === "pan") {
9013
9380
  const dx = p.x - drag.x;
9014
9381
  const dy = p.y - drag.y;
9015
9382
  drag.x = p.x;
@@ -9020,11 +9387,23 @@ function compare(container, options) {
9020
9387
  drawNow();
9021
9388
  };
9022
9389
  const endDrag = (e) => {
9390
+ if (press && e.type === "pointerup") {
9391
+ api.toggle();
9392
+ glyph.flash(isPlaying());
9393
+ }
9394
+ press = null;
9023
9395
  touches.delete(e.pointerId);
9024
9396
  if (touches.size < 2)
9025
9397
  pinch = null;
9026
9398
  if (!drag)
9027
9399
  return;
9400
+ if (drag.kind === "wipe") {
9401
+ if (reducedMotion(win))
9402
+ gripAt.snap(0.5);
9403
+ else
9404
+ gripAt.set(0.5);
9405
+ requestDraw();
9406
+ }
9028
9407
  drag = null;
9029
9408
  delete root.dataset.dragging;
9030
9409
  delete root.dataset.panning;
@@ -9174,6 +9553,7 @@ function compare(container, options) {
9174
9553
  old.el.replaceWith(s.el);
9175
9554
  old.preview.replaceWith(s.preview);
9176
9555
  sides[index] = s;
9556
+ root.toggleAttribute("data-video", sides.some((x) => x.kind === "video"));
9177
9557
  note.textContent = "";
9178
9558
  setLabel(index, labelOf(index));
9179
9559
  if (near)
@@ -9188,6 +9568,7 @@ function compare(container, options) {
9188
9568
  stage.insertBefore(sides[0].preview, labels[0]);
9189
9569
  setLabel(0, labelOf(0));
9190
9570
  setLabel(1, labelOf(1));
9571
+ root.toggleAttribute("data-video", sides.some((s) => s.kind === "video"));
9191
9572
  loupeBtn.setAttribute("aria-pressed", String(loupeOn));
9192
9573
  loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
9193
9574
  const modePill = modeList.length > 1 ? selectionPill(modes, {
@@ -9388,6 +9769,7 @@ function compare(container, options) {
9388
9769
  disposed = true;
9389
9770
  loop.stop();
9390
9771
  modePill?.dispose();
9772
+ glyph.dispose();
9391
9773
  win.clearTimeout(releaseTimer);
9392
9774
  ro.disconnect();
9393
9775
  nearIo?.disconnect();
@@ -9417,7 +9799,7 @@ function compare(container, options) {
9417
9799
  wake();
9418
9800
  return api;
9419
9801
  }
9420
- var COMPARE_FFMPEG = "ffmpeg -i in.mov -vf fps=30 -fps_mode cfr -c:v libx264 -preset slow -crf 18 -g 15 -keyint_min 15 -pix_fmt yuv420p -movflags +faststart -an out.mp4", LAYOUT, FADE2, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
9802
+ var COMPARE_FFMPEG = "ffmpeg -i in.mov -vf fps=30 -fps_mode cfr -c:v libx264 -preset slow -crf 18 -g 15 -keyint_min 15 -pix_fmt yuv420p -movflags +faststart -an out.mp4", LAYOUT, FADE2, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, MARK, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
9421
9803
  var init_compare = __esm(() => {
9422
9804
  init_motion();
9423
9805
  init_page_hints();
@@ -9428,15 +9810,17 @@ var init_compare = __esm(() => {
9428
9810
  init_icons();
9429
9811
  init_look();
9430
9812
  init_rgba();
9813
+ init_player();
9431
9814
  init_selection_pill();
9432
9815
  init_timeline();
9433
9816
  init_timeline_model();
9434
- LAYOUT = springMotion({ duration: 0.35, bounce: 0 });
9817
+ LAYOUT = springMotion(SPRINGS.default);
9435
9818
  FADE2 = springMotion(SPRINGS.snappy);
9436
9819
  FADE_REDUCED = springMotion(FADE);
9437
9820
  ZOOM = springMotion({ duration: 0.35, bounce: 0 });
9438
9821
  WIPE_CATCH = springMotion(CATCH_UP);
9439
9822
  GRIP = { w: 16, h: 28 };
9823
+ MARK = { w: 12, gap: 4 };
9440
9824
  LOUPE = { r: 88, zoom: 4 };
9441
9825
  MODE_LABEL = {
9442
9826
  side: { short: "Side", long: "Side by side" },
@@ -10482,19 +10866,19 @@ var init_diff_css = __esm(() => {
10482
10866
  @property --kd-fb{syntax:"<length>";inherits:false;initial-value:0px}
10483
10867
  :where(.knobs-ui){--kd-add:var(--_k-diff-add);--kd-del:var(--_k-diff-del);--kd-mod:var(--_k-diff-mod);--kd-move:var(--k-diff-move,var(--_k-info))}
10484
10868
  :where(.knobs-diff,.knobs-diff *,.knobs-diff-files,.knobs-diff-files *){box-sizing:border-box}
10485
- .knobs-ui.knobs-diff{--kd-bg:var(--k-diff-bg,var(--_kd-bg,var(--_k-bg)));--kd-solid:var(--kd-fill,var(--kd-bg));--kd-line:calc(var(--_k-font-size) * 1.5);
10869
+ .knobs-ui.knobs-diff{--kd-bg:var(--k-diff-bg,var(--_kd-bg,transparent));--kd-solid:var(--kd-fill,var(--kd-bg));--kd-line:calc(var(--_k-font-size) * 1.5);
10486
10870
  --kd-i:var(--k-diff-inset,8px);--kd-x:var(--kd-i);--kd-inner:max(3px,calc(var(--_ku-pane-radius) - var(--kd-i)));--kd-ctl:var(--kd-inner);
10487
10871
  --kd-head:calc(var(--_k-control-h) + 2 * var(--kd-i));--kd-fade:var(--k-diff-fade,20px);--kd-stick:0px;position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;
10488
10872
  background:var(--kd-bg);border-radius:var(--_ku-pane-radius);font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);text-align:left;
10489
10873
  letter-spacing:normal;word-spacing:normal;text-transform:none;-webkit-font-smoothing:antialiased}
10490
10874
  .knobs-ui.knobs-diff[data-height="auto"]{height:auto}
10491
- .knobs-diff::after{content:"";position:absolute;inset:0;z-index:3;border-radius:inherit;box-shadow:var(--_ku-pane-ring);pointer-events:none}
10875
+ .knobs-ui.knobs-diff:not([data-on="pane"])::after{content:"";position:absolute;inset:0;z-index:3;box-shadow:inset 0 -1px 0 var(--_k-border-subtle);pointer-events:none}
10492
10876
  .knobs-diff [hidden],.knobs-diff-files [hidden]{display:none!important}
10493
10877
  .knobs-diff svg{display:block;flex:none}
10494
- .knobs-ui.knobs-diff[data-on="surface"]{--kd-x:0px;--kd-head:var(--_k-control-h);--kd-ctl:var(--_k-radius)}
10495
- /* The header's hairline shows only once the code has scrolled under it. */
10878
+ .knobs-ui.knobs-diff:not([data-on="pane"]){--kd-x:0px;--kd-head:calc(var(--_k-control-h) + var(--kd-i));--kd-ctl:var(--_k-radius)}
10879
+ /* A hairline under the header, firmer once the code has scrolled under it. */
10496
10880
  .knobs-diff-head{position:relative;z-index:1;flex:none;display:flex;align-items:center;gap:8px;height:var(--kd-head);padding:0 var(--kd-x);
10497
- box-shadow:inset 0 -1px transparent;transition:box-shadow .15s;user-select:none;-webkit-user-select:none}
10881
+ box-shadow:inset 0 -1px var(--_k-border-subtle);transition:box-shadow .15s;user-select:none;-webkit-user-select:none}
10498
10882
  .knobs-diff[data-scrolled]>.knobs-diff-head{box-shadow:inset 0 -1px var(--_k-border)}
10499
10883
  .knobs-diff-path{flex:0 1 auto;min-width:0;overflow:hidden;white-space:nowrap}
10500
10884
  .knobs-diff-dir,.knobs-diff-from{color:var(--_k-fg-3)}
@@ -10532,6 +10916,15 @@ var init_diff_css = __esm(() => {
10532
10916
  .knobs-ui .knobs-diff-viewed[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
10533
10917
  .knobs-diff-viewed svg{width:16px;height:16px}
10534
10918
  .knobs-diff-viewed[aria-pressed="true"] svg{color:var(--kd-add)}
10919
+ /* Narrow, the header gives up words before anything is cut off at its end: Viewed keeps its
10920
+ check, then the counts' bar goes, then the counts; the path fades at its end throughout. */
10921
+ .knobs-diff-head,.knobs-diff-part-head{container-type:inline-size}
10922
+ @container (max-width:520px){
10923
+ .knobs-diff-viewed-label{display:none}
10924
+ .knobs-ui .knobs-diff-viewed{width:var(--_k-control-h);padding:0;justify-content:center}
10925
+ .knobs-diff-counts .knobs-diff-bar{display:none}
10926
+ }
10927
+ @container (max-width:400px){.knobs-diff-counts{display:none}.knobs-diff-nav>span{min-width:2.6em}}
10535
10928
  .knobs-diff-path{transition:opacity .2s}
10536
10929
  .knobs-diff[data-viewed]>.knobs-diff-head .knobs-diff-path,.knobs-diff-part[data-viewed] .knobs-diff-path{opacity:.6}
10537
10930
  /* Split | Unified: the selection pill on the one picked, its bright copy the picked one's text; a
@@ -10596,24 +10989,26 @@ var init_diff_css = __esm(() => {
10596
10989
  --diffs-bg-deletion-number-override:color-mix(in srgb,var(--kd-del) 55%,var(--kd-solid));
10597
10990
  --diffs-bg-addition-emphasis-override:color-mix(in srgb,var(--kd-add) 22%,transparent);
10598
10991
  --diffs-bg-deletion-emphasis-override:color-mix(in srgb,var(--kd-del) 22%,transparent);
10599
- --diffs-bg-separator-override:color-mix(in srgb,var(--_k-fg) 4.5%,var(--kd-solid));
10600
- --diffs-bg-context-override:color-mix(in srgb,var(--_k-fg) 3%,var(--kd-solid));
10601
- --diffs-bg-buffer-override:color-mix(in srgb,var(--_k-fg) 3.5%,var(--kd-solid));
10992
+ /* Unchanged lines and the folds between hunks sit on the void; only a split's missing lines
10993
+ are faintly filled. */
10994
+ --diffs-bg-separator-override:var(--kd-solid);
10995
+ --diffs-bg-context-override:var(--kd-solid);
10996
+ --diffs-bg-buffer-override:color-mix(in srgb,var(--_k-fg) 2.5%,var(--kd-solid));
10602
10997
  --diffs-bg-selection-override:var(--_k-accent);
10603
10998
  --diffs-fg-number-override:var(--_k-fg-3);
10604
10999
  }
10605
- /* diffReview: one card, the file list a drawer at its left (its width springs; the list inside
10606
- is pinned to its right edge, so it slides out from under the diff), parted from the diff by a
10607
- hairline. The diff in it draws no card of its own. */
10608
- .knobs-ui.knobs-diff-review{position:relative;display:flex;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;border-radius:var(--_ku-pane-radius);background:var(--_ku-pane-bg);
11000
+ /* diffReview, on the void as the diff is: the file list a drawer at its left (its width springs;
11001
+ the list inside is pinned to its right edge, so it slides out from under the diff), parted
11002
+ from the diff by a hairline; one hairline runs under both headers and one under it all. */
11003
+ .knobs-ui.knobs-diff-review{position:relative;display:flex;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;
10609
11004
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);text-align:left;letter-spacing:normal;-webkit-font-smoothing:antialiased}
10610
- .knobs-diff-review::after{content:"";position:absolute;inset:0;z-index:5;border-radius:inherit;box-shadow:var(--_ku-pane-ring);pointer-events:none}
11005
+ .knobs-diff-review::after{content:"";position:absolute;inset:0;z-index:5;box-shadow:inset 0 -1px 0 var(--_k-border-subtle);pointer-events:none}
10611
11006
  :where(.knobs-diff-review,.knobs-diff-review *){box-sizing:border-box}
10612
- .knobs-diff-drawer{position:relative;flex:none;width:0;height:100%;overflow:hidden;box-shadow:inset -1px 0 var(--_k-border)}
11007
+ .knobs-diff-drawer{position:relative;flex:none;width:0;height:100%;overflow:hidden;box-shadow:inset -1px 0 var(--_k-border-subtle)}
10613
11008
  .knobs-diff-drawer[data-shut]{visibility:hidden}
10614
11009
  .knobs-diff-drawer-in{position:absolute;top:0;bottom:0;right:0;display:flex;flex-direction:column}
10615
11010
  .knobs-diff-drawer-head{flex:none;display:flex;align-items:center;gap:6px;height:calc(var(--_k-control-h) + 2 * var(--k-diff-inset,8px));padding:0 var(--k-diff-inset,8px) 0 calc(var(--_ku-row-x) + 8px);
10616
- user-select:none;-webkit-user-select:none}
11011
+ box-shadow:inset 0 -1px var(--_k-border-subtle);user-select:none;-webkit-user-select:none}
10617
11012
  .knobs-diff-drawer-title{font-weight:var(--_k-weight-title)}
10618
11013
  .knobs-diff-drawer-count{color:var(--_k-fg-3);font-size:var(--_k-text-meta);font-variant-numeric:tabular-nums}
10619
11014
  .knobs-diff-drawer-in>.knobs-diff-files{flex:1;min-height:0;height:auto}
@@ -10625,10 +11020,11 @@ var init_diff_css = __esm(() => {
10625
11020
  .knobs-ui .knobs-diff-toggle[aria-pressed]:not(:hover,:focus-visible){background:transparent}
10626
11021
  /* diffReview's file list: a filter over a compact tree, a status letter, the counts and their
10627
11022
  bar on each file; one highlight glides to the file on show. Viewed files dim, with a tick. */
10628
- .knobs-diff-files{--k-row:var(--k-diff-row,28px);display:flex;flex-direction:column;height:100%;min-height:0;padding-top:2px}
11023
+ .knobs-diff-files{--k-row:var(--k-diff-row,28px);display:flex;flex-direction:column;height:100%;min-height:0;padding-top:4px}
10629
11024
  .knobs-diff-filter{position:relative;flex:none;display:flex;align-items:center;gap:6px;height:26px;margin:0 var(--_ku-row-x) 4px;padding:0 4px 0 8px;
10630
- border-radius:max(6px,var(--_ku-row-radius));background:var(--_ku-wash);color:var(--_k-fg-3);box-shadow:inset 0 0 0 1px transparent;transition:background .15s,box-shadow .15s}
10631
- .knobs-diff-filter:focus-within{background:var(--_ku-wash-hi);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_k-accent) 45%,transparent)}
11025
+ border-radius:max(6px,var(--_ku-row-radius));color:var(--_k-fg-3);box-shadow:inset 0 0 0 1px transparent;transition:box-shadow .15s}
11026
+ /* The filter is a line of text, no box; focused, a hairline ring. */
11027
+ .knobs-diff-filter:focus-within{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_k-accent) 45%,transparent)}
10632
11028
  .knobs-diff-filter svg{width:12px;height:12px;flex:none}
10633
11029
  .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}
10634
11030
  .knobs-ui .knobs-diff-filter>input::placeholder{color:var(--_k-fg-3)}
@@ -11447,7 +11843,10 @@ function codeDiff(container, options) {
11447
11843
  const viewedToggle = () => {
11448
11844
  const b = make("button", "knobs-diff-viewed");
11449
11845
  b.type = "button";
11450
- b.append(make("span", ""), "Viewed");
11846
+ const label = make("span", "knobs-diff-viewed-label");
11847
+ label.textContent = "Viewed";
11848
+ b.append(make("span", ""), label);
11849
+ b.setAttribute("aria-label", "Viewed");
11451
11850
  return b;
11452
11851
  };
11453
11852
  const paintViewed = (b, on) => {
@@ -16146,6 +16545,7 @@ init_timeline();
16146
16545
  init_timeline_model();
16147
16546
  init_compare();
16148
16547
  init_compare_strip();
16548
+ init_player();
16149
16549
  init_compare_model();
16150
16550
  init_tree();
16151
16551
  init_tree_model();
@@ -19859,7 +20259,7 @@ function normalizeWalkthrough(doc) {
19859
20259
  ...step.question ? { question: step.question } : {},
19860
20260
  media: list.map((m) => fillMedia(m, doc.base)),
19861
20261
  columns: Math.max(1, Math.min(3, Math.round(step.columns ?? 1))),
19862
- ...step.more?.trim() && list.length > 1 ? { more: step.more.trim() } : {},
20262
+ ...step.more?.trim() ? { more: step.more.trim() } : {},
19863
20263
  options,
19864
20264
  ask
19865
20265
  };
@@ -19874,6 +20274,7 @@ function normalizeWalkthrough(doc) {
19874
20274
  ...typeof s.kicker === "string" && s.kicker.trim() ? { kicker: s.kicker.trim() } : {},
19875
20275
  ...s.status ? { status: s.status } : {},
19876
20276
  ...s.summary ? { summary: s.summary } : {},
20277
+ ...s.more?.trim() ? { more: s.more.trim() } : {},
19877
20278
  steps: own
19878
20279
  };
19879
20280
  });
@@ -19907,6 +20308,7 @@ function normalizeWalkthrough(doc) {
19907
20308
  id: doc.id,
19908
20309
  title: doc.title,
19909
20310
  ...doc.summary ? { summary: doc.summary } : {},
20311
+ ...doc.more?.trim() ? { more: doc.more.trim() } : {},
19910
20312
  meta,
19911
20313
  feed,
19912
20314
  verdicts: ((doc.verdicts?.length) ? doc.verdicts : DEFAULT_VERDICTS).map((v) => ({
@@ -19914,9 +20316,40 @@ function normalizeWalkthrough(doc) {
19914
20316
  label: label(v)
19915
20317
  })),
19916
20318
  sections: withKickers(sections, doc.sections, doc.kickers),
19917
- steps
20319
+ steps,
20320
+ prs: knownPRs(doc, sections),
20321
+ ...repoOf(doc) ? { repo: repoOf(doc) } : {}
19918
20322
  };
19919
20323
  }
20324
+ var prNumber = (href) => /\/(?:pull|merge_requests)\/(\d+)(?:[/?#]|$)/.exec(href)?.[1] ?? null;
20325
+ var repoOf = (doc) => doc.repo?.trim().replace(/^https?:\/\/github\.com\//, "").replace(/\/+$/, "") || undefined;
20326
+ function knownPRs(doc, sections) {
20327
+ const out = {};
20328
+ const repo = repoOf(doc);
20329
+ for (const s of sections) {
20330
+ const n = s.link && prNumber(s.link.href);
20331
+ if (n)
20332
+ out[n] = { href: s.link.href.replace(/(\/pull\/\d+).*$/, "$1") };
20333
+ }
20334
+ for (const [k, pr] of Object.entries(doc.prs ?? {})) {
20335
+ const n = k.replace(/^#/, "");
20336
+ if (!/^\d+$/.test(n))
20337
+ continue;
20338
+ out[n] = { ...out[n], ...pr };
20339
+ }
20340
+ if (repo) {
20341
+ for (const n of Object.keys(out))
20342
+ if (!out[n].href)
20343
+ out[n] = { ...out[n], href: `https://github.com/${repo}/pull/${n}` };
20344
+ }
20345
+ return out;
20346
+ }
20347
+ function findPR(prs, n, repo) {
20348
+ const known = prs[n];
20349
+ if (known)
20350
+ return known;
20351
+ return repo ? { href: `https://github.com/${repo}/pull/${n}` } : null;
20352
+ }
19920
20353
  var isPullRequest = (href) => /\/(?:pull|merge_requests)\/\d+(?:[/?#]|$)/.test(href);
19921
20354
  function withKickers(sections, source, kickers = "pr") {
19922
20355
  if (kickers === false)
@@ -20742,6 +21175,74 @@ function disclosure(host, options) {
20742
21175
  toggle.addEventListener("click", () => api.toggle());
20743
21176
  return api;
20744
21177
  }
21178
+ var PR_PATHS = {
21179
+ 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",
21180
+ 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"
21181
+ };
21182
+ 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>`;
21183
+ var PR_STATE = { merged: "Merged", open: "Open", draft: "Draft", closed: "Closed" };
21184
+ function prChip(doc, n, pr) {
21185
+ const chip = el(doc, pr.href ? "a" : "span", "knobs-wt-pr");
21186
+ if (pr.href) {
21187
+ chip.setAttribute("href", pr.href);
21188
+ chip.setAttribute("target", "_blank");
21189
+ chip.setAttribute("rel", "noreferrer");
21190
+ }
21191
+ if (pr.state)
21192
+ chip.dataset.state = pr.state;
21193
+ chip.innerHTML = prGlyph(pr.state === "merged");
21194
+ chip.append(text(doc, "span", "", `#${n}`));
21195
+ const tip = [`#${n}`, pr.title, pr.state ? PR_STATE[pr.state] : ""].filter(Boolean).join(" · ");
21196
+ chip.title = tip;
21197
+ chip.setAttribute("aria-label", `Pull request ${tip}`);
21198
+ return chip;
21199
+ }
21200
+ var PR_URL = /^https?:\/\/[^/]+\/.+\/(?:pull|merge_requests)\/(\d+)\/?(?:[?#].*)?$/;
21201
+ var PR_REF = /(^|[^\w&/#])#(\d{3,7})\b/g;
21202
+ function linkPRs(root, find) {
21203
+ const doc = root.ownerDocument;
21204
+ for (const a of Array.from(root.querySelectorAll("a[href]"))) {
21205
+ if (a.classList.contains("knobs-wt-pr") || a.closest(".knobs-wt-slot,button"))
21206
+ continue;
21207
+ const n = PR_URL.exec(a.getAttribute("href") ?? "")?.[1];
21208
+ const words = a.textContent?.trim() ?? "";
21209
+ if (!n || !(words === `#${n}` || words === n || words === a.getAttribute("href")))
21210
+ continue;
21211
+ a.replaceWith(prChip(doc, n, { href: a.href, ...find(n) }));
21212
+ }
21213
+ const walker = doc.createTreeWalker(root, 5);
21214
+ const hits = [];
21215
+ for (let t = walker.nextNode();t; t = walker.nextNode()) {
21216
+ if (t.nodeType !== 3)
21217
+ continue;
21218
+ const p = t.parentElement;
21219
+ if (!p || p.closest("a,code,pre,button,.knobs-wt-slot,.knobs-wt-pr"))
21220
+ continue;
21221
+ PR_REF.lastIndex = 0;
21222
+ if (PR_REF.test(t.nodeValue ?? ""))
21223
+ hits.push(t);
21224
+ }
21225
+ for (const t of hits) {
21226
+ const value = t.nodeValue ?? "";
21227
+ const frag = doc.createDocumentFragment();
21228
+ let last = 0;
21229
+ let changed = false;
21230
+ PR_REF.lastIndex = 0;
21231
+ for (let m = PR_REF.exec(value);m; m = PR_REF.exec(value)) {
21232
+ const pr = find(m[2]);
21233
+ if (!pr)
21234
+ continue;
21235
+ const at = m.index + m[1].length;
21236
+ frag.append(value.slice(last, at), prChip(doc, m[2], pr));
21237
+ last = at + m[2].length + 1;
21238
+ changed = true;
21239
+ }
21240
+ if (!changed)
21241
+ continue;
21242
+ frag.append(value.slice(last));
21243
+ t.replaceWith(frag);
21244
+ }
21245
+ }
20745
21246
 
20746
21247
  // src/ui/walkthrough-media.ts
20747
21248
  init_compare_model();
@@ -20759,7 +21260,7 @@ var fade2 = "-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 1.5em)
20759
21260
  var walkthroughCss = `
20760
21261
  :where(.knobs-walkthrough,.knobs-walkthrough *,.knobs-wt-slot,.knobs-wt-slot *){box-sizing:border-box}
20761
21262
  .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;
20762
- --kw-measure:var(--k-walkthrough-measure,68ch);--kw-width:var(--k-walkthrough-width,680px);--kw-rail-w:220px;--kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));
21263
+ --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));
20763
21264
  --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);
20764
21265
  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;
20765
21266
  letter-spacing:normal;-webkit-font-smoothing:antialiased}
@@ -20772,44 +21273,57 @@ var walkthroughCss = `
20772
21273
  :where(.knobs-ui) :is(.knobs-wt-btn,.knobs-wt-pick,.knobs-wt-toc-row,button.knobs-wt-stitle,.knobs-wt-chapter,.knobs-wt-disc-toggle){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;
20773
21274
  background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
20774
21275
 
20775
- /* ——— layout: a readable column, centred, with a slim contents rail beside it (right by default);
20776
- one column where it's narrow ——— */
20777
- .knobs-wt-layout{display:grid;grid-template-columns:minmax(0,var(--kw-width)) var(--kw-rail-w);justify-content:center;gap:var(--_k-space-column);align-items:start}
21276
+ /* ——— layout: the contents rail sticky at the left edge (right if asked), and the column centred
21277
+ in the room beside it, up to --kw-width: media take the column, prose stops at the measure.
21278
+ One column where it's narrow ——— */
21279
+ .knobs-wt-layout{display:grid;grid-template-columns:var(--kw-rail-w) minmax(0,1fr);gap:var(--_k-space-column);align-items:start}
21280
+ .knobs-walkthrough[data-rail="right"] .knobs-wt-layout{grid-template-columns:minmax(0,1fr) var(--kw-rail-w)}
20778
21281
  .knobs-walkthrough[data-rail="right"] .knobs-wt-main{grid-column:1;grid-row:1}
20779
21282
  .knobs-walkthrough[data-rail="right"] .knobs-wt-rail{grid-column:2;grid-row:1}
20780
- .knobs-walkthrough[data-rail="left"] .knobs-wt-layout{grid-template-columns:var(--kw-rail-w) minmax(0,var(--kw-width))}
20781
- .knobs-walkthrough[data-rail="none"] .knobs-wt-layout{grid-template-columns:minmax(0,var(--kw-width))}
20782
- .knobs-wt-main{display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0;padding-bottom:12px}
21283
+ .knobs-walkthrough[data-rail="none"] .knobs-wt-layout{grid-template-columns:minmax(0,1fr)}
21284
+ .knobs-wt-main{justify-self:center;width:100%;max-width:var(--kw-width);display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0;padding-bottom:12px}
20783
21285
  /* The intro, the updates and each section: a section's room apart, the hairline in the middle. */
20784
21286
  .knobs-wt-flow{display:flex;flex-direction:column;gap:calc(var(--_k-space-section) / 2);min-width:0}
20785
21287
  .knobs-wt-flow>*+*{padding-top:calc(var(--_k-space-section) / 2);box-shadow:inset 0 1px 0 var(--_k-border-subtle)}
20786
- @container knobs-wt (max-width:940px){.knobs-walkthrough .knobs-wt-layout{grid-template-columns:minmax(0,1fr);gap:var(--_k-space-title)}
21288
+ @container knobs-wt (max-width:960px){.knobs-walkthrough[data-rail] .knobs-wt-layout{grid-template-columns:minmax(0,1fr);gap:var(--_k-space-title)}
20787
21289
  .knobs-walkthrough[data-rail] .knobs-wt-layout>*{grid-column:auto;grid-row:auto}}
20788
21290
 
20789
21291
  /* ——— one line of facts: small, faint, joined by ·, its trailing edge fading ——— */
20790
21292
  .knobs-wt-metaline{display:flex;align-items:baseline;gap:var(--_k-space-inside);min-width:0;overflow:hidden;white-space:nowrap;font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3);${fade2}}
20791
21293
  .knobs-wt-metaline>*{flex:none}
20792
21294
  .knobs-wt-metaline>*+*::before{content:"·";margin-right:var(--_k-space-inside);color:var(--_k-fg-3)}
20793
- .knobs-walkthrough .knobs-wt-metaline a{display:inline-flex;align-items:baseline;color:inherit;text-decoration:none;transition:color calc(var(--_k-hover-out) * 1ms)}
20794
- .knobs-walkthrough .knobs-wt-metaline a:hover{color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
21295
+ .knobs-walkthrough .knobs-wt-metaline a:not(.knobs-wt-pr){display:inline-flex;align-items:baseline;color:inherit;text-decoration:none;transition:color calc(var(--_k-hover-out) * 1ms)}
21296
+ .knobs-walkthrough .knobs-wt-metaline a:not(.knobs-wt-pr):hover{color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
20795
21297
  .knobs-wt-metaline svg{align-self:center;width:12px;height:12px;margin-left:2px}
21298
+ /* A pull request: a small chip, its state's icon and number in a quiet tint of the state (open
21299
+ the success hue, merged the violet series, draft and closed neutral), its title on hover. */
21300
+ .knobs-ui .knobs-wt-pr{--kw-pr:var(--_k-fg-2);display:inline-flex;align-items:center;gap:3px;height:18px;padding:0 5px 0 4px;margin:0 1px;border-radius:calc(var(--_k-radius) / 2);vertical-align:1px;
21301
+ background:color-mix(in oklch,var(--kw-pr) 12%,transparent);color:var(--kw-pr);font:var(--_k-weight-label) var(--_k-type-small)/1 var(--_k-font);font-variant-numeric:tabular-nums;white-space:nowrap;text-decoration:none;
21302
+ transition:background calc(var(--_k-hover-out) * 1ms)}
21303
+ .knobs-ui .knobs-wt-pr[data-state="open"]{--kw-pr:color-mix(in oklch,var(--_k-ok) 60%,var(--_k-fg-2))}
21304
+ .knobs-ui .knobs-wt-pr[data-state="merged"]{--kw-pr:color-mix(in oklch,var(--_k-series-5) 60%,var(--_k-fg-2))}
21305
+ .knobs-ui .knobs-wt-pr:is([data-state="draft"],[data-state="closed"]){--kw-pr:var(--_k-fg-3)}
21306
+ .knobs-ui a.knobs-wt-pr:is(:hover,:focus-visible){background:color-mix(in oklch,var(--kw-pr) 22%,transparent);transition-duration:calc(var(--_k-hover-in) * 1ms)}
21307
+ .knobs-wt-pr svg{flex:none}
21308
+ .knobs-wt-metaline .knobs-wt-pr{align-self:center;margin:0}
20796
21309
  .knobs-wt-kicker-lead{color:var(--_k-fg);font-weight:var(--kw-strong)}
20797
21310
  .knobs-wt-metaline [data-status="on"]{color:var(--_k-fg)}
20798
21311
  .knobs-wt-metaline [data-status="warn"]{color:var(--_k-warn)}
20799
21312
 
20800
21313
  /* ——— the page's head ——— */
20801
21314
  .knobs-wt-head{display:flex;flex-direction:column;gap:var(--_k-space-title)}
20802
- .knobs-wt-title{font-size:var(--_k-type-title)!important;line-height:var(--_k-leading-title);font-weight:var(--kw-strong)!important;text-wrap:balance}
21315
+ .knobs-wt-title{font-size:var(--_k-type-heading)!important;line-height:var(--_k-leading-heading);font-weight:var(--kw-strong)!important;text-wrap:balance}
20803
21316
  .knobs-wt-summary{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
20804
21317
  .knobs-wt-meta{display:flex;flex-wrap:wrap;gap:var(--_k-space-inside) var(--_k-space-title);color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
20805
21318
  .knobs-wt-meta>span{display:inline-flex;gap:var(--_k-space-inside);white-space:nowrap}
20806
21319
  .knobs-wt-meta b{font-weight:var(--_k-weight);color:var(--_k-fg-2);font-family:var(--_k-mono)}
20807
21320
 
20808
- /* ——— a section: a PR or topic. Its kicker leads the meta line; its title outranks the page's ——— */
21321
+ /* ——— a section: a PR or topic. Its kicker leads the meta line, its title under it (16/24, under
21322
+ the page's 20/28), both on the column's edge ——— */
20809
21323
  .knobs-wt-section{display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0}
20810
21324
  .knobs-wt-sintro{display:flex;flex-direction:column;gap:var(--_k-space-title);min-width:0}
20811
21325
  .knobs-wt-shead{display:flex;flex-direction:column;gap:var(--_k-space-inside);min-width:0;scroll-margin-top:16px}
20812
- .knobs-wt-heading{font-size:var(--_k-type-heading)!important;line-height:var(--_k-leading-heading);font-weight:var(--kw-strong)!important;letter-spacing:-.01em!important;text-wrap:balance}
21326
+ .knobs-wt-heading{font-size:var(--_k-type-title)!important;line-height:var(--_k-leading-title);font-weight:var(--kw-strong)!important;text-wrap:balance}
20813
21327
  .knobs-wt-ssum{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
20814
21328
  .knobs-wt-chip{display:inline-flex;align-items:center;gap:6px;height:20px;padding:0 8px;border-radius:var(--_k-round);font-size:var(--_k-type-small);font-weight:var(--kw-strong);white-space:nowrap;
20815
21329
  --kw-tone:var(--_k-fg-2);color:var(--kw-tone);background:color-mix(in srgb,var(--kw-tone) 14%,transparent)}
@@ -20870,6 +21384,8 @@ var walkthroughCss = `
20870
21384
  .knobs-wt-disc[data-open]>.knobs-wt-disc-body:not([data-moving]){overflow:visible}
20871
21385
  .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)}
20872
21386
  .knobs-wt-disc-content{padding-top:var(--_k-space-inside)}
21387
+ /* "More" under a caption or summary: the rest of its words, in the same voice. */
21388
+ .knobs-wt-more-text{display:flex;flex-direction:column;gap:var(--_k-space-sibling);max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
20873
21389
  /* A trail sits on the toggle's label edge: faint lines, the time trailing. */
20874
21390
  .knobs-wt-trail{display:flex;flex-direction:column;list-style:none;padding:0 0 0 calc(12px + var(--_k-space-inside));font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3);min-width:0}
20875
21391
  .knobs-wt-trail li{display:flex;align-items:baseline;gap:var(--_k-space-sibling);min-width:0;white-space:nowrap}
@@ -20987,32 +21503,28 @@ var walkthroughCss = `
20987
21503
  .knobs-wt-ocap{color:var(--_k-fg-2);max-width:var(--kw-measure)}
20988
21504
 
20989
21505
  /* ——— the rail: progress, contents, export ——— */
20990
- .knobs-wt-rail{position:sticky;top:16px;display:flex;flex-direction:column;gap:var(--_k-space-title);max-height:calc(100vh - 32px);min-height:0;font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
21506
+ .knobs-wt-rail{position:sticky;top:var(--_k-pad-page);display:flex;flex-direction:column;gap:var(--_k-space-title);max-height:calc(100vh - var(--_k-pad-page) * 2);min-height:0;font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
20991
21507
  .knobs-wt-progress{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
20992
21508
  .knobs-wt-count{display:flex;align-items:baseline;justify-content:space-between;color:var(--_k-fg-2)}
20993
21509
  .knobs-wt-count b{color:var(--_k-fg);font-weight:var(--kw-strong)}
20994
21510
  .knobs-wt-bar{position:relative;height:4px;border-radius:2px;overflow:hidden;background:var(--_k-fill)}
20995
21511
  .knobs-wt-fill{position:absolute;inset:0;border-radius:inherit;background:var(--_k-accent);transform-origin:0 50%;transform:scaleX(var(--kw-done,0))}
20996
21512
  .knobs-walkthrough[data-settled] .knobs-wt-fill{transition:transform var(--kw-calm-ms) var(--kw-calm)}
20997
- /* The contents: rows centred on one spine, title rows (32 high, the control radius) and the
20998
- steps under them (20 high, half of it), bled out so their text sits on the rail's edge; the
20999
- selection pill slides to the current step, and base rows never brighten for being current. */
21513
+ /* The contents: every row on one text edge and in one colour, title rows (32 high, the control
21514
+ radius) and the steps under them (20 high, half of it), bled out so their text sits on the
21515
+ rail's edge; the selection pill slides to the current step, and base rows never brighten. */
21000
21516
  .knobs-wt-toc{position:relative;flex:1 1 auto;min-height:0;overflow:auto;scrollbar-width:none;display:flex;flex-direction:column;gap:var(--_k-space-sibling);margin:0 calc(var(--_k-space-sibling) * -1);padding:0 0 8px}
21001
21517
  .knobs-wt-toc::-webkit-scrollbar{display:none}
21002
21518
  .knobs-wt-toc-group{display:flex;flex-direction:column;min-width:0}
21003
- .knobs-ui .knobs-wt-toc-row{position:relative;display:grid;grid-template-columns:var(--kw-toc-n,0px) minmax(0,1fr) auto;column-gap:var(--_k-space-sibling);align-items:center;flex:none;width:100%;height:32px;
21004
- padding:0 var(--_k-space-sibling);border-radius:var(--_k-radius);color:var(--_k-fg-2);font-size:var(--_k-type-body);line-height:var(--_k-leading-body);font-weight:var(--kw-strong);text-align:left;white-space:nowrap;cursor:pointer;
21519
+ .knobs-ui .knobs-wt-toc-row{position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:var(--_k-space-sibling);align-items:center;flex:none;width:100%;height:32px;
21520
+ padding:0 var(--_k-space-sibling);border-radius:var(--_k-radius);color:var(--_k-fg-2);font-size:var(--_k-type-body);line-height:var(--_k-leading-body);font-weight:var(--_k-weight);text-align:left;white-space:nowrap;cursor:pointer;
21005
21521
  transition:color calc(var(--_k-hover-out) * 1ms)}
21006
- .knobs-ui .knobs-wt-toc-sub{height:20px;border-radius:calc(var(--_k-radius) / 2);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight);color:var(--_k-fg-3)}
21007
- .knobs-wt-toc[data-bare] .knobs-wt-toc-row{grid-template-columns:minmax(0,1fr) auto}
21008
- .knobs-wt-toc[data-bare] .knobs-wt-toc-n{display:none}
21522
+ .knobs-ui .knobs-wt-toc-sub{height:20px;border-radius:calc(var(--_k-radius) / 2);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight)}
21009
21523
  .knobs-ui .knobs-wt-toc-row:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
21010
- .knobs-ui .knobs-wt-toc-sub:is(:hover,:focus-visible){color:var(--_k-fg-2)}
21011
21524
  .knobs-ui .knobs-wt-toc-row:focus-visible{box-shadow:inset 0 0 0 1px var(--_k-focus)}
21012
- .knobs-wt-toc-n,.knobs-wt-toc-end{display:flex;align-items:center;font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight);color:var(--_k-fg-3)}
21013
- .knobs-wt-toc-row[data-active] .knobs-wt-toc-n{color:var(--_k-fg-2)}
21525
+ .knobs-wt-toc-end{display:flex;align-items:center;font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight);color:var(--_k-fg-3)}
21014
21526
  .knobs-wt-toc-label{min-width:0;overflow:hidden;${fade2}}
21015
- .knobs-pick-copy .knobs-wt-toc-n,.knobs-pick-copy .knobs-wt-toc-end{color:var(--_k-fg-2)}
21527
+ .knobs-pick-copy .knobs-wt-toc-end{color:var(--_k-fg-2)}
21016
21528
  /* A step's state: an open ring, a filled dot once answered, nothing to answer a faint ring. */
21017
21529
  .knobs-wt-toc-glyph{--kw-tone:var(--_k-fg-3);display:block;width:6px;height:6px;border-radius:50%;box-shadow:inset 0 0 0 1.5px var(--kw-tone);
21018
21530
  transition:background var(--kw-snappy-ms) var(--kw-snappy),box-shadow var(--kw-snappy-ms) var(--kw-snappy)}
@@ -21027,10 +21539,12 @@ var walkthroughCss = `
21027
21539
  .knobs-ui .knobs-wt-btn[data-primary]:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_k-fg) 16%,transparent)}
21028
21540
  .knobs-wt-keys{color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
21029
21541
  .knobs-wt-keys kbd{font:inherit;color:var(--_k-fg-2)}
21030
- /* Narrow: the rail becomes a bar along the top, with the progress and the export only. */
21031
- @container knobs-wt (max-width:940px){
21542
+ /* Narrow: the contents go; with review on, the rail becomes a bar along the top with the
21543
+ progress and the export. */
21544
+ @container knobs-wt (max-width:960px){
21032
21545
  .knobs-wt-rail{z-index:4;flex-direction:row;align-items:center;gap:var(--_k-space-title);max-height:none;margin:0 calc(var(--_k-pad-page) * -1);padding:8px var(--_k-pad-page);top:0;background:var(--_k-bg-page)}
21033
21546
  .knobs-wt-progress{flex:1}.knobs-wt-toc,.knobs-wt-keys{display:none}
21547
+ .knobs-walkthrough:not([data-review]) .knobs-wt-rail{display:none}
21034
21548
  .knobs-wt-step,.knobs-wt-shead,.knobs-wt-feed{scroll-margin-top:60px}
21035
21549
  }
21036
21550
 
@@ -21040,9 +21554,14 @@ var walkthroughCss = `
21040
21554
  well for hairlines, so nothing draws a fill and a ring of its own there ——— */
21041
21555
  .knobs-ui.knobs-wt-slot{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0;container:knobs-wt-slot / inline-size;color:var(--_k-fg);
21042
21556
  font:var(--_k-weight) var(--_k-type-body)/var(--_k-leading-body) var(--_k-font);font-variant-numeric:tabular-nums;--kw-measure:var(--k-walkthrough-measure,68ch);
21043
- --kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info))}
21044
- .knobs-wt-slot-label{display:block;font:var(--_k-weight-label) var(--_k-type-label)/var(--_k-leading-label) var(--_k-font);color:var(--_k-fg-2)}
21557
+ --kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));--kw-frame-r:var(--_k-radius-panel)}
21558
+ .knobs-surface .knobs-wt-slot{--kw-frame-r:calc(var(--_k-radius-panel) - 4px)}
21559
+ /* A slot's label: sentence case as written, small and faint, never a heading. */
21560
+ .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}}
21045
21561
  .knobs-wt-body{min-width:0}
21562
+ /* One frame for every slot that has one (a picture, a video, a code block, a compare's stage, a
21563
+ slot still loading): the panel radius and a quiet fill, no ring; concentric on a card. */
21564
+ .knobs-wt-slot .knobs-cmp-stage{border-radius:var(--kw-frame-r)}
21046
21565
  /* A compare is exactly as tall as its pictures at this width, so there's no letterbox: the two
21047
21566
  sides edge to edge (side) or one (overlaid), plus its mode bar, the timeline and their two gaps
21048
21567
  (92 px), and the frame strip with its gap (158 px). */
@@ -21050,11 +21569,11 @@ var walkthroughCss = `
21050
21569
  .knobs-wt-fit[data-overlay]{height:calc(100cqw / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
21051
21570
  .knobs-wt-fit[data-strip]{--kw-chrome:158px}
21052
21571
  .knobs-wt-fit[data-settled]{transition:height var(--k-wt-calm-ms,${calm.duration}ms) ${calm.easing}}
21053
- .knobs-wt-frame{position:relative;width:100%;overflow:hidden;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill);aspect-ratio:var(--kw-aspect,16/9)}
21572
+ .knobs-wt-frame{position:relative;width:100%;overflow:hidden;border-radius:var(--kw-frame-r);background:var(--_k-fill);aspect-ratio:var(--kw-aspect,16/9)}
21054
21573
  .knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
21055
21574
  .knobs-wt-frame[data-natural]{aspect-ratio:auto;background:transparent}
21056
21575
  .knobs-wt-frame[data-natural]>img{position:static;height:auto}
21057
- .knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill);color:var(--_k-fg);
21576
+ .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);
21058
21577
  font:var(--_k-type-label)/var(--_k-leading-body) var(--_k-mono);white-space:pre;tab-size:2;scrollbar-width:thin}
21059
21578
  .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)}
21060
21579
  .knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
@@ -21064,8 +21583,8 @@ var walkthroughCss = `
21064
21583
  .knobs-wt-metric{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
21065
21584
  .knobs-wt-mlabel{color:var(--_k-fg-2)}
21066
21585
  .knobs-wt-mvalue{display:flex;align-items:baseline;flex-wrap:wrap;gap:var(--_k-space-sibling);font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}
21067
- .knobs-wt-mfrom{color:var(--_k-fg-3);font-size:var(--_k-type-title);line-height:var(--_k-leading-title)}
21068
- .knobs-wt-mto{font-size:var(--_k-type-heading);line-height:var(--_k-leading-heading);font-weight:var(--_k-weight-label)}
21586
+ .knobs-wt-mfrom{color:var(--_k-fg-3)}
21587
+ .knobs-wt-mto{font-size:var(--_k-type-title);line-height:var(--_k-leading-title);font-weight:var(--_k-weight-label)}
21069
21588
  .knobs-wt-mof{color:var(--_k-fg-3)}
21070
21589
  .knobs-wt-mdelta{--kw-tone:var(--_k-fg-2);font-size:var(--_k-type-small);font-weight:var(--_k-weight-label);color:var(--kw-tone)}
21071
21590
  .knobs-wt-mdelta[data-tone="good"]{--kw-tone:var(--kw-good)}
@@ -21080,7 +21599,7 @@ var walkthroughCss = `
21080
21599
  .knobs-wt-callout[data-tone="warn"]{--kw-tone:var(--_k-warn)}
21081
21600
  .knobs-wt-callout[data-tone="bad"]{--kw-tone:var(--_k-danger)}
21082
21601
  .knobs-wt-text{max-width:var(--kw-measure);color:var(--_k-fg-2)}
21083
- .knobs-wt-hold{border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill)}
21602
+ .knobs-wt-hold{border-radius:var(--kw-frame-r);background:var(--_k-fill)}
21084
21603
  .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)}
21085
21604
 
21086
21605
  /* ——— data blocks: history, stack, table, chapters. On the step's card, so no card of their own:
@@ -21192,7 +21711,7 @@ var walkthroughCss = `
21192
21711
  .knobs-wt-table{max-height:520px;overflow:auto;overscroll-behavior-x:contain;outline:none;scrollbar-width:thin;scrollbar-color:transparent transparent;
21193
21712
  transition:scrollbar-color calc(var(--_k-hover-out) * 1ms)}
21194
21713
  .knobs-wt-table:is(:hover,:focus-visible){scrollbar-color:color-mix(in oklch,var(--_k-fg) 28%,transparent) transparent;transition-duration:calc(var(--_k-hover-in) * 1ms)}
21195
- .knobs-wt-table:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus);border-radius:calc(var(--_k-radius-panel) - 4px)}
21714
+ .knobs-wt-table:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus);border-radius:var(--kw-frame-r)}
21196
21715
  @supports (animation-timeline:scroll()){
21197
21716
  .knobs-wt-table{animation:knobs-fade-a linear both,knobs-fade-b linear both,kw-fade-l linear both,kw-fade-r linear both;animation-duration:auto;
21198
21717
  animation-timeline:scroll(self block),scroll(self block),scroll(self inline),scroll(self inline);animation-range:0 24px,calc(100% - 24px) 100%,0 8px,calc(100% - 8px) 100%;
@@ -21806,16 +22325,25 @@ function tasteTest(container, options) {
21806
22325
  ...o.media !== undefined ? { media: fillMedia(o.media) } : {},
21807
22326
  suggested: o.suggested === true
21808
22327
  });
21809
- let picked = options.picked ?? null;
22328
+ const pickable = options.pickable !== false;
22329
+ let picked = pickable ? options.picked ?? null : null;
21810
22330
  const root = el2(doc, "div", "knobs-wt-options");
21811
- root.setAttribute("role", "radiogroup");
21812
- root.setAttribute("aria-label", "Options");
22331
+ if (pickable) {
22332
+ root.setAttribute("role", "radiogroup");
22333
+ root.setAttribute("aria-label", "Options");
22334
+ } else
22335
+ root.dataset.static = "";
21813
22336
  root.style.setProperty("--kw-n", String(Math.min(list.length, 3)));
21814
22337
  if (!container.closest(".knobs-ui"))
21815
22338
  applyLook2(root, options);
21816
22339
  const slots = [];
21817
22340
  const tiles = list.map((o, i) => {
21818
22341
  const tile = el2(doc, "div", "knobs-wt-option");
22342
+ if (!pickable) {
22343
+ const name = el2(doc, "span", "knobs-wt-slot-label");
22344
+ name.textContent = o.label;
22345
+ tile.append(name);
22346
+ }
21819
22347
  const pick = el2(doc, "button", "knobs-wt-pick");
21820
22348
  pick.type = "button";
21821
22349
  pick.setAttribute("role", "radio");
@@ -21844,7 +22372,8 @@ function tasteTest(container, options) {
21844
22372
  api.pick(next);
21845
22373
  options.onPick?.(next);
21846
22374
  });
21847
- tile.append(pick);
22375
+ if (pickable)
22376
+ tile.append(pick);
21848
22377
  if (o.media)
21849
22378
  slots.push(mediaSlot(tile, {
21850
22379
  ...look,
@@ -21927,7 +22456,8 @@ function walkthrough(container, options) {
21927
22456
  let tocRows = [];
21928
22457
  let discs = [];
21929
22458
  let full = null;
21930
- const noteMode = options.noteBar ?? "collapsed";
22459
+ const review = options.review === true;
22460
+ const noteMode = review ? options.noteBar ?? "collapsed" : "hidden";
21931
22461
  let reserve = noteBarReserve(52, DOCK_INSET, DOCK_GAP);
21932
22462
  const viewOf = (key) => views.find((v) => v.step.key === key);
21933
22463
  const feedback = () => collectFeedback(page, state);
@@ -22663,7 +23193,14 @@ function walkthrough(container, options) {
22663
23193
  line.append(s);
22664
23194
  return s;
22665
23195
  };
23196
+ const findPRIn = (n) => findPR(page.prs, n, page.repo);
22666
23197
  const metaLink = (line, text, href) => {
23198
+ const n = prNumber(href);
23199
+ if (n) {
23200
+ const chip = prChip(doc, n, { href, ...findPRIn(n) });
23201
+ line.append(chip);
23202
+ return chip;
23203
+ }
22667
23204
  const a = el2(doc, "a");
22668
23205
  a.href = href;
22669
23206
  a.target = "_blank";
@@ -22750,8 +23287,9 @@ function walkthrough(container, options) {
22750
23287
  views = [];
22751
23288
  root.textContent = "";
22752
23289
  root.removeAttribute("data-settled");
22753
- root.dataset.rail = options.rail === false ? "none" : options.rail === "left" ? "left" : "right";
23290
+ root.dataset.rail = options.rail === false ? "none" : options.rail === "right" ? "right" : "left";
22754
23291
  root.toggleAttribute("data-cards", options.cards === true);
23292
+ root.toggleAttribute("data-review", review);
22755
23293
  const layout = el2(doc, "div", "knobs-wt-layout");
22756
23294
  const main = el2(doc, "div", "knobs-wt-main");
22757
23295
  const flow = el2(doc, "div", "knobs-wt-flow");
@@ -22761,6 +23299,8 @@ function walkthrough(container, options) {
22761
23299
  head.append(h1);
22762
23300
  if (page.summary)
22763
23301
  head.append(prose2(doc, "p", "knobs-wt-summary", page.summary));
23302
+ if (page.more)
23303
+ head.append(more(page.more));
22764
23304
  if (page.meta.length) {
22765
23305
  const meta = el2(doc, "div", "knobs-wt-meta");
22766
23306
  for (const m of page.meta) {
@@ -22804,28 +23344,28 @@ function walkthrough(container, options) {
22804
23344
  count = fill = tocEl = null;
22805
23345
  if (options.rail !== false) {
22806
23346
  rail = el2(doc, "aside", "knobs-wt-rail");
22807
- rail.setAttribute("aria-label", "Progress");
22808
- const progress = el2(doc, "div", "knobs-wt-progress");
22809
- count = el2(doc, "div", "knobs-wt-count");
22810
- const meter = el2(doc, "div", "knobs-wt-bar");
22811
- fill = el2(doc, "div", "knobs-wt-fill");
22812
- meter.append(fill);
22813
- progress.append(count, meter);
23347
+ rail.setAttribute("aria-label", "Contents");
22814
23348
  tocEl = el2(doc, "nav", "knobs-wt-toc");
22815
23349
  tocEl.setAttribute("aria-label", "Contents");
22816
- const ns = page.sections.map((s) => s.kicker && s.link ? s.link.label : "");
22817
- const nw = Math.max(0, ...ns.map((n) => n.length));
22818
- tocEl.style.setProperty("--kw-toc-n", nw ? `${nw + 0.5}ch` : "0px");
22819
- if (!nw)
22820
- tocEl.dataset.bare = "";
22821
23350
  if (feedEl) {
22822
23351
  const group = el2(doc, "div", "knobs-wt-toc-group");
22823
23352
  const target = feedEl;
22824
- group.append(tocRow("Latest", "", String(page.feed.length), false, () => target.scrollIntoView({ block: "start", behavior: reduced() ? "auto" : "smooth" })));
23353
+ group.append(tocRow("Latest", false, () => target.scrollIntoView({ block: "start", behavior: reduced() ? "auto" : "smooth" })));
22825
23354
  tocEl.append(group);
22826
23355
  }
22827
- rail.append(progress, tocEl, actions(true));
22828
- if (options.keys !== false) {
23356
+ if (review) {
23357
+ const progress = el2(doc, "div", "knobs-wt-progress");
23358
+ count = el2(doc, "div", "knobs-wt-count");
23359
+ const meter = el2(doc, "div", "knobs-wt-bar");
23360
+ fill = el2(doc, "div", "knobs-wt-fill");
23361
+ meter.append(fill);
23362
+ progress.append(count, meter);
23363
+ rail.append(progress);
23364
+ }
23365
+ rail.append(tocEl);
23366
+ if (review)
23367
+ rail.append(actions(true));
23368
+ if (review && options.keys !== false) {
22829
23369
  const keys = el2(doc, "div", "knobs-wt-keys");
22830
23370
  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" : "");
22831
23371
  rail.append(keys);
@@ -22835,14 +23375,15 @@ function walkthrough(container, options) {
22835
23375
  const sec = el2(doc, "section", "knobs-wt-section");
22836
23376
  sec.id = `wt-${section.id}`;
22837
23377
  const sh = el2(doc, "header", "knobs-wt-shead");
22838
- if (section.kicker || section.link || section.status) {
23378
+ const status = review ? section.status : undefined;
23379
+ if (section.kicker || section.link || status) {
22839
23380
  const meta = metaLine("knobs-wt-kicker");
22840
23381
  if (section.kicker)
22841
23382
  metaText(meta, section.kicker, "knobs-wt-kicker-lead");
22842
23383
  if (section.link)
22843
23384
  metaLink(meta, section.link.label, section.link.href);
22844
- if (section.status) {
22845
- const s = statusLook(section.status);
23385
+ if (status) {
23386
+ const s = statusLook(status);
22846
23387
  metaText(meta, s.label, "knobs-wt-status").dataset.tone = s.tone;
22847
23388
  }
22848
23389
  sh.append(meta);
@@ -22854,13 +23395,14 @@ function walkthrough(container, options) {
22854
23395
  intro.append(sh);
22855
23396
  if (section.summary)
22856
23397
  intro.append(prose2(doc, "p", "knobs-wt-ssum", section.summary));
23398
+ if (section.more)
23399
+ intro.append(more(section.more));
22857
23400
  sec.append(intro);
22858
23401
  let group = null;
22859
23402
  if (tocEl) {
22860
23403
  group = el2(doc, "div", "knobs-wt-toc-group");
22861
23404
  const first = section.steps[0]?.key;
22862
- const n = section.kicker && section.link ? section.link.label : "";
22863
- group.append(tocRow(plainMarkdown(section.title), n, "", false, () => first ? go(first) : undefined, section.id));
23405
+ group.append(tocRow(plainMarkdown(section.title), false, () => first ? go(first) : undefined, section.id));
22864
23406
  tocEl.append(group);
22865
23407
  }
22866
23408
  for (const step of section.steps) {
@@ -22870,6 +23412,7 @@ function walkthrough(container, options) {
22870
23412
  }
22871
23413
  flow.append(sec);
22872
23414
  }
23415
+ linkPRs(flow, findPRIn);
22873
23416
  main.append(flow);
22874
23417
  if (noteMode !== "hidden")
22875
23418
  main.append(bar.dock);
@@ -22898,19 +23441,25 @@ function walkthrough(container, options) {
22898
23441
  root.dataset.settled = "";
22899
23442
  }));
22900
23443
  };
22901
- const tocRow = (label, n, end, sub, run, section) => {
23444
+ const more = (text) => {
23445
+ const box = el2(doc, "div", "knobs-wt-more");
23446
+ const body = el2(doc, "div", "knobs-wt-more-text");
23447
+ for (const para of text.split(/\n\s*\n/))
23448
+ if (para.trim())
23449
+ body.append(prose2(doc, "p", "", para.trim()));
23450
+ discs.push(disclosure(box, { label: "More", content: body }));
23451
+ return box;
23452
+ };
23453
+ const tocRow = (label, sub, run, section) => {
22902
23454
  const b = el2(doc, "button", sub ? "knobs-wt-toc-row knobs-wt-toc-sub" : "knobs-wt-toc-row");
22903
23455
  b.type = "button";
22904
23456
  if (section)
22905
23457
  b.dataset.section = section;
22906
- const ncol = el2(doc, "span", "knobs-wt-toc-n");
22907
- ncol.textContent = n;
22908
23458
  const t = el2(doc, "span", "knobs-wt-toc-label");
22909
23459
  t.textContent = label;
22910
23460
  const e = el2(doc, "span", "knobs-wt-toc-end");
22911
- e.textContent = end;
22912
23461
  b.title = label;
22913
- b.append(ncol, t, e);
23462
+ b.append(t, e);
22914
23463
  b.addEventListener("mousedown", (ev) => ev.preventDefault());
22915
23464
  b.addEventListener("click", run);
22916
23465
  tocRows.push(b);
@@ -22968,33 +23517,26 @@ function walkthrough(container, options) {
22968
23517
  head.append(h3);
22969
23518
  if (step.caption)
22970
23519
  head.append(prose2(doc, "p", "knobs-wt-caption", step.caption));
23520
+ if (step.more)
23521
+ head.append(more(step.more));
22971
23522
  card.append(head);
22972
23523
  const slots = [];
22973
23524
  const onEvent = (e) => record({ ...e, step: step.key });
22974
23525
  let media = null;
22975
23526
  if (step.media.length) {
22976
- const grid = (list) => {
22977
- const g = el2(doc, "div", "knobs-wt-media");
22978
- g.style.setProperty("--kw-n", String(Math.min(list.length, step.columns)));
22979
- for (const m of list)
22980
- slots.push(mediaSlot(g, { ...look, media: m, onEvent, steps: stepLinks }));
22981
- return g;
22982
- };
22983
- const [first, ...rest] = step.media;
22984
- media = grid(step.more ? [first] : step.media);
23527
+ media = el2(doc, "div", "knobs-wt-media");
23528
+ media.style.setProperty("--kw-n", String(Math.min(step.media.length, step.columns)));
23529
+ for (const m of step.media)
23530
+ slots.push(mediaSlot(media, { ...look, media: m, onEvent, steps: stepLinks }));
22985
23531
  card.append(media);
22986
- if (step.more && rest.length) {
22987
- const holder = el2(doc, "div", "knobs-wt-more");
22988
- discs.push(disclosure(holder, { label: step.more, count: rest.length, content: grid(rest) }));
22989
- card.append(holder);
22990
- }
22991
23532
  }
22992
23533
  let taste = null;
22993
- if (step.question)
23534
+ if (step.question && review)
22994
23535
  card.append(prose2(doc, "p", "knobs-wt-question", step.question));
22995
23536
  if (step.ask === "pick") {
22996
23537
  taste = tasteTest(card, {
22997
23538
  ...look,
23539
+ pickable: review,
22998
23540
  options: step.options,
22999
23541
  picked: state.steps[step.key]?.pick ?? null,
23000
23542
  onPick: (id) => pick(step.key, id),
@@ -23003,7 +23545,8 @@ function walkthrough(container, options) {
23003
23545
  media ??= taste.element;
23004
23546
  }
23005
23547
  const notes = el2(doc, "div", "knobs-wt-notes");
23006
- card.append(notes);
23548
+ if (review)
23549
+ card.append(notes);
23007
23550
  card.addEventListener("focusin", () => {
23008
23551
  if (scroll.to)
23009
23552
  return;
@@ -23013,9 +23556,11 @@ function walkthrough(container, options) {
23013
23556
  let toc = null;
23014
23557
  let glyph = null;
23015
23558
  if (group) {
23016
- toc = tocRow(plainMarkdown(step.title), "", "", true, () => go(step.key));
23017
- glyph = el2(doc, "span", "knobs-wt-toc-glyph");
23018
- toc.querySelector(".knobs-wt-toc-end").append(glyph);
23559
+ toc = tocRow(plainMarkdown(step.title), true, () => go(step.key));
23560
+ if (review) {
23561
+ glyph = el2(doc, "span", "knobs-wt-toc-glyph");
23562
+ toc.querySelector(".knobs-wt-toc-end").append(glyph);
23563
+ }
23019
23564
  group.append(toc);
23020
23565
  }
23021
23566
  return { step, card, media, notes, toc, glyph, taste, slots };
@@ -23093,7 +23638,7 @@ function walkthrough(container, options) {
23093
23638
  return;
23094
23639
  }
23095
23640
  const v = viewOf(current);
23096
- if (!v || e.shiftKey)
23641
+ if (!v || e.shiftKey || !review)
23097
23642
  return;
23098
23643
  const choice = digitChoice(v.step, state.steps[v.step.key], key);
23099
23644
  if (choice === undefined)
@@ -23258,6 +23803,7 @@ export {
23258
23803
  mediaKind,
23259
23804
  mediaSlot,
23260
23805
  menu,
23806
+ minutes,
23261
23807
  modeForKey,
23262
23808
  motionBlur,
23263
23809
  moveHead,
@@ -23277,6 +23823,7 @@ export {
23277
23823
  plainAccess,
23278
23824
  planBento,
23279
23825
  planPill,
23826
+ player,
23280
23827
  popover,
23281
23828
  railSegments,
23282
23829
  rank,