@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-react.js CHANGED
@@ -596,6 +596,8 @@ var compareCss = `
596
596
  .knobs-cmp-stage{position:relative;flex:1;min-height:96px;overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_ku-cmp-stage);touch-action:pan-y;outline:none}
597
597
  .knobs-compare[data-frame="flat"] .knobs-cmp-stage{border-radius:0}
598
598
  .knobs-compare[data-mode="wipe"] .knobs-cmp-stage{cursor:ew-resize}
599
+ /* Videos: a click on the stage plays and pauses. */
600
+ .knobs-compare[data-video]:not([data-mode="wipe"],[data-zoomed]) .knobs-cmp-stage{cursor:pointer}
599
601
  /* Unzoomed, a finger still scrolls the page up and down; zoomed, the stage takes every gesture. */
600
602
  .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:grab;touch-action:none}
601
603
  .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:grabbing!important}
@@ -605,6 +607,9 @@ var compareCss = `
605
607
  .knobs-cmp-canvas{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
606
608
  .knobs-cmp-media{position:absolute;left:0;top:0;display:block;max-width:none;margin:0;padding:0;border:0;object-fit:fill;transform-origin:0 0;pointer-events:none;will-change:transform}
607
609
  .knobs-compare[data-renderer="gpu"] .knobs-cmp-media{opacity:0}
610
+ /* The media comes in with its first frames: a fade and a 4px blur on the default spring. */
611
+ .knobs-cmp-canvas,.knobs-compare[data-renderer="dom"] .knobs-cmp-media{transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
612
+ .knobs-compare:not([data-ready]) :is(.knobs-cmp-canvas,.knobs-cmp-media){opacity:0;filter:blur(4px)}
608
613
  /* Glass: what's behind shows through, blurred, on the knobs tokens. */
609
614
  .knobs-cmp-label,.knobs-cmp-zoom{background:color-mix(in srgb,var(--_k-bg) 52%,transparent);-webkit-backdrop-filter:blur(14px) saturate(1.6);backdrop-filter:blur(14px) saturate(1.6);
610
615
  box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim),0 2px 10px rgba(0,0,0,calc(.18 * var(--_k-shadow-k,1)))}
@@ -613,11 +618,17 @@ var compareCss = `
613
618
  white-space:nowrap;pointer-events:none;will-change:transform}
614
619
  .knobs-cmp-label[data-side="before"]{color:var(--_k-fg-3)}
615
620
  .knobs-cmp-label[data-side="after"]{color:var(--_k-fg)}
616
- .knobs-cmp-mark svg{display:block;width:12px;height:12px}
617
- /* The wipe: a hairline down the line with a raised grip at the pointer's height; a wider strip
618
- takes the hand and the keys (a slider). It fades with the mode; wiping, line and grip brighten. */
619
- .knobs-cmp-handle{cursor:ew-resize;position:absolute;left:-12px;top:0;width:24px;height:0;opacity:0;pointer-events:none;outline:none;will-change:transform;transition:opacity .18s cubic-bezier(.4,0,1,1)}
620
- .knobs-cmp-handle[data-on]{opacity:1;pointer-events:auto;transition:opacity .22s cubic-bezier(.2,.8,.2,1)}
621
+ /* Fusing, the mark folds away and the label is its name. */
622
+ .knobs-cmp-mark{flex:none;display:flex;justify-content:flex-end;overflow:hidden}
623
+ .knobs-cmp-mark svg{display:block;flex:none;width:12px;height:12px}
624
+ /* The fuse's seam: a blurred strip over where the sides meet, soft at both sides. */
625
+ .knobs-cmp-seam{position:absolute;left:0;top:0;width:8px;height:0;opacity:0;pointer-events:none;will-change:transform,opacity;
626
+ -webkit-mask-image:linear-gradient(to right,transparent,#000 35%,#000 65%,transparent);mask-image:linear-gradient(to right,transparent,#000 35%,#000 65%,transparent)}
627
+ /* The wipe: a hairline down the line with a raised grip in its middle (at the pointer's height
628
+ while dragged); a wider strip takes the hand and the keys (a slider). It fades in with the
629
+ fuse; wiping, line and grip brighten. */
630
+ .knobs-cmp-handle{cursor:ew-resize;position:absolute;left:-12px;top:0;width:24px;height:0;opacity:0;pointer-events:none;outline:none;will-change:transform,opacity}
631
+ .knobs-cmp-handle[data-on]{pointer-events:auto}
621
632
  .knobs-cmp-handle::before{content:"";position:absolute;top:0;bottom:0;left:11.5px;width:1px;background:color-mix(in srgb,var(--_ku-cmp-line) 72%,transparent);box-shadow:0 0 0 .5px rgba(0,0,0,.28),0 0 6px rgba(0,0,0,.25)}
622
633
  .knobs-compare[data-dragging] .knobs-cmp-handle::before{background:var(--_ku-cmp-line)}
623
634
  .knobs-cmp-grip{position:absolute;top:0;left:4px;width:16px;height:28px;border-radius:min(8px,calc(var(--_k-radius) * 999));background:var(--_k-bg-overlay);
@@ -649,6 +660,27 @@ var compareCss = `
649
660
  @media(prefers-reduced-motion:reduce){:is(.knobs-compare,.knobs-cmp-strip) :where(*,::before,::after){transition-duration:.12s!important}}
650
661
  `;
651
662
 
663
+ // src/ui/player-css.ts
664
+ var playerCss = `
665
+ :where(.knobs-player,.knobs-player *){box-sizing:border-box}
666
+ .knobs-ui.knobs-player{--_kp-stage:var(--_k-compare-stage);display:flex;flex-direction:column;gap:6px;width:100%;outline:none;
667
+ font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
668
+ :where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-ui.knobs-player,.knobs-ui.knobs-player[data-nested]{--_kp-stage:var(--k-compare-stage,transparent)}
669
+ .knobs-player-frame{position:relative;width:100%;aspect-ratio:var(--kp-aspect,16/9);overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_kp-stage);cursor:pointer}
670
+ .knobs-player[data-frame="flat"] .knobs-player-frame{border-radius:0}
671
+ .knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
672
+ opacity:0;filter:blur(4px);transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
673
+ .knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}
674
+ .knobs-player-glyph{position:absolute;left:50%;top:50%;display:grid;place-items:center;width:44px;height:44px;margin:-22px 0 0 -22px;border-radius:50%;color:var(--_k-fg);pointer-events:none;
675
+ opacity:0;filter:blur(4px);scale:.96;
676
+ background:color-mix(in srgb,var(--_k-bg) 52%,transparent);-webkit-backdrop-filter:blur(14px) saturate(1.6);backdrop-filter:blur(14px) saturate(1.6);
677
+ box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim);
678
+ transition:opacity calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy),filter calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy),scale calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy)}
679
+ .knobs-player-glyph svg{width:16px;height:16px}
680
+ .knobs-player-glyph[data-flash]{opacity:1;filter:none;scale:1}
681
+ .knobs-player-line{flex:none;min-height:0}
682
+ `;
683
+
652
684
  // src/ui/overlay-css.ts
653
685
  var overlayCss = `
654
686
  .knobs-layers{position:fixed;inset:0;z-index:2147480005;pointer-events:none}
@@ -1513,7 +1545,7 @@ var init_schema = __esm(() => {
1513
1545
  type: "length",
1514
1546
  group: "Type",
1515
1547
  source: value("20px"),
1516
- doc: "Section headings (a PR's title, under its kicker), weight 500."
1548
+ doc: "A page's title, weight 500."
1517
1549
  },
1518
1550
  {
1519
1551
  name: "leading-heading",
@@ -1529,7 +1561,7 @@ var init_schema = __esm(() => {
1529
1561
  type: "length",
1530
1562
  group: "Type",
1531
1563
  source: value("16px"),
1532
- doc: "A page's title, weight 500."
1564
+ doc: "Section titles (under their kicker), weight 500."
1533
1565
  },
1534
1566
  {
1535
1567
  name: "leading-title",
@@ -1561,7 +1593,7 @@ var init_schema = __esm(() => {
1561
1593
  type: "length",
1562
1594
  group: "Type",
1563
1595
  source: value("12px"),
1564
- doc: "Labels over media, weight 500; mono inside body text."
1596
+ doc: "Mono inside body text: code and paths."
1565
1597
  },
1566
1598
  {
1567
1599
  name: "leading-label",
@@ -3004,8 +3036,6 @@ var css2 = `
3004
3036
  .knobs-tl-hover{position:absolute;left:0;top:0;bottom:0;width:1px;margin-left:-.5px;background:var(--_k-border-strong);transform:translateX(-100px);pointer-events:none;opacity:0;transition:opacity .15s}
3005
3037
  .knobs-tl-track:hover .knobs-tl-hover{opacity:1}
3006
3038
  .knobs-timeline[data-scrubbing] .knobs-tl-hover{opacity:0}
3007
- @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
3008
- @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
3009
3039
  .knobs-tl-tag{position:absolute;left:0;bottom:2px;font-size:var(--_k-text-meta);line-height:16px;color:var(--_k-fg);font-variant-numeric:tabular-nums;font-feature-settings:"tnum";white-space:nowrap;pointer-events:none;opacity:0}
3010
3040
  .knobs-tl-tag[data-on]{opacity:1}
3011
3041
  /* The lane (variant: "lane"): one slim track of ticks under a compare or a video; markers are
@@ -3021,7 +3051,26 @@ var css2 = `
3021
3051
  .knobs-timeline[data-variant="lane"] .knobs-tl-head::before{top:0}
3022
3052
  .knobs-timeline[data-variant="lane"] .knobs-tl-head::after{content:none}
3023
3053
  .knobs-timeline[data-variant="lane"] .knobs-tl-tag{top:2px;bottom:auto}
3024
- `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + selectionPillCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
3054
+ /* The line (variant: "line"): a player's progress under its media. One 2px line, fg-3 with fg
3055
+ filling it, a segment per chapter; it thickens under the pointer and while scrubbing. The
3056
+ chapter and time under the pointer float over it; nothing else shows. */
3057
+ .knobs-ui.knobs-timeline[data-variant="line"]{height:auto;min-height:0;padding:0;gap:0}
3058
+ .knobs-timeline[data-variant="line"] .knobs-tl-transport{display:none}
3059
+ .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}
3060
+ .knobs-timeline[data-variant="line"] :is(.knobs-tl-ruler,.knobs-tl-lane,.knobs-tl-hover,.knobs-tl-head,.knobs-tl-rlabel){display:none}
3061
+ .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)}
3062
+ .knobs-tl-seg{position:absolute;left:0;top:0;height:100%;overflow:hidden;border-radius:1px;background:var(--_k-fg-3);will-change:transform}
3063
+ .knobs-tl-seg-fill{position:absolute;inset:0;background:var(--_k-fg);transform-origin:0 0;will-change:transform}
3064
+ .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}
3065
+ .knobs-timeline[data-variant="line"] .knobs-tl-range{top:3px;bottom:3px;border-radius:3px}
3066
+ .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)}
3067
+ .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);
3068
+ -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:inset 0 0 0 .5px var(--_k-border)}
3069
+ .knobs-tl-tag-label{color:var(--_k-fg-2)}
3070
+ .knobs-tl-tag-label:empty{display:none}
3071
+ @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
3072
+ @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
3073
+ `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + playerCss + selectionPillCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
3025
3074
  s.omega = m.omega;
3026
3075
  s.zeta = m.zeta;
3027
3076
  return s;
@@ -3144,19 +3193,19 @@ var init_diff_css = __esm(() => {
3144
3193
  @property --kd-fb{syntax:"<length>";inherits:false;initial-value:0px}
3145
3194
  :where(.knobs-ui){--kd-add:var(--_k-diff-add);--kd-del:var(--_k-diff-del);--kd-mod:var(--_k-diff-mod);--kd-move:var(--k-diff-move,var(--_k-info))}
3146
3195
  :where(.knobs-diff,.knobs-diff *,.knobs-diff-files,.knobs-diff-files *){box-sizing:border-box}
3147
- .knobs-ui.knobs-diff{--kd-bg:var(--k-diff-bg,var(--_kd-bg,var(--_k-bg)));--kd-solid:var(--kd-fill,var(--kd-bg));--kd-line:calc(var(--_k-font-size) * 1.5);
3196
+ .knobs-ui.knobs-diff{--kd-bg:var(--k-diff-bg,var(--_kd-bg,transparent));--kd-solid:var(--kd-fill,var(--kd-bg));--kd-line:calc(var(--_k-font-size) * 1.5);
3148
3197
  --kd-i:var(--k-diff-inset,8px);--kd-x:var(--kd-i);--kd-inner:max(3px,calc(var(--_ku-pane-radius) - var(--kd-i)));--kd-ctl:var(--kd-inner);
3149
3198
  --kd-head:calc(var(--_k-control-h) + 2 * var(--kd-i));--kd-fade:var(--k-diff-fade,20px);--kd-stick:0px;position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;
3150
3199
  background:var(--kd-bg);border-radius:var(--_ku-pane-radius);font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);text-align:left;
3151
3200
  letter-spacing:normal;word-spacing:normal;text-transform:none;-webkit-font-smoothing:antialiased}
3152
3201
  .knobs-ui.knobs-diff[data-height="auto"]{height:auto}
3153
- .knobs-diff::after{content:"";position:absolute;inset:0;z-index:3;border-radius:inherit;box-shadow:var(--_ku-pane-ring);pointer-events:none}
3202
+ .knobs-ui.knobs-diff:not([data-on="pane"])::after{content:"";position:absolute;inset:0;z-index:3;box-shadow:inset 0 -1px 0 var(--_k-border-subtle);pointer-events:none}
3154
3203
  .knobs-diff [hidden],.knobs-diff-files [hidden]{display:none!important}
3155
3204
  .knobs-diff svg{display:block;flex:none}
3156
- .knobs-ui.knobs-diff[data-on="surface"]{--kd-x:0px;--kd-head:var(--_k-control-h);--kd-ctl:var(--_k-radius)}
3157
- /* The header's hairline shows only once the code has scrolled under it. */
3205
+ .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)}
3206
+ /* A hairline under the header, firmer once the code has scrolled under it. */
3158
3207
  .knobs-diff-head{position:relative;z-index:1;flex:none;display:flex;align-items:center;gap:8px;height:var(--kd-head);padding:0 var(--kd-x);
3159
- box-shadow:inset 0 -1px transparent;transition:box-shadow .15s;user-select:none;-webkit-user-select:none}
3208
+ box-shadow:inset 0 -1px var(--_k-border-subtle);transition:box-shadow .15s;user-select:none;-webkit-user-select:none}
3160
3209
  .knobs-diff[data-scrolled]>.knobs-diff-head{box-shadow:inset 0 -1px var(--_k-border)}
3161
3210
  .knobs-diff-path{flex:0 1 auto;min-width:0;overflow:hidden;white-space:nowrap}
3162
3211
  .knobs-diff-dir,.knobs-diff-from{color:var(--_k-fg-3)}
@@ -3194,6 +3243,15 @@ var init_diff_css = __esm(() => {
3194
3243
  .knobs-ui .knobs-diff-viewed[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
3195
3244
  .knobs-diff-viewed svg{width:16px;height:16px}
3196
3245
  .knobs-diff-viewed[aria-pressed="true"] svg{color:var(--kd-add)}
3246
+ /* Narrow, the header gives up words before anything is cut off at its end: Viewed keeps its
3247
+ check, then the counts' bar goes, then the counts; the path fades at its end throughout. */
3248
+ .knobs-diff-head,.knobs-diff-part-head{container-type:inline-size}
3249
+ @container (max-width:520px){
3250
+ .knobs-diff-viewed-label{display:none}
3251
+ .knobs-ui .knobs-diff-viewed{width:var(--_k-control-h);padding:0;justify-content:center}
3252
+ .knobs-diff-counts .knobs-diff-bar{display:none}
3253
+ }
3254
+ @container (max-width:400px){.knobs-diff-counts{display:none}.knobs-diff-nav>span{min-width:2.6em}}
3197
3255
  .knobs-diff-path{transition:opacity .2s}
3198
3256
  .knobs-diff[data-viewed]>.knobs-diff-head .knobs-diff-path,.knobs-diff-part[data-viewed] .knobs-diff-path{opacity:.6}
3199
3257
  /* Split | Unified: the selection pill on the one picked, its bright copy the picked one's text; a
@@ -3258,24 +3316,26 @@ var init_diff_css = __esm(() => {
3258
3316
  --diffs-bg-deletion-number-override:color-mix(in srgb,var(--kd-del) 55%,var(--kd-solid));
3259
3317
  --diffs-bg-addition-emphasis-override:color-mix(in srgb,var(--kd-add) 22%,transparent);
3260
3318
  --diffs-bg-deletion-emphasis-override:color-mix(in srgb,var(--kd-del) 22%,transparent);
3261
- --diffs-bg-separator-override:color-mix(in srgb,var(--_k-fg) 4.5%,var(--kd-solid));
3262
- --diffs-bg-context-override:color-mix(in srgb,var(--_k-fg) 3%,var(--kd-solid));
3263
- --diffs-bg-buffer-override:color-mix(in srgb,var(--_k-fg) 3.5%,var(--kd-solid));
3319
+ /* Unchanged lines and the folds between hunks sit on the void; only a split's missing lines
3320
+ are faintly filled. */
3321
+ --diffs-bg-separator-override:var(--kd-solid);
3322
+ --diffs-bg-context-override:var(--kd-solid);
3323
+ --diffs-bg-buffer-override:color-mix(in srgb,var(--_k-fg) 2.5%,var(--kd-solid));
3264
3324
  --diffs-bg-selection-override:var(--_k-accent);
3265
3325
  --diffs-fg-number-override:var(--_k-fg-3);
3266
3326
  }
3267
- /* diffReview: one card, the file list a drawer at its left (its width springs; the list inside
3268
- is pinned to its right edge, so it slides out from under the diff), parted from the diff by a
3269
- hairline. The diff in it draws no card of its own. */
3270
- .knobs-ui.knobs-diff-review{position:relative;display:flex;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;border-radius:var(--_ku-pane-radius);background:var(--_ku-pane-bg);
3327
+ /* diffReview, on the void as the diff is: the file list a drawer at its left (its width springs;
3328
+ the list inside is pinned to its right edge, so it slides out from under the diff), parted
3329
+ from the diff by a hairline; one hairline runs under both headers and one under it all. */
3330
+ .knobs-ui.knobs-diff-review{position:relative;display:flex;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;
3271
3331
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);text-align:left;letter-spacing:normal;-webkit-font-smoothing:antialiased}
3272
- .knobs-diff-review::after{content:"";position:absolute;inset:0;z-index:5;border-radius:inherit;box-shadow:var(--_ku-pane-ring);pointer-events:none}
3332
+ .knobs-diff-review::after{content:"";position:absolute;inset:0;z-index:5;box-shadow:inset 0 -1px 0 var(--_k-border-subtle);pointer-events:none}
3273
3333
  :where(.knobs-diff-review,.knobs-diff-review *){box-sizing:border-box}
3274
- .knobs-diff-drawer{position:relative;flex:none;width:0;height:100%;overflow:hidden;box-shadow:inset -1px 0 var(--_k-border)}
3334
+ .knobs-diff-drawer{position:relative;flex:none;width:0;height:100%;overflow:hidden;box-shadow:inset -1px 0 var(--_k-border-subtle)}
3275
3335
  .knobs-diff-drawer[data-shut]{visibility:hidden}
3276
3336
  .knobs-diff-drawer-in{position:absolute;top:0;bottom:0;right:0;display:flex;flex-direction:column}
3277
3337
  .knobs-diff-drawer-head{flex:none;display:flex;align-items:center;gap:6px;height:calc(var(--_k-control-h) + 2 * var(--k-diff-inset,8px));padding:0 var(--k-diff-inset,8px) 0 calc(var(--_ku-row-x) + 8px);
3278
- user-select:none;-webkit-user-select:none}
3338
+ box-shadow:inset 0 -1px var(--_k-border-subtle);user-select:none;-webkit-user-select:none}
3279
3339
  .knobs-diff-drawer-title{font-weight:var(--_k-weight-title)}
3280
3340
  .knobs-diff-drawer-count{color:var(--_k-fg-3);font-size:var(--_k-text-meta);font-variant-numeric:tabular-nums}
3281
3341
  .knobs-diff-drawer-in>.knobs-diff-files{flex:1;min-height:0;height:auto}
@@ -3287,10 +3347,11 @@ var init_diff_css = __esm(() => {
3287
3347
  .knobs-ui .knobs-diff-toggle[aria-pressed]:not(:hover,:focus-visible){background:transparent}
3288
3348
  /* diffReview's file list: a filter over a compact tree, a status letter, the counts and their
3289
3349
  bar on each file; one highlight glides to the file on show. Viewed files dim, with a tick. */
3290
- .knobs-diff-files{--k-row:var(--k-diff-row,28px);display:flex;flex-direction:column;height:100%;min-height:0;padding-top:2px}
3350
+ .knobs-diff-files{--k-row:var(--k-diff-row,28px);display:flex;flex-direction:column;height:100%;min-height:0;padding-top:4px}
3291
3351
  .knobs-diff-filter{position:relative;flex:none;display:flex;align-items:center;gap:6px;height:26px;margin:0 var(--_ku-row-x) 4px;padding:0 4px 0 8px;
3292
- border-radius:max(6px,var(--_ku-row-radius));background:var(--_ku-wash);color:var(--_k-fg-3);box-shadow:inset 0 0 0 1px transparent;transition:background .15s,box-shadow .15s}
3293
- .knobs-diff-filter:focus-within{background:var(--_ku-wash-hi);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_k-accent) 45%,transparent)}
3352
+ 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}
3353
+ /* The filter is a line of text, no box; focused, a hairline ring. */
3354
+ .knobs-diff-filter:focus-within{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_k-accent) 45%,transparent)}
3294
3355
  .knobs-diff-filter svg{width:12px;height:12px;flex:none}
3295
3356
  .knobs-ui .knobs-diff-filter>input{flex:1;min-width:0;height:100%;font:var(--_k-weight) var(--_k-font-size)/1 var(--_k-font);color:var(--_k-fg);caret-color:var(--_k-accent);background:transparent}
3296
3357
  .knobs-ui .knobs-diff-filter>input::placeholder{color:var(--_k-fg-3)}
@@ -6215,7 +6276,10 @@ function codeDiff(container, options) {
6215
6276
  const viewedToggle = () => {
6216
6277
  const b = make("button", "knobs-diff-viewed");
6217
6278
  b.type = "button";
6218
- b.append(make("span", ""), "Viewed");
6279
+ const label = make("span", "knobs-diff-viewed-label");
6280
+ label.textContent = "Viewed";
6281
+ b.append(make("span", ""), label);
6282
+ b.setAttribute("aria-label", "Viewed");
6219
6283
  return b;
6220
6284
  };
6221
6285
  const paintViewed = (b, on) => {
@@ -8502,6 +8566,17 @@ function currentMarker(markers, t, fps) {
8502
8566
  best = m;
8503
8567
  return best;
8504
8568
  }
8569
+ function minutes(t) {
8570
+ const s = Math.max(0, Math.floor(t + 0.000001));
8571
+ const h = Math.floor(s / 3600);
8572
+ const m = Math.floor(s % 3600 / 60);
8573
+ const ss = String(s % 60).padStart(2, "0");
8574
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
8575
+ }
8576
+ function chapterBounds(times, duration) {
8577
+ const inside = times.filter((t) => t > 0.000001 && t < duration - 0.000001);
8578
+ return [0, ...[...new Set(inside)].sort((a, b) => a - b), duration];
8579
+ }
8505
8580
  var SECOND_STEPS, FRAME_STEPS, toFrame = (t, fps) => Math.round(t * fps), frameTime = (t, fps) => Math.round(t * fps) / fps, round = (t) => Math.round(t * 1e6) / 1e6, CATCH_UP;
8506
8581
  var init_timeline_model = __esm(() => {
8507
8582
  SECOND_STEPS = [0.1, 0.2, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600];
@@ -8519,7 +8594,8 @@ function timeline(container, options) {
8519
8594
  let speed = options.speed ?? 1;
8520
8595
  let repeat = options.repeat ?? true;
8521
8596
  const arrows = options.arrows ?? "frames";
8522
- const lane = options.variant === "lane";
8597
+ const line = options.variant === "line";
8598
+ const lane = line || options.variant === "lane";
8523
8599
  let time = Math.min(duration, Math.max(0, options.time ?? 0));
8524
8600
  let playing = false;
8525
8601
  let markers = [...options.markers ?? []];
@@ -8554,7 +8630,7 @@ function timeline(container, options) {
8554
8630
  root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight , . Home End [ ] I O M");
8555
8631
  applyLook2(root, options);
8556
8632
  if (lane)
8557
- root.dataset.variant = "lane";
8633
+ root.dataset.variant = line ? "line" : "lane";
8558
8634
  const transport = el("div", "knobs-tl-transport");
8559
8635
  const playBtn = el("button", "knobs-tl-play");
8560
8636
  playBtn.type = "button";
@@ -8567,6 +8643,10 @@ function timeline(container, options) {
8567
8643
  const hover = el("div", "knobs-tl-hover");
8568
8644
  const tag = el("span", "knobs-tl-tag");
8569
8645
  tag.setAttribute("aria-hidden", "true");
8646
+ const tagLabel = el("span", "knobs-tl-tag-label");
8647
+ const tagTime = el("span", "knobs-tl-tag-time");
8648
+ tag.append(tagLabel, tagTime);
8649
+ const segs = el("div", "knobs-tl-segs");
8570
8650
  const head = el("div", "knobs-tl-head");
8571
8651
  head.id = `${id}-head`;
8572
8652
  head.setAttribute("role", "slider");
@@ -8574,11 +8654,13 @@ function timeline(container, options) {
8574
8654
  head.setAttribute("aria-valuemin", "0");
8575
8655
  head.setAttribute("aria-valuemax", String(duration));
8576
8656
  track.append(canvas, markerLane, hover, head, tag);
8657
+ if (line)
8658
+ track.insertBefore(segs, markerLane);
8577
8659
  root.append(transport, track);
8578
8660
  container.append(root);
8579
8661
  let width = 1;
8580
8662
  let height = 1;
8581
- const fitView = () => clampView({ start: 0, span: duration }, duration, { pad: 0.02 });
8663
+ const fitView = () => clampView({ start: 0, span: duration }, duration, { pad: line ? 0 : 0.02 });
8582
8664
  let target = fitView();
8583
8665
  const vStart = new Spring(target.start, INSTANT2.omega, 1, 0.00001);
8584
8666
  const vSpan = new Spring(target.span, INSTANT2.omega, 1, 0.00001);
@@ -8587,13 +8669,13 @@ function timeline(container, options) {
8587
8669
  const xOf = (t) => (t - vStart.x) * pxPerSec();
8588
8670
  const tOf = (x) => vStart.x + x / pxPerSec();
8589
8671
  const minSpan = () => Math.max(4 / fps, 0.05);
8590
- const retarget = (next, motion = INSTANT2) => {
8591
- target = clampView(next, duration, { pad: 0.04, minSpan: minSpan() });
8672
+ const retarget = (next, motion = INSTANT2, hand = false) => {
8673
+ target = clampView(next, duration, { pad: line ? 0 : 0.04, minSpan: minSpan() });
8592
8674
  for (const s of [vStart, vSpan]) {
8593
8675
  s.omega = motion.omega;
8594
8676
  s.zeta = motion.zeta;
8595
8677
  }
8596
- if (reducedMotion(win) && motion === CALM) {
8678
+ if (hand || reducedMotion(win) && motion === CALM) {
8597
8679
  vStart.snap(target.start);
8598
8680
  vSpan.snap(target.span);
8599
8681
  } else {
@@ -8619,6 +8701,8 @@ function timeline(container, options) {
8619
8701
  };
8620
8702
  let drawn = "";
8621
8703
  const drawRuler = () => {
8704
+ if (line)
8705
+ return;
8622
8706
  const v = view();
8623
8707
  const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${height}:${units}:${fps}`;
8624
8708
  if (key === drawn)
@@ -8706,6 +8790,8 @@ function timeline(container, options) {
8706
8790
  n.title = `${m.label ? `${m.label} · ` : ""}${clock(m.time)}`;
8707
8791
  }
8708
8792
  placeMarkers();
8793
+ if (line)
8794
+ syncSegments();
8709
8795
  };
8710
8796
  const placeMarkers = () => {
8711
8797
  for (const m of markers) {
@@ -8714,6 +8800,36 @@ function timeline(container, options) {
8714
8800
  n.style.transform = `translateX(${xOf(m.time).toFixed(2)}px)`;
8715
8801
  }
8716
8802
  };
8803
+ let bounds = [];
8804
+ const segEls = [];
8805
+ const syncSegments = () => {
8806
+ bounds = chapterBounds(markers.map((m) => m.time), duration);
8807
+ while (segEls.length > bounds.length - 1)
8808
+ segEls.pop().el.remove();
8809
+ while (segEls.length < bounds.length - 1) {
8810
+ const n = el("i", "knobs-tl-seg");
8811
+ const fill = el("b", "knobs-tl-seg-fill");
8812
+ n.append(fill);
8813
+ segs.append(n);
8814
+ segEls.push({ el: n, fill });
8815
+ }
8816
+ placeSegments();
8817
+ };
8818
+ const placeSegments = () => {
8819
+ if (!line)
8820
+ return;
8821
+ const last = segEls.length - 1;
8822
+ segEls.forEach((s, i) => {
8823
+ const a = bounds[i];
8824
+ const b = bounds[i + 1];
8825
+ const x0 = xOf(a) + (i > 0 ? SEG_GAP / 2 : 0);
8826
+ const x1 = xOf(b) - (i < last ? SEG_GAP / 2 : 0);
8827
+ s.el.style.transform = `translateX(${x0.toFixed(2)}px)`;
8828
+ s.el.style.width = `${Math.max(0, x1 - x0).toFixed(2)}px`;
8829
+ const k = Math.min(1, Math.max(0, (shownHead - a) / Math.max(0.000000001, b - a)));
8830
+ s.fill.style.transform = `scaleX(${k.toFixed(4)})`;
8831
+ });
8832
+ };
8717
8833
  const syncRanges = () => {
8718
8834
  const keep = new Set(ranges.map((r) => r.id));
8719
8835
  for (const [k, n] of rangeEls)
@@ -8730,7 +8846,7 @@ function timeline(container, options) {
8730
8846
  b.dataset.edge = "end";
8731
8847
  const label = el("span", "knobs-tl-rlabel");
8732
8848
  n.append(label, a, b);
8733
- track.insertBefore(n, markerLane);
8849
+ track.insertBefore(n, line ? segs : markerLane);
8734
8850
  rangeEls.set(r.id, n);
8735
8851
  if (built)
8736
8852
  enter(n);
@@ -8782,6 +8898,7 @@ function timeline(container, options) {
8782
8898
  let currentId = null;
8783
8899
  const renderHead = () => {
8784
8900
  head.style.transform = `translateX(${xOf(shownHead).toFixed(2)}px)`;
8901
+ placeSegments();
8785
8902
  head.setAttribute("aria-valuenow", time.toFixed(3));
8786
8903
  head.setAttribute("aria-valuetext", `${time.toFixed(2)} seconds, frame ${toFrame(time, fps)}`);
8787
8904
  const cur = currentMarker(markers, time, fps)?.id ?? null;
@@ -9045,14 +9162,25 @@ function timeline(container, options) {
9045
9162
  setTime(s.t, "scrub", s.shown, press);
9046
9163
  root.toggleAttribute("data-stuck", s.stop !== null);
9047
9164
  };
9165
+ let pointerX = null;
9166
+ const zoomAtHand = (factor) => zoom(factor, pointerX === null ? time : target.start + pointerX / width * target.span);
9048
9167
  const showTag = (x) => {
9049
9168
  const at = Math.min(duration, Math.max(0, tOf(x)));
9050
- const text = `${at.toFixed(2)} s`;
9051
- if (tag.textContent !== text)
9052
- tag.textContent = text;
9169
+ const text = line ? minutes(at) : `${at.toFixed(2)} s`;
9170
+ if (tagTime.textContent !== text)
9171
+ tagTime.textContent = text;
9172
+ const label = line ? currentMarker(markers, at, fps)?.label ?? "" : "";
9173
+ if (tagLabel.textContent !== label)
9174
+ tagLabel.textContent = label;
9053
9175
  const cx = Math.min(width, Math.max(0, x));
9054
- const flip = cx > width - tag.offsetWidth - 12;
9055
- tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
9176
+ if (line) {
9177
+ const w = tag.offsetWidth;
9178
+ const left = Math.min(Math.max(0, width - w), Math.max(0, cx - w / 2));
9179
+ tag.style.transform = `translateX(${left.toFixed(1)}px)`;
9180
+ } else {
9181
+ const flip = cx > width - tag.offsetWidth - 12;
9182
+ tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
9183
+ }
9056
9184
  tag.dataset.on = "";
9057
9185
  };
9058
9186
  const hideTag = () => delete tag.dataset.on;
@@ -9113,6 +9241,7 @@ function timeline(container, options) {
9113
9241
  });
9114
9242
  track.addEventListener("pointermove", (e) => {
9115
9243
  const x = localX(e);
9244
+ pointerX = x;
9116
9245
  hover.style.transform = `translateX(${x.toFixed(1)}px)`;
9117
9246
  if (!drag || drag.kind === "scrub")
9118
9247
  showTag(x);
@@ -9186,6 +9315,7 @@ function timeline(container, options) {
9186
9315
  track.addEventListener("pointercancel", endDrag);
9187
9316
  track.addEventListener("lostpointercapture", endDrag);
9188
9317
  track.addEventListener("pointerleave", () => {
9318
+ pointerX = null;
9189
9319
  if (drag)
9190
9320
  return;
9191
9321
  hover.style.transform = "translateX(-100px)";
@@ -9193,7 +9323,7 @@ function timeline(container, options) {
9193
9323
  });
9194
9324
  track.addEventListener("dblclick", (e) => {
9195
9325
  if (lane)
9196
- return;
9326
+ return fit();
9197
9327
  const markerEl = e.target.closest(".knobs-tl-marker");
9198
9328
  if (markerEl)
9199
9329
  return rename(markerEl.dataset.id);
@@ -9204,18 +9334,40 @@ function timeline(container, options) {
9204
9334
  rename(m.id);
9205
9335
  });
9206
9336
  track.addEventListener("wheel", (e) => {
9207
- if (lane)
9337
+ const zooming = e.ctrlKey || e.metaKey;
9338
+ if (zooming && pinching)
9339
+ return e.preventDefault();
9340
+ const sideways = Math.abs(e.deltaX) > Math.abs(e.deltaY);
9341
+ if (lane && !zooming && !sideways)
9208
9342
  return;
9209
9343
  e.preventDefault();
9210
9344
  const scale = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? height : 1;
9211
- if (e.ctrlKey || e.metaKey) {
9345
+ if (zooming) {
9212
9346
  const at = target.start + localX(e) / width * target.span;
9213
- retarget(zoomView(target, Math.exp(-e.deltaY * scale * (e.ctrlKey ? 0.01 : 0.002)), at));
9347
+ const k = Math.exp(-e.deltaY * scale * (e.ctrlKey ? 0.01 : 0.002));
9348
+ retarget(zoomView(target, k, at), INSTANT2, lane);
9214
9349
  } else {
9215
- const d = (Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY) * scale;
9216
- retarget({ ...target, start: target.start + d / width * target.span });
9350
+ const d = (sideways ? e.deltaX : e.deltaY) * scale;
9351
+ retarget({ ...target, start: target.start + d / width * target.span }, INSTANT2, lane);
9217
9352
  }
9218
9353
  }, { passive: false });
9354
+ let pinchScale = 1;
9355
+ let pinching = false;
9356
+ const onGesture = (e) => {
9357
+ const g = e;
9358
+ e.preventDefault();
9359
+ if (e.type === "gesturestart")
9360
+ pinchScale = 1;
9361
+ pinching = e.type !== "gestureend";
9362
+ if (e.type !== "gesturechange")
9363
+ return;
9364
+ const s = g.scale ?? 1;
9365
+ const x = g.clientX !== undefined ? localX({ clientX: g.clientX }) : width / 2;
9366
+ retarget(zoomView(target, s / pinchScale, target.start + x / width * target.span), INSTANT2, true);
9367
+ pinchScale = s;
9368
+ };
9369
+ for (const type of ["gesturestart", "gesturechange", "gestureend"])
9370
+ track.addEventListener(type, onGesture);
9219
9371
  root.addEventListener("keydown", (e) => {
9220
9372
  if (e.target !== root || e.metaKey || e.ctrlKey) {
9221
9373
  if ((e.metaKey || e.ctrlKey) && (e.key === "=" || e.key === "-" || e.key === "0") && e.target === root) {
@@ -9223,7 +9375,7 @@ function timeline(container, options) {
9223
9375
  if (e.key === "0")
9224
9376
  fit();
9225
9377
  else
9226
- zoom(e.key === "=" ? 1.5 : 1 / 1.5);
9378
+ zoomAtHand(e.key === "=" ? 1.5 : 1 / 1.5);
9227
9379
  }
9228
9380
  return;
9229
9381
  }
@@ -9305,18 +9457,12 @@ function timeline(container, options) {
9305
9457
  return;
9306
9458
  case "=":
9307
9459
  case "+":
9308
- if (lane)
9309
- return;
9310
9460
  claim();
9311
- return zoom(1.5);
9461
+ return zoomAtHand(1.5);
9312
9462
  case "-":
9313
- if (lane)
9314
- return;
9315
9463
  claim();
9316
- return zoom(1 / 1.5);
9464
+ return zoomAtHand(1 / 1.5);
9317
9465
  case "0":
9318
- if (lane)
9319
- return;
9320
9466
  claim();
9321
9467
  return fit();
9322
9468
  }
@@ -9532,7 +9678,7 @@ function timeline(container, options) {
9532
9678
  };
9533
9679
  return api;
9534
9680
  }
9535
- var RULER = 22, INSTANT2, CALM, GLIDE, uid2 = 0, PLAY2 = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.6v8.8c0 .5.5.8.9.5l6.6-4.4c.4-.3.4-.8 0-1L5.9 3.1c-.4-.3-.9 0-.9.5Z" fill="currentColor"/></svg>', PAUSE = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="4" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/><rect x="9.2" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/></svg>';
9681
+ var RULER = 22, SEG_GAP = 3, INSTANT2, CALM, GLIDE, uid2 = 0, PLAY2 = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.6v8.8c0 .5.5.8.9.5l6.6-4.4c.4-.3.4-.8 0-1L5.9 3.1c-.4-.3-.9 0-.9.5Z" fill="currentColor"/></svg>', PAUSE = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="4" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/><rect x="9.2" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/></svg>';
9536
9682
  var init_timeline = __esm(() => {
9537
9683
  init_rgba();
9538
9684
  init_motion();
@@ -9604,15 +9750,28 @@ function compareLayout(box, before, after) {
9604
9750
  const shape = after.w > 0 && after.h > 0 ? after : before;
9605
9751
  const pair = letterbox({ w: shape.w * 2, h: shape.h }, box);
9606
9752
  const half = pair.w / 2;
9753
+ const fused = { x: pair.x + half / 2, y: pair.y, w: half, h: pair.h };
9607
9754
  return {
9608
9755
  side: {
9609
9756
  before: letterbox(before, { x: pair.x, y: pair.y, w: half, h: pair.h }),
9610
9757
  after: letterbox(after, { x: pair.x + half, y: pair.y, w: half, h: pair.h }),
9611
9758
  split: pair.x + half
9612
9759
  },
9613
- overlay: { before: letterbox(before, box), after: letterbox(after, box) }
9760
+ overlay: { before: letterbox(before, fused), after: letterbox(after, fused) }
9614
9761
  };
9615
9762
  }
9763
+ function fuseFrame(L, before, after, k, wipe) {
9764
+ const t = Math.min(1, Math.max(0, k));
9765
+ const o = L.overlay;
9766
+ const left = Math.min(o.before.x, o.after.x);
9767
+ 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);
9768
+ const cut = L.side.split + (line - L.side.split) * t;
9769
+ const contact = Math.max(0, Math.min(cut - after.x, before.x + before.w - cut));
9770
+ const feather = Math.min(contact, 1 + FUSE.feather * Math.sin(Math.PI * t));
9771
+ const overlap = before.x + before.w - after.x;
9772
+ const seam = t > 0.001 && overlap >= 0 ? Math.min(1, Math.max(0, overlap / FUSE.seamFade)) * Math.min(1, t * 4) : 0;
9773
+ return { cut, feather, seam, blur: t < 0.995 ? FUSE.blur * (1 - t) : 0 };
9774
+ }
9616
9775
  function clampZoom(r) {
9617
9776
  const w = Math.min(1, Math.max(1 / ZOOM_MAX, r.w));
9618
9777
  const u = Math.min(1 - w, Math.max(0, r.u));
@@ -9698,7 +9857,15 @@ function stripPeaks(diffs, o = {}) {
9698
9857
  });
9699
9858
  return out;
9700
9859
  }
9701
- var ALL_MODES, MODE_KEYS, modeKey = (mode) => MODE_KEYS[mode].toUpperCase(), VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), lastFrame = (duration, fps) => Math.max(0, Math.ceil(duration * fps - 0.0001) - 1), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, splitHeight = (w, media) => media.w > 0 && media.h > 0 ? w / 2 * (media.h / media.w) : w / 2 * (9 / 16), ZOOM_FIT, ZOOM_MAX = 16, panBy = (r, dx, dy, base) => clampZoom({
9860
+ var ALL_MODES, MODE_KEYS, modeKey = (mode) => MODE_KEYS[mode].toUpperCase(), VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), lastFrame = (duration, fps) => Math.max(0, Math.ceil(duration * fps - 0.0001) - 1), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, FUSE, labelFold = (fold, mark, blur = 3) => {
9861
+ const f = Math.min(1, Math.max(0, fold));
9862
+ const open = 1 - f;
9863
+ return {
9864
+ width: mark * open,
9865
+ opacity: Math.min(1, Math.max(0, (open - 0.2) / 0.8)),
9866
+ blur: blur * Math.min(1, Math.max(0, (f - 0.25) / 0.75))
9867
+ };
9868
+ }, splitHeight = (w, media) => media.w > 0 && media.h > 0 ? w / 2 * (media.h / media.w) : w / 2 * (9 / 16), ZOOM_FIT, ZOOM_MAX = 16, panBy = (r, dx, dy, base) => clampZoom({
9702
9869
  u: r.u - dx / Math.max(1, base.w) * r.w,
9703
9870
  v: r.v - dy / Math.max(1, base.h) * r.h,
9704
9871
  w: r.w
@@ -9721,6 +9888,7 @@ var init_compare_model = __esm(() => {
9721
9888
  MODE_KEYS = { side: "s", wipe: "w", difference: "d" };
9722
9889
  VIDEO = /\.(mp4|m4v|mov|webm|ogv|mkv)$/i;
9723
9890
  SEEK_IDLE = { seeking: false, pending: null };
9891
+ FUSE = { feather: 24, blur: 4, seamFade: 48 };
9724
9892
  ZOOM_FIT = { u: 0, v: 0, w: 1, h: 1 };
9725
9893
  });
9726
9894
 
@@ -10615,11 +10783,6 @@ fn pm(c: vec4f) -> vec4f { return vec4f(c.rgb * c.a, c.a); }
10615
10783
 
10616
10784
  fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
10617
10785
  switch mode {
10618
- case 1u: {
10619
- let s = max(u.wipe.y, 0.5);
10620
- let m = smoothstep(u.wipe.x - s, u.wipe.x + s, p.x);
10621
- return pm(vec4f(mix(a.rgb, b.rgb, m), mix(a.a, b.a, m)));
10622
- }
10623
10786
  case 2u: {
10624
10787
  let d = abs(a.rgb - b.rgb);
10625
10788
  let h = smoothstep(u.diff.x, u.diff.x + 0.06, max(d.r, max(d.g, d.b)) * u.diff.y) * min(a.a, b.a);
@@ -10628,8 +10791,10 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
10628
10791
  }
10629
10792
  default: {}
10630
10793
  }
10631
- // Side by side: each side in its own place, Before over After where they meet.
10632
- return vec4f(a.rgb * a.a + b.rgb * b.a * (1.0 - a.a), a.a + b.a * (1.0 - a.a));
10794
+ // Side by side, fusing and wiping: Before over After, cut at the seam with its feather.
10795
+ let s = max(u.wipe.y, 0.5);
10796
+ let ka = a.a * (1.0 - smoothstep(u.wipe.x - s, u.wipe.x + s, p.x));
10797
+ return vec4f(a.rgb * ka + b.rgb * b.a * (1.0 - ka), ka + b.a * (1.0 - ka));
10633
10798
  }
10634
10799
 
10635
10800
  @fragment fn fs(@builtin(position) q: vec4f) -> @location(0) vec4f {
@@ -11031,6 +11196,29 @@ var init_compare_strip = __esm(() => {
11031
11196
  DIFF = { w: 32, h: 18 };
11032
11197
  });
11033
11198
 
11199
+ // src/ui/player.ts
11200
+ function playGlyph(doc) {
11201
+ const element = doc.createElement("span");
11202
+ element.className = "knobs-player-glyph";
11203
+ element.setAttribute("aria-hidden", "true");
11204
+ let timer = 0;
11205
+ const win = doc.defaultView;
11206
+ return {
11207
+ element,
11208
+ flash(playing) {
11209
+ element.innerHTML = playing ? PLAY2 : PAUSE;
11210
+ element.setAttribute("data-flash", "");
11211
+ win.clearTimeout(timer);
11212
+ timer = win.setTimeout(() => element.removeAttribute("data-flash"), FLASH_MS);
11213
+ },
11214
+ dispose: () => win.clearTimeout(timer)
11215
+ };
11216
+ }
11217
+ var FLASH_MS = 450;
11218
+ var init_player = __esm(() => {
11219
+ init_timeline();
11220
+ });
11221
+
11034
11222
  // src/ui/compare.ts
11035
11223
  function compare(container, options) {
11036
11224
  const doc = container.ownerDocument;
@@ -11138,6 +11326,8 @@ function compare(container, options) {
11138
11326
  if (name.textContent !== text)
11139
11327
  name.textContent = text;
11140
11328
  };
11329
+ const seam = el("div", "knobs-cmp-seam");
11330
+ seam.setAttribute("aria-hidden", "true");
11141
11331
  const handle = el("div", "knobs-cmp-handle");
11142
11332
  handle.setAttribute("role", "slider");
11143
11333
  handle.setAttribute("aria-label", "Wipe");
@@ -11147,7 +11337,8 @@ function compare(container, options) {
11147
11337
  grip.setAttribute("aria-hidden", "true");
11148
11338
  handle.append(grip);
11149
11339
  const note = el("div", "knobs-cmp-note");
11150
- stage.append(canvas, split, labels[0], labels[1], handle, zoomPill, note);
11340
+ const glyph = playGlyph(doc);
11341
+ stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note, glyph.element);
11151
11342
  root.append(bar, stage);
11152
11343
  const wantsStrip = options.strip === true;
11153
11344
  let tl = bound;
@@ -11162,7 +11353,7 @@ function compare(container, options) {
11162
11353
  root.append(host);
11163
11354
  tl = timeline(host, {
11164
11355
  ...look,
11165
- variant: "lane",
11356
+ variant: "line",
11166
11357
  duration: 1,
11167
11358
  fps: fps(),
11168
11359
  arrows: { step: 0.5, shift: 2 },
@@ -11183,12 +11374,13 @@ function compare(container, options) {
11183
11374
  let toMode = mode;
11184
11375
  const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
11185
11376
  let wipeCatching = false;
11186
- let gripY = null;
11377
+ const gripAt = new Spring(0.5, LAYOUT2.omega, LAYOUT2.zeta, 0.0001);
11187
11378
  let loupeHeld = false;
11188
11379
  let pointer = null;
11189
11380
  let drag = null;
11190
11381
  const touches = new Map;
11191
11382
  let pinch = null;
11383
+ let press = null;
11192
11384
  const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
11193
11385
  const zv = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
11194
11386
  const zw = new Spring(1, ZOOM.omega, ZOOM.zeta, 0.00001);
@@ -11475,6 +11667,8 @@ function compare(container, options) {
11475
11667
  for (const s of sides) {
11476
11668
  const st = s.el.style;
11477
11669
  st.clipPath = "";
11670
+ st.maskImage = "";
11671
+ st.setProperty("-webkit-mask-image", "");
11478
11672
  st.mixBlendMode = "";
11479
11673
  st.opacity = "";
11480
11674
  s.shown = null;
@@ -11701,43 +11895,65 @@ function compare(container, options) {
11701
11895
  viewSent = key;
11702
11896
  options.onView({ zoom: clampZoom(z), loop: l });
11703
11897
  };
11704
- const labelsIn = (m, r, wr, wx) => {
11705
- if (m === "wipe") {
11706
- const at = seamLabels(wx, wr.x, wr.x + wr.w, labelW[0], labelW[1], INSET);
11707
- return [at.before, at.after];
11708
- }
11709
- const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * lay.x;
11710
- return [
11711
- { x: r.before.x + INSET, opacity: 1 },
11712
- { x: ax, opacity: 1 }
11713
- ];
11898
+ const differenceWeight = () => {
11899
+ const f = Math.min(1, Math.max(0, fade.x));
11900
+ return (fromMode === "difference" ? 1 - f : 0) + (toMode === "difference" ? f : 0);
11714
11901
  };
11715
11902
  const placeGrip = (wr) => {
11716
- const gy = gripTop(gripY ?? wr.y + wr.h / 2, wr, GRIP.h) - wr.y;
11903
+ const gy = gripTop(wr.y + gripAt.x * wr.h, wr, GRIP.h) - wr.y;
11717
11904
  grip.style.transform = `translate3d(0,${gy.toFixed(2)}px,0)`;
11718
11905
  };
11906
+ let folded = 0;
11719
11907
  const draw = () => {
11720
11908
  dirty = false;
11909
+ if (!root.hasAttribute("data-ready") && sides.every((x) => x.shown || x.failed))
11910
+ root.setAttribute("data-ready", "");
11721
11911
  const r = layoutNow();
11722
11912
  const wr = wipeRect(r.L.overlay);
11723
- const wx = wipeX(wr, wipeLine.x);
11724
- const k = lay.x;
11725
- const a = labelsIn(fromMode, r, wr, wx);
11726
- const b = labelsIn(toMode, r, wr, wx);
11727
- const f = Math.min(1, Math.max(0, fade.x));
11913
+ const k = Math.min(1, Math.max(0, lay.x));
11914
+ const fu = fuseFrame(r.L, r.before, r.after, k, wipeLine.x);
11915
+ const dw = differenceWeight();
11916
+ const fusing = 1 - dw;
11917
+ folded = k * fusing;
11918
+ const fold = labelFold(folded, MARK.w);
11919
+ const w = labelW.map((x) => x - (MARK.w - fold.width) - MARK.gap * folded);
11920
+ const ride = seamLabels(fu.cut, r.before.x, r.after.x + r.after.w, w[0], w[1], INSET);
11921
+ const apart = [r.before.x + INSET, r.after.x + INSET];
11922
+ const corner = [r.before.x + INSET, r.after.x + r.after.w - labelW[1] - INSET];
11923
+ const rides = [ride.before, ride.after];
11728
11924
  const ys = [r.before.y + INSET, r.after.y + INSET];
11729
11925
  for (const i of [0, 1]) {
11730
- const x = a[i].x + (b[i].x - a[i].x) * f;
11731
- labels[i].style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
11732
- labels[i].style.opacity = (a[i].opacity + (b[i].opacity - a[i].opacity) * f).toFixed(3);
11926
+ const fused = apart[i] + (rides[i].x - apart[i]) * k;
11927
+ const x = fused + (corner[i] - fused) * dw;
11928
+ const opacity = (1 - k + k * rides[i].opacity) * fusing + dw;
11929
+ const l = labels[i];
11930
+ l.style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
11931
+ l.style.opacity = opacity.toFixed(3);
11932
+ const mark = l.firstElementChild;
11933
+ if (mark) {
11934
+ mark.style.width = `${fold.width.toFixed(2)}px`;
11935
+ mark.style.marginRight = `${(-MARK.gap * folded).toFixed(2)}px`;
11936
+ mark.style.opacity = fold.opacity.toFixed(3);
11937
+ mark.style.filter = fold.blur > 0.05 ? `blur(${fold.blur.toFixed(2)}px)` : "";
11938
+ }
11733
11939
  }
11734
11940
  const top = Math.min(r.L.side.before.y, r.L.side.after.y);
11735
11941
  split.style.transform = `translate(${r.L.side.split.toFixed(2)}px,${top.toFixed(2)}px)`;
11736
11942
  split.style.height = `${Math.max(0, r.L.side.after.h).toFixed(2)}px`;
11737
11943
  split.style.opacity = String(Math.max(0, 1 - k * 2));
11944
+ const seamOn = fu.seam * fusing;
11945
+ const blur = fu.blur > 0.01 ? `blur(${fu.blur.toFixed(2)}px)` : "none";
11946
+ seam.style.transform = `translate(${fu.cut.toFixed(2)}px,${wr.y.toFixed(2)}px) translateX(-50%)`;
11947
+ seam.style.width = `${(8 + 2 * fu.feather).toFixed(2)}px`;
11948
+ seam.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
11949
+ seam.style.opacity = (blur === "none" ? 0 : seamOn).toFixed(3);
11950
+ seam.style.backdropFilter = blur;
11951
+ seam.style.setProperty("-webkit-backdrop-filter", blur);
11738
11952
  handle.toggleAttribute("data-on", mode === "wipe");
11739
- handle.style.transform = `translate(${wx.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
11953
+ handle.style.transform = `translate(${fu.cut.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
11740
11954
  handle.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
11955
+ handle.style.opacity = seamOn.toFixed(3);
11956
+ handle.style.filter = blur;
11741
11957
  placeGrip(wr);
11742
11958
  handle.setAttribute("aria-valuenow", String(Math.round(wipe * 100)));
11743
11959
  handle.setAttribute("aria-valuetext", `${Math.round(wipe * 100)}% Before`);
@@ -11750,24 +11966,27 @@ function compare(container, options) {
11750
11966
  if (pending)
11751
11967
  return;
11752
11968
  if (renderer === "dom") {
11753
- const place = (n, rect, clip, cutAt) => {
11969
+ const cut = mode === "difference" ? null : fu;
11970
+ const place = (n, rect, clip, before) => {
11754
11971
  const st = n.style;
11755
11972
  st.transform = `translate(${rect.x.toFixed(2)}px,${rect.y.toFixed(2)}px)`;
11756
11973
  st.width = `${rect.w.toFixed(2)}px`;
11757
11974
  st.height = `${rect.h.toFixed(2)}px`;
11975
+ const edge = before && cut ? cut.cut + cut.feather : Number.POSITIVE_INFINITY;
11758
11976
  const t = Math.max(0, clip.y - rect.y);
11759
11977
  const l = Math.max(0, clip.x - rect.x);
11760
11978
  const b = Math.max(0, rect.y + rect.h - (clip.y + clip.h));
11761
- const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, cutAt));
11979
+ const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, edge));
11762
11980
  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)` : "";
11981
+ 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)` : "";
11982
+ st.maskImage = mask;
11983
+ st.setProperty("-webkit-mask-image", mask);
11763
11984
  };
11764
- const cut = mode === "wipe" ? wx : Number.POSITIVE_INFINITY;
11765
11985
  for (const s of sides) {
11766
11986
  const rect = s.index === 0 ? r.zBefore : r.zAfter;
11767
11987
  const clip = s.index === 0 ? r.before : r.after;
11768
- const at = s.index === 0 ? cut : Number.POSITIVE_INFINITY;
11769
- place(s.el, rect, clip, at);
11770
- place(s.preview, rect, clip, at);
11988
+ place(s.el, rect, clip, s.index === 0);
11989
+ place(s.preview, rect, clip, s.index === 0);
11771
11990
  if (s.index !== 0)
11772
11991
  continue;
11773
11992
  for (const n of [s.el, s.preview])
@@ -11787,8 +12006,8 @@ function compare(container, options) {
11787
12006
  width: Math.max(1, Math.round(stageW * dpr)),
11788
12007
  height: Math.max(1, Math.round(stageH * dpr)),
11789
12008
  dpr,
11790
- wipeX: wx * dpr,
11791
- soft: 0.75 * dpr,
12009
+ wipeX: fu.cut * dpr,
12010
+ soft: Math.max(0.75, fu.feather) * dpr,
11792
12011
  from: MODE_INDEX[fromMode],
11793
12012
  to: MODE_INDEX[toMode],
11794
12013
  mix: Math.min(1, Math.max(0, fade.x)),
@@ -11815,6 +12034,7 @@ function compare(container, options) {
11815
12034
  zu.step(dt);
11816
12035
  zv.step(dt);
11817
12036
  zw.step(dt);
12037
+ gripAt.step(dt);
11818
12038
  const playing = isPlaying();
11819
12039
  if (playing) {
11820
12040
  const m = master();
@@ -11845,11 +12065,9 @@ function compare(container, options) {
11845
12065
  pushFrame(s);
11846
12066
  }
11847
12067
  }
11848
- const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching;
12068
+ const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching || !gripAt.resting;
11849
12069
  if (moving)
11850
12070
  dirty = true;
11851
- if (auto && !lay.resting)
11852
- fitHeight();
11853
12071
  if (dirty)
11854
12072
  draw();
11855
12073
  return playing || moving;
@@ -11875,13 +12093,7 @@ function compare(container, options) {
11875
12093
  zw.set(c.w);
11876
12094
  requestDraw();
11877
12095
  };
11878
- const autoHeight = (w) => {
11879
- const shape = sizeOf(sides[1]);
11880
- const side = splitHeight(w, shape);
11881
- const over = w * shape.h / shape.w;
11882
- const h = side + (over - side) * lay.x;
11883
- return Math.max(60, Math.min(win.innerHeight * 0.8, h));
11884
- };
12096
+ const autoHeight = (w) => Math.max(60, Math.min(win.innerHeight * 0.8, splitHeight(w, sizeOf(sides[1]))));
11885
12097
  const fitHeight = () => {
11886
12098
  const h = autoHeight(stageW);
11887
12099
  if (Math.abs(h - stageH) < 0.25)
@@ -11893,7 +12105,8 @@ function compare(container, options) {
11893
12105
  const readColors = () => {
11894
12106
  const c = tokenColor(win.getComputedStyle(root), "--_ku-cmp-diff", [0.49, 0.81, 0.61, 1]);
11895
12107
  accent = [c[0], c[1], c[2]];
11896
- labelW = [labels[0].offsetWidth, labels[1].offsetWidth];
12108
+ const unfold = MARK.w - labelFold(folded, MARK.w).width + MARK.gap * folded;
12109
+ labelW = [labels[0].offsetWidth + unfold, labels[1].offsetWidth + unfold];
11897
12110
  requestDraw();
11898
12111
  };
11899
12112
  const measure = () => {
@@ -11984,8 +12197,10 @@ function compare(container, options) {
11984
12197
  root.focus({ preventScroll: true });
11985
12198
  touches.set(e.pointerId, p);
11986
12199
  stage.setPointerCapture(e.pointerId);
12200
+ press = touches.size === 1 && mode !== "wipe" && master() ? p : null;
11987
12201
  if (touches.size >= 2) {
11988
12202
  e.preventDefault();
12203
+ press = null;
11989
12204
  return startPinch();
11990
12205
  }
11991
12206
  if (mode === "wipe") {
@@ -11995,7 +12210,7 @@ function compare(container, options) {
11995
12210
  if (inside && (grab.onLine || !zoomed())) {
11996
12211
  drag = { kind: "wipe", offset: grab.offset };
11997
12212
  root.dataset.dragging = "";
11998
- gripY = p.y;
12213
+ gripAt.snap(wr.h > 0 ? (p.y - wr.y) / wr.h : 0.5);
11999
12214
  e.preventDefault();
12000
12215
  if (!grab.onLine)
12001
12216
  setWipe(wipeAt(p.x, wr), true, true);
@@ -12013,6 +12228,8 @@ function compare(container, options) {
12013
12228
  const onMove = (e) => {
12014
12229
  const p = local(e);
12015
12230
  pointer = p;
12231
+ if (press && Math.hypot(p.x - press.x, p.y - press.y) > 4)
12232
+ press = null;
12016
12233
  if (touches.has(e.pointerId))
12017
12234
  touches.set(e.pointerId, p);
12018
12235
  if (pinch && touches.size >= 2) {
@@ -12026,14 +12243,12 @@ function compare(container, options) {
12026
12243
  pinch = { d, x: mid.x, y: mid.y };
12027
12244
  return handZoom(zoomAbout(panned, k, q.pu, q.pv));
12028
12245
  }
12029
- if (mode === "wipe" && drag?.kind !== "pan") {
12030
- gripY = p.y;
12031
- if (!drag)
12032
- placeGrip(wipeRect(layoutNow().L.overlay));
12033
- }
12034
- if (drag?.kind === "wipe")
12035
- setWipe(wipeAt(p.x, wipeRect(layoutNow().L.overlay), drag.offset));
12036
- else if (drag?.kind === "pan") {
12246
+ if (drag?.kind === "wipe") {
12247
+ const wr = wipeRect(layoutNow().L.overlay);
12248
+ gripAt.snap(wr.h > 0 ? (p.y - wr.y) / wr.h : 0.5);
12249
+ setWipe(wipeAt(p.x, wr, drag.offset));
12250
+ placeGrip(wr);
12251
+ } else if (drag?.kind === "pan") {
12037
12252
  const dx = p.x - drag.x;
12038
12253
  const dy = p.y - drag.y;
12039
12254
  drag.x = p.x;
@@ -12044,11 +12259,23 @@ function compare(container, options) {
12044
12259
  drawNow();
12045
12260
  };
12046
12261
  const endDrag = (e) => {
12262
+ if (press && e.type === "pointerup") {
12263
+ api.toggle();
12264
+ glyph.flash(isPlaying());
12265
+ }
12266
+ press = null;
12047
12267
  touches.delete(e.pointerId);
12048
12268
  if (touches.size < 2)
12049
12269
  pinch = null;
12050
12270
  if (!drag)
12051
12271
  return;
12272
+ if (drag.kind === "wipe") {
12273
+ if (reducedMotion(win))
12274
+ gripAt.snap(0.5);
12275
+ else
12276
+ gripAt.set(0.5);
12277
+ requestDraw();
12278
+ }
12052
12279
  drag = null;
12053
12280
  delete root.dataset.dragging;
12054
12281
  delete root.dataset.panning;
@@ -12198,6 +12425,7 @@ function compare(container, options) {
12198
12425
  old.el.replaceWith(s.el);
12199
12426
  old.preview.replaceWith(s.preview);
12200
12427
  sides[index] = s;
12428
+ root.toggleAttribute("data-video", sides.some((x) => x.kind === "video"));
12201
12429
  note.textContent = "";
12202
12430
  setLabel(index, labelOf(index));
12203
12431
  if (near)
@@ -12212,6 +12440,7 @@ function compare(container, options) {
12212
12440
  stage.insertBefore(sides[0].preview, labels[0]);
12213
12441
  setLabel(0, labelOf(0));
12214
12442
  setLabel(1, labelOf(1));
12443
+ root.toggleAttribute("data-video", sides.some((s) => s.kind === "video"));
12215
12444
  loupeBtn.setAttribute("aria-pressed", String(loupeOn));
12216
12445
  loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
12217
12446
  const modePill = modeList.length > 1 ? selectionPill(modes, {
@@ -12412,6 +12641,7 @@ function compare(container, options) {
12412
12641
  disposed = true;
12413
12642
  loop.stop();
12414
12643
  modePill?.dispose();
12644
+ glyph.dispose();
12415
12645
  win.clearTimeout(releaseTimer);
12416
12646
  ro.disconnect();
12417
12647
  nearIo?.disconnect();
@@ -12441,7 +12671,7 @@ function compare(container, options) {
12441
12671
  wake();
12442
12672
  return api;
12443
12673
  }
12444
- var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
12674
+ var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, MARK, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
12445
12675
  var init_compare = __esm(() => {
12446
12676
  init_motion();
12447
12677
  init_page_hints();
@@ -12452,15 +12682,17 @@ var init_compare = __esm(() => {
12452
12682
  init_icons();
12453
12683
  init_look();
12454
12684
  init_rgba();
12685
+ init_player();
12455
12686
  init_selection_pill();
12456
12687
  init_timeline();
12457
12688
  init_timeline_model();
12458
- LAYOUT2 = springMotion({ duration: 0.35, bounce: 0 });
12689
+ LAYOUT2 = springMotion(SPRINGS.default);
12459
12690
  FADE6 = springMotion(SPRINGS.snappy);
12460
12691
  FADE_REDUCED = springMotion(FADE);
12461
12692
  ZOOM = springMotion({ duration: 0.35, bounce: 0 });
12462
12693
  WIPE_CATCH = springMotion(CATCH_UP);
12463
12694
  GRIP = { w: 16, h: 28 };
12695
+ MARK = { w: 12, gap: 4 };
12464
12696
  LOUPE = { r: 88, zoom: 4 };
12465
12697
  MODE_LABEL = {
12466
12698
  side: { short: "Side", long: "Side by side" },
@@ -17259,7 +17491,7 @@ function normalizeWalkthrough(doc) {
17259
17491
  ...step.question ? { question: step.question } : {},
17260
17492
  media: list.map((m) => fillMedia(m, doc.base)),
17261
17493
  columns: Math.max(1, Math.min(3, Math.round(step.columns ?? 1))),
17262
- ...step.more?.trim() && list.length > 1 ? { more: step.more.trim() } : {},
17494
+ ...step.more?.trim() ? { more: step.more.trim() } : {},
17263
17495
  options,
17264
17496
  ask
17265
17497
  };
@@ -17274,6 +17506,7 @@ function normalizeWalkthrough(doc) {
17274
17506
  ...typeof s.kicker === "string" && s.kicker.trim() ? { kicker: s.kicker.trim() } : {},
17275
17507
  ...s.status ? { status: s.status } : {},
17276
17508
  ...s.summary ? { summary: s.summary } : {},
17509
+ ...s.more?.trim() ? { more: s.more.trim() } : {},
17277
17510
  steps: own
17278
17511
  };
17279
17512
  });
@@ -17307,6 +17540,7 @@ function normalizeWalkthrough(doc) {
17307
17540
  id: doc.id,
17308
17541
  title: doc.title,
17309
17542
  ...doc.summary ? { summary: doc.summary } : {},
17543
+ ...doc.more?.trim() ? { more: doc.more.trim() } : {},
17310
17544
  meta,
17311
17545
  feed,
17312
17546
  verdicts: ((doc.verdicts?.length) ? doc.verdicts : DEFAULT_VERDICTS).map((v) => ({
@@ -17314,9 +17548,40 @@ function normalizeWalkthrough(doc) {
17314
17548
  label: label(v)
17315
17549
  })),
17316
17550
  sections: withKickers(sections, doc.sections, doc.kickers),
17317
- steps
17551
+ steps,
17552
+ prs: knownPRs(doc, sections),
17553
+ ...repoOf(doc) ? { repo: repoOf(doc) } : {}
17318
17554
  };
17319
17555
  }
17556
+ var prNumber = (href) => /\/(?:pull|merge_requests)\/(\d+)(?:[/?#]|$)/.exec(href)?.[1] ?? null;
17557
+ var repoOf = (doc) => doc.repo?.trim().replace(/^https?:\/\/github\.com\//, "").replace(/\/+$/, "") || undefined;
17558
+ function knownPRs(doc, sections) {
17559
+ const out = {};
17560
+ const repo = repoOf(doc);
17561
+ for (const s of sections) {
17562
+ const n = s.link && prNumber(s.link.href);
17563
+ if (n)
17564
+ out[n] = { href: s.link.href.replace(/(\/pull\/\d+).*$/, "$1") };
17565
+ }
17566
+ for (const [k, pr] of Object.entries(doc.prs ?? {})) {
17567
+ const n = k.replace(/^#/, "");
17568
+ if (!/^\d+$/.test(n))
17569
+ continue;
17570
+ out[n] = { ...out[n], ...pr };
17571
+ }
17572
+ if (repo) {
17573
+ for (const n of Object.keys(out))
17574
+ if (!out[n].href)
17575
+ out[n] = { ...out[n], href: `https://github.com/${repo}/pull/${n}` };
17576
+ }
17577
+ return out;
17578
+ }
17579
+ function findPR(prs, n, repo) {
17580
+ const known = prs[n];
17581
+ if (known)
17582
+ return known;
17583
+ return repo ? { href: `https://github.com/${repo}/pull/${n}` } : null;
17584
+ }
17320
17585
  var isPullRequest = (href) => /\/(?:pull|merge_requests)\/\d+(?:[/?#]|$)/.test(href);
17321
17586
  function withKickers(sections, source, kickers = "pr") {
17322
17587
  if (kickers === false)
@@ -18142,6 +18407,74 @@ function disclosure(host, options) {
18142
18407
  toggle.addEventListener("click", () => api.toggle());
18143
18408
  return api;
18144
18409
  }
18410
+ var PR_PATHS = {
18411
+ pull: "M104,64A32,32,0,1,0,64,95v66a32,32,0,1,0,16,0V95A32.06,32.06,0,0,0,104,64ZM56,64A16,16,0,1,1,72,80,16,16,0,0,1,56,64ZM88,192a16,16,0,1,1-16-16A16,16,0,0,1,88,192Zm120-31V110.63a23.85,23.85,0,0,0-7-17L163.31,56H192a8,8,0,0,0,0-16H144a8,8,0,0,0-8,8V96a8,8,0,0,0,16,0V67.31L189.66,105a8,8,0,0,1,2.34,5.66V161a32,32,0,1,0,16,0Zm-8,47a16,16,0,1,1,16-16A16,16,0,0,1,200,208Z",
18412
+ merge: "M208,112a32.05,32.05,0,0,0-30.69,23l-42.21-6a8,8,0,0,1-4.95-2.71L94.43,84.55A32,32,0,1,0,72,87v82a32,32,0,1,0,16,0V101.63l30,35a24,24,0,0,0,14.83,8.14l44,6.28A32,32,0,1,0,208,112ZM64,56A16,16,0,1,1,80,72,16,16,0,0,1,64,56ZM96,200a16,16,0,1,1-16-16A16,16,0,0,1,96,200Zm112-40a16,16,0,1,1,16-16A16,16,0,0,1,208,160Z"
18413
+ };
18414
+ var prGlyph = (merged) => `<svg class="knobs-icon" viewBox="0 0 256 256" width="12" height="12" fill="currentColor" aria-hidden="true" focusable="false"><path d="${merged ? PR_PATHS.merge : PR_PATHS.pull}"/></svg>`;
18415
+ var PR_STATE = { merged: "Merged", open: "Open", draft: "Draft", closed: "Closed" };
18416
+ function prChip(doc, n, pr) {
18417
+ const chip = el(doc, pr.href ? "a" : "span", "knobs-wt-pr");
18418
+ if (pr.href) {
18419
+ chip.setAttribute("href", pr.href);
18420
+ chip.setAttribute("target", "_blank");
18421
+ chip.setAttribute("rel", "noreferrer");
18422
+ }
18423
+ if (pr.state)
18424
+ chip.dataset.state = pr.state;
18425
+ chip.innerHTML = prGlyph(pr.state === "merged");
18426
+ chip.append(text(doc, "span", "", `#${n}`));
18427
+ const tip = [`#${n}`, pr.title, pr.state ? PR_STATE[pr.state] : ""].filter(Boolean).join(" · ");
18428
+ chip.title = tip;
18429
+ chip.setAttribute("aria-label", `Pull request ${tip}`);
18430
+ return chip;
18431
+ }
18432
+ var PR_URL = /^https?:\/\/[^/]+\/.+\/(?:pull|merge_requests)\/(\d+)\/?(?:[?#].*)?$/;
18433
+ var PR_REF = /(^|[^\w&/#])#(\d{3,7})\b/g;
18434
+ function linkPRs(root, find) {
18435
+ const doc = root.ownerDocument;
18436
+ for (const a of Array.from(root.querySelectorAll("a[href]"))) {
18437
+ if (a.classList.contains("knobs-wt-pr") || a.closest(".knobs-wt-slot,button"))
18438
+ continue;
18439
+ const n = PR_URL.exec(a.getAttribute("href") ?? "")?.[1];
18440
+ const words = a.textContent?.trim() ?? "";
18441
+ if (!n || !(words === `#${n}` || words === n || words === a.getAttribute("href")))
18442
+ continue;
18443
+ a.replaceWith(prChip(doc, n, { href: a.href, ...find(n) }));
18444
+ }
18445
+ const walker = doc.createTreeWalker(root, 5);
18446
+ const hits = [];
18447
+ for (let t = walker.nextNode();t; t = walker.nextNode()) {
18448
+ if (t.nodeType !== 3)
18449
+ continue;
18450
+ const p = t.parentElement;
18451
+ if (!p || p.closest("a,code,pre,button,.knobs-wt-slot,.knobs-wt-pr"))
18452
+ continue;
18453
+ PR_REF.lastIndex = 0;
18454
+ if (PR_REF.test(t.nodeValue ?? ""))
18455
+ hits.push(t);
18456
+ }
18457
+ for (const t of hits) {
18458
+ const value = t.nodeValue ?? "";
18459
+ const frag = doc.createDocumentFragment();
18460
+ let last = 0;
18461
+ let changed = false;
18462
+ PR_REF.lastIndex = 0;
18463
+ for (let m = PR_REF.exec(value);m; m = PR_REF.exec(value)) {
18464
+ const pr = find(m[2]);
18465
+ if (!pr)
18466
+ continue;
18467
+ const at = m.index + m[1].length;
18468
+ frag.append(value.slice(last, at), prChip(doc, m[2], pr));
18469
+ last = at + m[2].length + 1;
18470
+ changed = true;
18471
+ }
18472
+ if (!changed)
18473
+ continue;
18474
+ frag.append(value.slice(last));
18475
+ t.replaceWith(frag);
18476
+ }
18477
+ }
18145
18478
 
18146
18479
  // src/ui/walkthrough-media.ts
18147
18480
  init_compare_model();
@@ -18159,7 +18492,7 @@ var fade2 = "-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 1.5em)
18159
18492
  var walkthroughCss = `
18160
18493
  :where(.knobs-walkthrough,.knobs-walkthrough *,.knobs-wt-slot,.knobs-wt-slot *){box-sizing:border-box}
18161
18494
  .knobs-ui.knobs-walkthrough{--kw-snappy:${snappy3.easing};--kw-snappy-ms:${snappy3.duration}ms;--kw-calm:${calm.easing};--kw-calm-ms:${calm.duration}ms;--kw-state:${state.easing};--kw-state-ms:${state.duration}ms;--kw-quick:${quick.easing};--kw-quick-ms:${quick.duration}ms;
18162
- --kw-measure:var(--k-walkthrough-measure,68ch);--kw-width:var(--k-walkthrough-width,680px);--kw-rail-w:220px;--kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));
18495
+ --kw-measure:var(--k-walkthrough-measure,68ch);--kw-width:var(--k-walkthrough-width,960px);--kw-rail-w:220px;--kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info));
18163
18496
  --kw-card:var(--_ku-pane-bg);--kw-card-ring:var(--_ku-pane-ring);--kw-under:var(--_k-bg-page);--kw-ink:var(--k-walkthrough-ink,#ff375f);--kw-strong:var(--_k-weight-label);
18164
18497
  container:knobs-wt / inline-size;display:block;width:100%;color:var(--_k-fg);font:var(--_k-weight) var(--_k-type-body)/var(--_k-leading-body) var(--_k-font);font-variant-numeric:tabular-nums;text-align:left;
18165
18498
  letter-spacing:normal;-webkit-font-smoothing:antialiased}
@@ -18172,44 +18505,57 @@ var walkthroughCss = `
18172
18505
  :where(.knobs-ui) :is(.knobs-wt-btn,.knobs-wt-pick,.knobs-wt-toc-row,button.knobs-wt-stitle,.knobs-wt-chapter,.knobs-wt-disc-toggle){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;
18173
18506
  background:transparent;border:0;border-radius:0;margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
18174
18507
 
18175
- /* ——— layout: a readable column, centred, with a slim contents rail beside it (right by default);
18176
- one column where it's narrow ——— */
18177
- .knobs-wt-layout{display:grid;grid-template-columns:minmax(0,var(--kw-width)) var(--kw-rail-w);justify-content:center;gap:var(--_k-space-column);align-items:start}
18508
+ /* ——— layout: the contents rail sticky at the left edge (right if asked), and the column centred
18509
+ in the room beside it, up to --kw-width: media take the column, prose stops at the measure.
18510
+ One column where it's narrow ——— */
18511
+ .knobs-wt-layout{display:grid;grid-template-columns:var(--kw-rail-w) minmax(0,1fr);gap:var(--_k-space-column);align-items:start}
18512
+ .knobs-walkthrough[data-rail="right"] .knobs-wt-layout{grid-template-columns:minmax(0,1fr) var(--kw-rail-w)}
18178
18513
  .knobs-walkthrough[data-rail="right"] .knobs-wt-main{grid-column:1;grid-row:1}
18179
18514
  .knobs-walkthrough[data-rail="right"] .knobs-wt-rail{grid-column:2;grid-row:1}
18180
- .knobs-walkthrough[data-rail="left"] .knobs-wt-layout{grid-template-columns:var(--kw-rail-w) minmax(0,var(--kw-width))}
18181
- .knobs-walkthrough[data-rail="none"] .knobs-wt-layout{grid-template-columns:minmax(0,var(--kw-width))}
18182
- .knobs-wt-main{display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0;padding-bottom:12px}
18515
+ .knobs-walkthrough[data-rail="none"] .knobs-wt-layout{grid-template-columns:minmax(0,1fr)}
18516
+ .knobs-wt-main{justify-self:center;width:100%;max-width:var(--kw-width);display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0;padding-bottom:12px}
18183
18517
  /* The intro, the updates and each section: a section's room apart, the hairline in the middle. */
18184
18518
  .knobs-wt-flow{display:flex;flex-direction:column;gap:calc(var(--_k-space-section) / 2);min-width:0}
18185
18519
  .knobs-wt-flow>*+*{padding-top:calc(var(--_k-space-section) / 2);box-shadow:inset 0 1px 0 var(--_k-border-subtle)}
18186
- @container knobs-wt (max-width:940px){.knobs-walkthrough .knobs-wt-layout{grid-template-columns:minmax(0,1fr);gap:var(--_k-space-title)}
18520
+ @container knobs-wt (max-width:960px){.knobs-walkthrough[data-rail] .knobs-wt-layout{grid-template-columns:minmax(0,1fr);gap:var(--_k-space-title)}
18187
18521
  .knobs-walkthrough[data-rail] .knobs-wt-layout>*{grid-column:auto;grid-row:auto}}
18188
18522
 
18189
18523
  /* ——— one line of facts: small, faint, joined by ·, its trailing edge fading ——— */
18190
18524
  .knobs-wt-metaline{display:flex;align-items:baseline;gap:var(--_k-space-inside);min-width:0;overflow:hidden;white-space:nowrap;font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3);${fade2}}
18191
18525
  .knobs-wt-metaline>*{flex:none}
18192
18526
  .knobs-wt-metaline>*+*::before{content:"·";margin-right:var(--_k-space-inside);color:var(--_k-fg-3)}
18193
- .knobs-walkthrough .knobs-wt-metaline a{display:inline-flex;align-items:baseline;color:inherit;text-decoration:none;transition:color calc(var(--_k-hover-out) * 1ms)}
18194
- .knobs-walkthrough .knobs-wt-metaline a:hover{color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18527
+ .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)}
18528
+ .knobs-walkthrough .knobs-wt-metaline a:not(.knobs-wt-pr):hover{color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18195
18529
  .knobs-wt-metaline svg{align-self:center;width:12px;height:12px;margin-left:2px}
18530
+ /* A pull request: a small chip, its state's icon and number in a quiet tint of the state (open
18531
+ the success hue, merged the violet series, draft and closed neutral), its title on hover. */
18532
+ .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;
18533
+ 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;
18534
+ transition:background calc(var(--_k-hover-out) * 1ms)}
18535
+ .knobs-ui .knobs-wt-pr[data-state="open"]{--kw-pr:color-mix(in oklch,var(--_k-ok) 60%,var(--_k-fg-2))}
18536
+ .knobs-ui .knobs-wt-pr[data-state="merged"]{--kw-pr:color-mix(in oklch,var(--_k-series-5) 60%,var(--_k-fg-2))}
18537
+ .knobs-ui .knobs-wt-pr:is([data-state="draft"],[data-state="closed"]){--kw-pr:var(--_k-fg-3)}
18538
+ .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)}
18539
+ .knobs-wt-pr svg{flex:none}
18540
+ .knobs-wt-metaline .knobs-wt-pr{align-self:center;margin:0}
18196
18541
  .knobs-wt-kicker-lead{color:var(--_k-fg);font-weight:var(--kw-strong)}
18197
18542
  .knobs-wt-metaline [data-status="on"]{color:var(--_k-fg)}
18198
18543
  .knobs-wt-metaline [data-status="warn"]{color:var(--_k-warn)}
18199
18544
 
18200
18545
  /* ——— the page's head ——— */
18201
18546
  .knobs-wt-head{display:flex;flex-direction:column;gap:var(--_k-space-title)}
18202
- .knobs-wt-title{font-size:var(--_k-type-title)!important;line-height:var(--_k-leading-title);font-weight:var(--kw-strong)!important;text-wrap:balance}
18547
+ .knobs-wt-title{font-size:var(--_k-type-heading)!important;line-height:var(--_k-leading-heading);font-weight:var(--kw-strong)!important;text-wrap:balance}
18203
18548
  .knobs-wt-summary{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
18204
18549
  .knobs-wt-meta{display:flex;flex-wrap:wrap;gap:var(--_k-space-inside) var(--_k-space-title);color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18205
18550
  .knobs-wt-meta>span{display:inline-flex;gap:var(--_k-space-inside);white-space:nowrap}
18206
18551
  .knobs-wt-meta b{font-weight:var(--_k-weight);color:var(--_k-fg-2);font-family:var(--_k-mono)}
18207
18552
 
18208
- /* ——— a section: a PR or topic. Its kicker leads the meta line; its title outranks the page's ——— */
18553
+ /* ——— a section: a PR or topic. Its kicker leads the meta line, its title under it (16/24, under
18554
+ the page's 20/28), both on the column's edge ——— */
18209
18555
  .knobs-wt-section{display:flex;flex-direction:column;gap:var(--_k-space-group);min-width:0}
18210
18556
  .knobs-wt-sintro{display:flex;flex-direction:column;gap:var(--_k-space-title);min-width:0}
18211
18557
  .knobs-wt-shead{display:flex;flex-direction:column;gap:var(--_k-space-inside);min-width:0;scroll-margin-top:16px}
18212
- .knobs-wt-heading{font-size:var(--_k-type-heading)!important;line-height:var(--_k-leading-heading);font-weight:var(--kw-strong)!important;letter-spacing:-.01em!important;text-wrap:balance}
18558
+ .knobs-wt-heading{font-size:var(--_k-type-title)!important;line-height:var(--_k-leading-title);font-weight:var(--kw-strong)!important;text-wrap:balance}
18213
18559
  .knobs-wt-ssum{max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
18214
18560
  .knobs-wt-chip{display:inline-flex;align-items:center;gap:6px;height:20px;padding:0 8px;border-radius:var(--_k-round);font-size:var(--_k-type-small);font-weight:var(--kw-strong);white-space:nowrap;
18215
18561
  --kw-tone:var(--_k-fg-2);color:var(--kw-tone);background:color-mix(in srgb,var(--kw-tone) 14%,transparent)}
@@ -18270,6 +18616,8 @@ var walkthroughCss = `
18270
18616
  .knobs-wt-disc[data-open]>.knobs-wt-disc-body:not([data-moving]){overflow:visible}
18271
18617
  .knobs-wt-disc-body[data-moving]{-webkit-mask-image:linear-gradient(#000 calc(100% - 16px),transparent);mask-image:linear-gradient(#000 calc(100% - 16px),transparent)}
18272
18618
  .knobs-wt-disc-content{padding-top:var(--_k-space-inside)}
18619
+ /* "More" under a caption or summary: the rest of its words, in the same voice. */
18620
+ .knobs-wt-more-text{display:flex;flex-direction:column;gap:var(--_k-space-sibling);max-width:var(--kw-measure);color:var(--_k-fg-2);text-wrap:pretty}
18273
18621
  /* A trail sits on the toggle's label edge: faint lines, the time trailing. */
18274
18622
  .knobs-wt-trail{display:flex;flex-direction:column;list-style:none;padding:0 0 0 calc(12px + var(--_k-space-inside));font-size:var(--_k-type-small);line-height:var(--_k-leading-small);color:var(--_k-fg-3);min-width:0}
18275
18623
  .knobs-wt-trail li{display:flex;align-items:baseline;gap:var(--_k-space-sibling);min-width:0;white-space:nowrap}
@@ -18387,32 +18735,28 @@ var walkthroughCss = `
18387
18735
  .knobs-wt-ocap{color:var(--_k-fg-2);max-width:var(--kw-measure)}
18388
18736
 
18389
18737
  /* ——— the rail: progress, contents, export ——— */
18390
- .knobs-wt-rail{position:sticky;top:16px;display:flex;flex-direction:column;gap:var(--_k-space-title);max-height:calc(100vh - 32px);min-height:0;font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18738
+ .knobs-wt-rail{position:sticky;top:var(--_k-pad-page);display:flex;flex-direction:column;gap:var(--_k-space-title);max-height:calc(100vh - var(--_k-pad-page) * 2);min-height:0;font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18391
18739
  .knobs-wt-progress{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
18392
18740
  .knobs-wt-count{display:flex;align-items:baseline;justify-content:space-between;color:var(--_k-fg-2)}
18393
18741
  .knobs-wt-count b{color:var(--_k-fg);font-weight:var(--kw-strong)}
18394
18742
  .knobs-wt-bar{position:relative;height:4px;border-radius:2px;overflow:hidden;background:var(--_k-fill)}
18395
18743
  .knobs-wt-fill{position:absolute;inset:0;border-radius:inherit;background:var(--_k-accent);transform-origin:0 50%;transform:scaleX(var(--kw-done,0))}
18396
18744
  .knobs-walkthrough[data-settled] .knobs-wt-fill{transition:transform var(--kw-calm-ms) var(--kw-calm)}
18397
- /* The contents: rows centred on one spine, title rows (32 high, the control radius) and the
18398
- steps under them (20 high, half of it), bled out so their text sits on the rail's edge; the
18399
- selection pill slides to the current step, and base rows never brighten for being current. */
18745
+ /* The contents: every row on one text edge and in one colour, title rows (32 high, the control
18746
+ radius) and the steps under them (20 high, half of it), bled out so their text sits on the
18747
+ rail's edge; the selection pill slides to the current step, and base rows never brighten. */
18400
18748
  .knobs-wt-toc{position:relative;flex:1 1 auto;min-height:0;overflow:auto;scrollbar-width:none;display:flex;flex-direction:column;gap:var(--_k-space-sibling);margin:0 calc(var(--_k-space-sibling) * -1);padding:0 0 8px}
18401
18749
  .knobs-wt-toc::-webkit-scrollbar{display:none}
18402
18750
  .knobs-wt-toc-group{display:flex;flex-direction:column;min-width:0}
18403
- .knobs-ui .knobs-wt-toc-row{position:relative;display:grid;grid-template-columns:var(--kw-toc-n,0px) minmax(0,1fr) auto;column-gap:var(--_k-space-sibling);align-items:center;flex:none;width:100%;height:32px;
18404
- padding:0 var(--_k-space-sibling);border-radius:var(--_k-radius);color:var(--_k-fg-2);font-size:var(--_k-type-body);line-height:var(--_k-leading-body);font-weight:var(--kw-strong);text-align:left;white-space:nowrap;cursor:pointer;
18751
+ .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;
18752
+ padding:0 var(--_k-space-sibling);border-radius:var(--_k-radius);color:var(--_k-fg-2);font-size:var(--_k-type-body);line-height:var(--_k-leading-body);font-weight:var(--_k-weight);text-align:left;white-space:nowrap;cursor:pointer;
18405
18753
  transition:color calc(var(--_k-hover-out) * 1ms)}
18406
- .knobs-ui .knobs-wt-toc-sub{height:20px;border-radius:calc(var(--_k-radius) / 2);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight);color:var(--_k-fg-3)}
18407
- .knobs-wt-toc[data-bare] .knobs-wt-toc-row{grid-template-columns:minmax(0,1fr) auto}
18408
- .knobs-wt-toc[data-bare] .knobs-wt-toc-n{display:none}
18754
+ .knobs-ui .knobs-wt-toc-sub{height:20px;border-radius:calc(var(--_k-radius) / 2);font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight)}
18409
18755
  .knobs-ui .knobs-wt-toc-row:is(:hover,:focus-visible){color:var(--_k-fg);transition-duration:calc(var(--_k-hover-in) * 1ms)}
18410
- .knobs-ui .knobs-wt-toc-sub:is(:hover,:focus-visible){color:var(--_k-fg-2)}
18411
18756
  .knobs-ui .knobs-wt-toc-row:focus-visible{box-shadow:inset 0 0 0 1px var(--_k-focus)}
18412
- .knobs-wt-toc-n,.knobs-wt-toc-end{display:flex;align-items:center;font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight);color:var(--_k-fg-3)}
18413
- .knobs-wt-toc-row[data-active] .knobs-wt-toc-n{color:var(--_k-fg-2)}
18757
+ .knobs-wt-toc-end{display:flex;align-items:center;font-size:var(--_k-type-small);line-height:var(--_k-leading-small);font-weight:var(--_k-weight);color:var(--_k-fg-3)}
18414
18758
  .knobs-wt-toc-label{min-width:0;overflow:hidden;${fade2}}
18415
- .knobs-pick-copy .knobs-wt-toc-n,.knobs-pick-copy .knobs-wt-toc-end{color:var(--_k-fg-2)}
18759
+ .knobs-pick-copy .knobs-wt-toc-end{color:var(--_k-fg-2)}
18416
18760
  /* A step's state: an open ring, a filled dot once answered, nothing to answer a faint ring. */
18417
18761
  .knobs-wt-toc-glyph{--kw-tone:var(--_k-fg-3);display:block;width:6px;height:6px;border-radius:50%;box-shadow:inset 0 0 0 1.5px var(--kw-tone);
18418
18762
  transition:background var(--kw-snappy-ms) var(--kw-snappy),box-shadow var(--kw-snappy-ms) var(--kw-snappy)}
@@ -18427,10 +18771,12 @@ var walkthroughCss = `
18427
18771
  .knobs-ui .knobs-wt-btn[data-primary]:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_k-fg) 16%,transparent)}
18428
18772
  .knobs-wt-keys{color:var(--_k-fg-3);font-size:var(--_k-type-small);line-height:var(--_k-leading-small)}
18429
18773
  .knobs-wt-keys kbd{font:inherit;color:var(--_k-fg-2)}
18430
- /* Narrow: the rail becomes a bar along the top, with the progress and the export only. */
18431
- @container knobs-wt (max-width:940px){
18774
+ /* Narrow: the contents go; with review on, the rail becomes a bar along the top with the
18775
+ progress and the export. */
18776
+ @container knobs-wt (max-width:960px){
18432
18777
  .knobs-wt-rail{z-index:4;flex-direction:row;align-items:center;gap:var(--_k-space-title);max-height:none;margin:0 calc(var(--_k-pad-page) * -1);padding:8px var(--_k-pad-page);top:0;background:var(--_k-bg-page)}
18433
18778
  .knobs-wt-progress{flex:1}.knobs-wt-toc,.knobs-wt-keys{display:none}
18779
+ .knobs-walkthrough:not([data-review]) .knobs-wt-rail{display:none}
18434
18780
  .knobs-wt-step,.knobs-wt-shead,.knobs-wt-feed{scroll-margin-top:60px}
18435
18781
  }
18436
18782
 
@@ -18440,9 +18786,14 @@ var walkthroughCss = `
18440
18786
  well for hairlines, so nothing draws a fill and a ring of its own there ——— */
18441
18787
  .knobs-ui.knobs-wt-slot{position:relative;display:flex;flex-direction:column;gap:var(--_k-space-sibling);min-width:0;container:knobs-wt-slot / inline-size;color:var(--_k-fg);
18442
18788
  font:var(--_k-weight) var(--_k-type-body)/var(--_k-leading-body) var(--_k-font);font-variant-numeric:tabular-nums;--kw-measure:var(--k-walkthrough-measure,68ch);
18443
- --kw-good:var(--k-walkthrough-good,var(--_k-ok));--kw-info:var(--k-walkthrough-info,var(--_k-info))}
18444
- .knobs-wt-slot-label{display:block;font:var(--_k-weight-label) var(--_k-type-label)/var(--_k-leading-label) var(--_k-font);color:var(--_k-fg-2)}
18789
+ --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)}
18790
+ .knobs-surface .knobs-wt-slot{--kw-frame-r:calc(var(--_k-radius-panel) - 4px)}
18791
+ /* A slot's label: sentence case as written, small and faint, never a heading. */
18792
+ .knobs-wt-slot-label{display:block;min-width:0;overflow:hidden;white-space:nowrap;font:var(--_k-weight) var(--_k-type-small)/var(--_k-leading-small) var(--_k-font);letter-spacing:normal;text-transform:none;color:var(--_k-fg-3);${fade2}}
18445
18793
  .knobs-wt-body{min-width:0}
18794
+ /* One frame for every slot that has one (a picture, a video, a code block, a compare's stage, a
18795
+ slot still loading): the panel radius and a quiet fill, no ring; concentric on a card. */
18796
+ .knobs-wt-slot .knobs-cmp-stage{border-radius:var(--kw-frame-r)}
18446
18797
  /* A compare is exactly as tall as its pictures at this width, so there's no letterbox: the two
18447
18798
  sides edge to edge (side) or one (overlaid), plus its mode bar, the timeline and their two gaps
18448
18799
  (92 px), and the frame strip with its gap (158 px). */
@@ -18450,11 +18801,11 @@ var walkthroughCss = `
18450
18801
  .knobs-wt-fit[data-overlay]{height:calc(100cqw / var(--kw-aspect,1.7778) + var(--kw-chrome,92px))}
18451
18802
  .knobs-wt-fit[data-strip]{--kw-chrome:158px}
18452
18803
  .knobs-wt-fit[data-settled]{transition:height var(--k-wt-calm-ms,${calm.duration}ms) ${calm.easing}}
18453
- .knobs-wt-frame{position:relative;width:100%;overflow:hidden;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill);aspect-ratio:var(--kw-aspect,16/9)}
18804
+ .knobs-wt-frame{position:relative;width:100%;overflow:hidden;border-radius:var(--kw-frame-r);background:var(--_k-fill);aspect-ratio:var(--kw-aspect,16/9)}
18454
18805
  .knobs-wt-frame>:is(video,img){position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;margin:0;border:0}
18455
18806
  .knobs-wt-frame[data-natural]{aspect-ratio:auto;background:transparent}
18456
18807
  .knobs-wt-frame[data-natural]>img{position:static;height:auto}
18457
- .knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill);color:var(--_k-fg);
18808
+ .knobs-wt-code{margin:0;padding:var(--_k-pad-card);max-height:420px;overflow:auto;border-radius:var(--kw-frame-r);background:var(--_k-fill);color:var(--_k-fg);
18458
18809
  font:var(--_k-type-label)/var(--_k-leading-body) var(--_k-mono);white-space:pre;tab-size:2;scrollbar-width:thin}
18459
18810
  .knobs-surface .knobs-wt-code{padding-inline:0;border-radius:0;background:transparent;box-shadow:inset 0 1px 0 var(--_k-border-subtle),inset 0 -1px 0 var(--_k-border-subtle)}
18460
18811
  .knobs-wt-code[data-terminal]{white-space:pre-wrap;overflow-wrap:anywhere}
@@ -18464,8 +18815,8 @@ var walkthroughCss = `
18464
18815
  .knobs-wt-metric{display:flex;flex-direction:column;gap:var(--_k-space-sibling)}
18465
18816
  .knobs-wt-mlabel{color:var(--_k-fg-2)}
18466
18817
  .knobs-wt-mvalue{display:flex;align-items:baseline;flex-wrap:wrap;gap:var(--_k-space-sibling);font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}
18467
- .knobs-wt-mfrom{color:var(--_k-fg-3);font-size:var(--_k-type-title);line-height:var(--_k-leading-title)}
18468
- .knobs-wt-mto{font-size:var(--_k-type-heading);line-height:var(--_k-leading-heading);font-weight:var(--_k-weight-label)}
18818
+ .knobs-wt-mfrom{color:var(--_k-fg-3)}
18819
+ .knobs-wt-mto{font-size:var(--_k-type-title);line-height:var(--_k-leading-title);font-weight:var(--_k-weight-label)}
18469
18820
  .knobs-wt-mof{color:var(--_k-fg-3)}
18470
18821
  .knobs-wt-mdelta{--kw-tone:var(--_k-fg-2);font-size:var(--_k-type-small);font-weight:var(--_k-weight-label);color:var(--kw-tone)}
18471
18822
  .knobs-wt-mdelta[data-tone="good"]{--kw-tone:var(--kw-good)}
@@ -18480,7 +18831,7 @@ var walkthroughCss = `
18480
18831
  .knobs-wt-callout[data-tone="warn"]{--kw-tone:var(--_k-warn)}
18481
18832
  .knobs-wt-callout[data-tone="bad"]{--kw-tone:var(--_k-danger)}
18482
18833
  .knobs-wt-text{max-width:var(--kw-measure);color:var(--_k-fg-2)}
18483
- .knobs-wt-hold{border-radius:calc(var(--_k-radius-panel) - 4px);background:var(--_k-fill)}
18834
+ .knobs-wt-hold{border-radius:var(--kw-frame-r);background:var(--_k-fill)}
18484
18835
  .knobs-wt-failed{display:flex;align-items:center;justify-content:center;min-height:80px;color:var(--_k-fg-3);font-size:var(--_k-type-small)}
18485
18836
 
18486
18837
  /* ——— data blocks: history, stack, table, chapters. On the step's card, so no card of their own:
@@ -18592,7 +18943,7 @@ var walkthroughCss = `
18592
18943
  .knobs-wt-table{max-height:520px;overflow:auto;overscroll-behavior-x:contain;outline:none;scrollbar-width:thin;scrollbar-color:transparent transparent;
18593
18944
  transition:scrollbar-color calc(var(--_k-hover-out) * 1ms)}
18594
18945
  .knobs-wt-table:is(:hover,:focus-visible){scrollbar-color:color-mix(in oklch,var(--_k-fg) 28%,transparent) transparent;transition-duration:calc(var(--_k-hover-in) * 1ms)}
18595
- .knobs-wt-table:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus);border-radius:calc(var(--_k-radius-panel) - 4px)}
18946
+ .knobs-wt-table:focus-visible{box-shadow:inset 0 0 0 2px var(--_k-focus);border-radius:var(--kw-frame-r)}
18596
18947
  @supports (animation-timeline:scroll()){
18597
18948
  .knobs-wt-table{animation:knobs-fade-a linear both,knobs-fade-b linear both,kw-fade-l linear both,kw-fade-r linear both;animation-duration:auto;
18598
18949
  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%;
@@ -19206,16 +19557,25 @@ function tasteTest(container, options) {
19206
19557
  ...o.media !== undefined ? { media: fillMedia(o.media) } : {},
19207
19558
  suggested: o.suggested === true
19208
19559
  });
19209
- let picked = options.picked ?? null;
19560
+ const pickable = options.pickable !== false;
19561
+ let picked = pickable ? options.picked ?? null : null;
19210
19562
  const root = el2(doc, "div", "knobs-wt-options");
19211
- root.setAttribute("role", "radiogroup");
19212
- root.setAttribute("aria-label", "Options");
19563
+ if (pickable) {
19564
+ root.setAttribute("role", "radiogroup");
19565
+ root.setAttribute("aria-label", "Options");
19566
+ } else
19567
+ root.dataset.static = "";
19213
19568
  root.style.setProperty("--kw-n", String(Math.min(list.length, 3)));
19214
19569
  if (!container.closest(".knobs-ui"))
19215
19570
  applyLook2(root, options);
19216
19571
  const slots = [];
19217
19572
  const tiles = list.map((o, i) => {
19218
19573
  const tile = el2(doc, "div", "knobs-wt-option");
19574
+ if (!pickable) {
19575
+ const name = el2(doc, "span", "knobs-wt-slot-label");
19576
+ name.textContent = o.label;
19577
+ tile.append(name);
19578
+ }
19219
19579
  const pick = el2(doc, "button", "knobs-wt-pick");
19220
19580
  pick.type = "button";
19221
19581
  pick.setAttribute("role", "radio");
@@ -19244,7 +19604,8 @@ function tasteTest(container, options) {
19244
19604
  api.pick(next);
19245
19605
  options.onPick?.(next);
19246
19606
  });
19247
- tile.append(pick);
19607
+ if (pickable)
19608
+ tile.append(pick);
19248
19609
  if (o.media)
19249
19610
  slots.push(mediaSlot(tile, {
19250
19611
  ...look,
@@ -19327,7 +19688,8 @@ function walkthrough(container, options) {
19327
19688
  let tocRows = [];
19328
19689
  let discs = [];
19329
19690
  let full = null;
19330
- const noteMode = options.noteBar ?? "collapsed";
19691
+ const review = options.review === true;
19692
+ const noteMode = review ? options.noteBar ?? "collapsed" : "hidden";
19331
19693
  let reserve = noteBarReserve(52, DOCK_INSET, DOCK_GAP);
19332
19694
  const viewOf = (key) => views.find((v) => v.step.key === key);
19333
19695
  const feedback = () => collectFeedback(page, state);
@@ -20063,7 +20425,14 @@ function walkthrough(container, options) {
20063
20425
  line.append(s);
20064
20426
  return s;
20065
20427
  };
20428
+ const findPRIn = (n) => findPR(page.prs, n, page.repo);
20066
20429
  const metaLink = (line, text, href) => {
20430
+ const n = prNumber(href);
20431
+ if (n) {
20432
+ const chip = prChip(doc, n, { href, ...findPRIn(n) });
20433
+ line.append(chip);
20434
+ return chip;
20435
+ }
20067
20436
  const a = el2(doc, "a");
20068
20437
  a.href = href;
20069
20438
  a.target = "_blank";
@@ -20150,8 +20519,9 @@ function walkthrough(container, options) {
20150
20519
  views = [];
20151
20520
  root.textContent = "";
20152
20521
  root.removeAttribute("data-settled");
20153
- root.dataset.rail = options.rail === false ? "none" : options.rail === "left" ? "left" : "right";
20522
+ root.dataset.rail = options.rail === false ? "none" : options.rail === "right" ? "right" : "left";
20154
20523
  root.toggleAttribute("data-cards", options.cards === true);
20524
+ root.toggleAttribute("data-review", review);
20155
20525
  const layout = el2(doc, "div", "knobs-wt-layout");
20156
20526
  const main = el2(doc, "div", "knobs-wt-main");
20157
20527
  const flow = el2(doc, "div", "knobs-wt-flow");
@@ -20161,6 +20531,8 @@ function walkthrough(container, options) {
20161
20531
  head.append(h1);
20162
20532
  if (page.summary)
20163
20533
  head.append(prose2(doc, "p", "knobs-wt-summary", page.summary));
20534
+ if (page.more)
20535
+ head.append(more(page.more));
20164
20536
  if (page.meta.length) {
20165
20537
  const meta = el2(doc, "div", "knobs-wt-meta");
20166
20538
  for (const m of page.meta) {
@@ -20204,28 +20576,28 @@ function walkthrough(container, options) {
20204
20576
  count = fill = tocEl = null;
20205
20577
  if (options.rail !== false) {
20206
20578
  rail = el2(doc, "aside", "knobs-wt-rail");
20207
- rail.setAttribute("aria-label", "Progress");
20208
- const progress = el2(doc, "div", "knobs-wt-progress");
20209
- count = el2(doc, "div", "knobs-wt-count");
20210
- const meter = el2(doc, "div", "knobs-wt-bar");
20211
- fill = el2(doc, "div", "knobs-wt-fill");
20212
- meter.append(fill);
20213
- progress.append(count, meter);
20579
+ rail.setAttribute("aria-label", "Contents");
20214
20580
  tocEl = el2(doc, "nav", "knobs-wt-toc");
20215
20581
  tocEl.setAttribute("aria-label", "Contents");
20216
- const ns = page.sections.map((s) => s.kicker && s.link ? s.link.label : "");
20217
- const nw = Math.max(0, ...ns.map((n) => n.length));
20218
- tocEl.style.setProperty("--kw-toc-n", nw ? `${nw + 0.5}ch` : "0px");
20219
- if (!nw)
20220
- tocEl.dataset.bare = "";
20221
20582
  if (feedEl) {
20222
20583
  const group = el2(doc, "div", "knobs-wt-toc-group");
20223
20584
  const target = feedEl;
20224
- group.append(tocRow("Latest", "", String(page.feed.length), false, () => target.scrollIntoView({ block: "start", behavior: reduced() ? "auto" : "smooth" })));
20585
+ group.append(tocRow("Latest", false, () => target.scrollIntoView({ block: "start", behavior: reduced() ? "auto" : "smooth" })));
20225
20586
  tocEl.append(group);
20226
20587
  }
20227
- rail.append(progress, tocEl, actions(true));
20228
- if (options.keys !== false) {
20588
+ if (review) {
20589
+ const progress = el2(doc, "div", "knobs-wt-progress");
20590
+ count = el2(doc, "div", "knobs-wt-count");
20591
+ const meter = el2(doc, "div", "knobs-wt-bar");
20592
+ fill = el2(doc, "div", "knobs-wt-fill");
20593
+ meter.append(fill);
20594
+ progress.append(count, meter);
20595
+ rail.append(progress);
20596
+ }
20597
+ rail.append(tocEl);
20598
+ if (review)
20599
+ rail.append(actions(true));
20600
+ if (review && options.keys !== false) {
20229
20601
  const keys = el2(doc, "div", "knobs-wt-keys");
20230
20602
  keys.innerHTML = "<kbd>j</kbd> <kbd>k</kbd> steps · <kbd>←</kbd> <kbd>→</kbd> seek · <kbd>f</kbd> full · <kbd>n</kbd> note" + (page.steps.some((s) => s.ask === "pick") ? " · <kbd>1</kbd>–<kbd>9</kbd> pick" : "");
20231
20603
  rail.append(keys);
@@ -20235,14 +20607,15 @@ function walkthrough(container, options) {
20235
20607
  const sec = el2(doc, "section", "knobs-wt-section");
20236
20608
  sec.id = `wt-${section.id}`;
20237
20609
  const sh = el2(doc, "header", "knobs-wt-shead");
20238
- if (section.kicker || section.link || section.status) {
20610
+ const status = review ? section.status : undefined;
20611
+ if (section.kicker || section.link || status) {
20239
20612
  const meta = metaLine("knobs-wt-kicker");
20240
20613
  if (section.kicker)
20241
20614
  metaText(meta, section.kicker, "knobs-wt-kicker-lead");
20242
20615
  if (section.link)
20243
20616
  metaLink(meta, section.link.label, section.link.href);
20244
- if (section.status) {
20245
- const s = statusLook(section.status);
20617
+ if (status) {
20618
+ const s = statusLook(status);
20246
20619
  metaText(meta, s.label, "knobs-wt-status").dataset.tone = s.tone;
20247
20620
  }
20248
20621
  sh.append(meta);
@@ -20254,13 +20627,14 @@ function walkthrough(container, options) {
20254
20627
  intro.append(sh);
20255
20628
  if (section.summary)
20256
20629
  intro.append(prose2(doc, "p", "knobs-wt-ssum", section.summary));
20630
+ if (section.more)
20631
+ intro.append(more(section.more));
20257
20632
  sec.append(intro);
20258
20633
  let group = null;
20259
20634
  if (tocEl) {
20260
20635
  group = el2(doc, "div", "knobs-wt-toc-group");
20261
20636
  const first = section.steps[0]?.key;
20262
- const n = section.kicker && section.link ? section.link.label : "";
20263
- group.append(tocRow(plainMarkdown(section.title), n, "", false, () => first ? go(first) : undefined, section.id));
20637
+ group.append(tocRow(plainMarkdown(section.title), false, () => first ? go(first) : undefined, section.id));
20264
20638
  tocEl.append(group);
20265
20639
  }
20266
20640
  for (const step of section.steps) {
@@ -20270,6 +20644,7 @@ function walkthrough(container, options) {
20270
20644
  }
20271
20645
  flow.append(sec);
20272
20646
  }
20647
+ linkPRs(flow, findPRIn);
20273
20648
  main.append(flow);
20274
20649
  if (noteMode !== "hidden")
20275
20650
  main.append(bar.dock);
@@ -20298,19 +20673,25 @@ function walkthrough(container, options) {
20298
20673
  root.dataset.settled = "";
20299
20674
  }));
20300
20675
  };
20301
- const tocRow = (label, n, end, sub, run, section) => {
20676
+ const more = (text) => {
20677
+ const box = el2(doc, "div", "knobs-wt-more");
20678
+ const body = el2(doc, "div", "knobs-wt-more-text");
20679
+ for (const para of text.split(/\n\s*\n/))
20680
+ if (para.trim())
20681
+ body.append(prose2(doc, "p", "", para.trim()));
20682
+ discs.push(disclosure(box, { label: "More", content: body }));
20683
+ return box;
20684
+ };
20685
+ const tocRow = (label, sub, run, section) => {
20302
20686
  const b = el2(doc, "button", sub ? "knobs-wt-toc-row knobs-wt-toc-sub" : "knobs-wt-toc-row");
20303
20687
  b.type = "button";
20304
20688
  if (section)
20305
20689
  b.dataset.section = section;
20306
- const ncol = el2(doc, "span", "knobs-wt-toc-n");
20307
- ncol.textContent = n;
20308
20690
  const t = el2(doc, "span", "knobs-wt-toc-label");
20309
20691
  t.textContent = label;
20310
20692
  const e = el2(doc, "span", "knobs-wt-toc-end");
20311
- e.textContent = end;
20312
20693
  b.title = label;
20313
- b.append(ncol, t, e);
20694
+ b.append(t, e);
20314
20695
  b.addEventListener("mousedown", (ev) => ev.preventDefault());
20315
20696
  b.addEventListener("click", run);
20316
20697
  tocRows.push(b);
@@ -20368,33 +20749,26 @@ function walkthrough(container, options) {
20368
20749
  head.append(h3);
20369
20750
  if (step.caption)
20370
20751
  head.append(prose2(doc, "p", "knobs-wt-caption", step.caption));
20752
+ if (step.more)
20753
+ head.append(more(step.more));
20371
20754
  card.append(head);
20372
20755
  const slots = [];
20373
20756
  const onEvent = (e) => record({ ...e, step: step.key });
20374
20757
  let media = null;
20375
20758
  if (step.media.length) {
20376
- const grid = (list) => {
20377
- const g = el2(doc, "div", "knobs-wt-media");
20378
- g.style.setProperty("--kw-n", String(Math.min(list.length, step.columns)));
20379
- for (const m of list)
20380
- slots.push(mediaSlot(g, { ...look, media: m, onEvent, steps: stepLinks }));
20381
- return g;
20382
- };
20383
- const [first, ...rest] = step.media;
20384
- media = grid(step.more ? [first] : step.media);
20759
+ media = el2(doc, "div", "knobs-wt-media");
20760
+ media.style.setProperty("--kw-n", String(Math.min(step.media.length, step.columns)));
20761
+ for (const m of step.media)
20762
+ slots.push(mediaSlot(media, { ...look, media: m, onEvent, steps: stepLinks }));
20385
20763
  card.append(media);
20386
- if (step.more && rest.length) {
20387
- const holder = el2(doc, "div", "knobs-wt-more");
20388
- discs.push(disclosure(holder, { label: step.more, count: rest.length, content: grid(rest) }));
20389
- card.append(holder);
20390
- }
20391
20764
  }
20392
20765
  let taste = null;
20393
- if (step.question)
20766
+ if (step.question && review)
20394
20767
  card.append(prose2(doc, "p", "knobs-wt-question", step.question));
20395
20768
  if (step.ask === "pick") {
20396
20769
  taste = tasteTest(card, {
20397
20770
  ...look,
20771
+ pickable: review,
20398
20772
  options: step.options,
20399
20773
  picked: state.steps[step.key]?.pick ?? null,
20400
20774
  onPick: (id) => pick(step.key, id),
@@ -20403,7 +20777,8 @@ function walkthrough(container, options) {
20403
20777
  media ??= taste.element;
20404
20778
  }
20405
20779
  const notes = el2(doc, "div", "knobs-wt-notes");
20406
- card.append(notes);
20780
+ if (review)
20781
+ card.append(notes);
20407
20782
  card.addEventListener("focusin", () => {
20408
20783
  if (scroll.to)
20409
20784
  return;
@@ -20413,9 +20788,11 @@ function walkthrough(container, options) {
20413
20788
  let toc = null;
20414
20789
  let glyph = null;
20415
20790
  if (group) {
20416
- toc = tocRow(plainMarkdown(step.title), "", "", true, () => go(step.key));
20417
- glyph = el2(doc, "span", "knobs-wt-toc-glyph");
20418
- toc.querySelector(".knobs-wt-toc-end").append(glyph);
20791
+ toc = tocRow(plainMarkdown(step.title), true, () => go(step.key));
20792
+ if (review) {
20793
+ glyph = el2(doc, "span", "knobs-wt-toc-glyph");
20794
+ toc.querySelector(".knobs-wt-toc-end").append(glyph);
20795
+ }
20419
20796
  group.append(toc);
20420
20797
  }
20421
20798
  return { step, card, media, notes, toc, glyph, taste, slots };
@@ -20493,7 +20870,7 @@ function walkthrough(container, options) {
20493
20870
  return;
20494
20871
  }
20495
20872
  const v = viewOf(current);
20496
- if (!v || e.shiftKey)
20873
+ if (!v || e.shiftKey || !review)
20497
20874
  return;
20498
20875
  const choice = digitChoice(v.step, state.steps[v.step.key], key);
20499
20876
  if (choice === undefined)