@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-solid.js CHANGED
@@ -595,6 +595,8 @@ var compareCss = `
595
595
  .knobs-cmp-stage{position:relative;flex:1;min-height:96px;overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_ku-cmp-stage);touch-action:pan-y;outline:none}
596
596
  .knobs-compare[data-frame="flat"] .knobs-cmp-stage{border-radius:0}
597
597
  .knobs-compare[data-mode="wipe"] .knobs-cmp-stage{cursor:ew-resize}
598
+ /* Videos: a click on the stage plays and pauses. */
599
+ .knobs-compare[data-video]:not([data-mode="wipe"],[data-zoomed]) .knobs-cmp-stage{cursor:pointer}
598
600
  /* Unzoomed, a finger still scrolls the page up and down; zoomed, the stage takes every gesture. */
599
601
  .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:grab;touch-action:none}
600
602
  .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:grabbing!important}
@@ -604,6 +606,9 @@ var compareCss = `
604
606
  .knobs-cmp-canvas{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
605
607
  .knobs-cmp-media{position:absolute;left:0;top:0;display:block;max-width:none;margin:0;padding:0;border:0;object-fit:fill;transform-origin:0 0;pointer-events:none;will-change:transform}
606
608
  .knobs-compare[data-renderer="gpu"] .knobs-cmp-media{opacity:0}
609
+ /* The media comes in with its first frames: a fade and a 4px blur on the default spring. */
610
+ .knobs-cmp-canvas,.knobs-compare[data-renderer="dom"] .knobs-cmp-media{transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
611
+ .knobs-compare:not([data-ready]) :is(.knobs-cmp-canvas,.knobs-cmp-media){opacity:0;filter:blur(4px)}
607
612
  /* Glass: what's behind shows through, blurred, on the knobs tokens. */
608
613
  .knobs-cmp-label,.knobs-cmp-zoom{background:color-mix(in srgb,var(--_k-bg) 52%,transparent);-webkit-backdrop-filter:blur(14px) saturate(1.6);backdrop-filter:blur(14px) saturate(1.6);
609
614
  box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim),0 2px 10px rgba(0,0,0,calc(.18 * var(--_k-shadow-k,1)))}
@@ -612,11 +617,17 @@ var compareCss = `
612
617
  white-space:nowrap;pointer-events:none;will-change:transform}
613
618
  .knobs-cmp-label[data-side="before"]{color:var(--_k-fg-3)}
614
619
  .knobs-cmp-label[data-side="after"]{color:var(--_k-fg)}
615
- .knobs-cmp-mark svg{display:block;width:12px;height:12px}
616
- /* The wipe: a hairline down the line with a raised grip at the pointer's height; a wider strip
617
- takes the hand and the keys (a slider). It fades with the mode; wiping, line and grip brighten. */
618
- .knobs-cmp-handle{cursor:ew-resize;position:absolute;left:-12px;top:0;width:24px;height:0;opacity:0;pointer-events:none;outline:none;will-change:transform;transition:opacity .18s cubic-bezier(.4,0,1,1)}
619
- .knobs-cmp-handle[data-on]{opacity:1;pointer-events:auto;transition:opacity .22s cubic-bezier(.2,.8,.2,1)}
620
+ /* Fusing, the mark folds away and the label is its name. */
621
+ .knobs-cmp-mark{flex:none;display:flex;justify-content:flex-end;overflow:hidden}
622
+ .knobs-cmp-mark svg{display:block;flex:none;width:12px;height:12px}
623
+ /* The fuse's seam: a blurred strip over where the sides meet, soft at both sides. */
624
+ .knobs-cmp-seam{position:absolute;left:0;top:0;width:8px;height:0;opacity:0;pointer-events:none;will-change:transform,opacity;
625
+ -webkit-mask-image:linear-gradient(to right,transparent,#000 35%,#000 65%,transparent);mask-image:linear-gradient(to right,transparent,#000 35%,#000 65%,transparent)}
626
+ /* The wipe: a hairline down the line with a raised grip in its middle (at the pointer's height
627
+ while dragged); a wider strip takes the hand and the keys (a slider). It fades in with the
628
+ fuse; wiping, line and grip brighten. */
629
+ .knobs-cmp-handle{cursor:ew-resize;position:absolute;left:-12px;top:0;width:24px;height:0;opacity:0;pointer-events:none;outline:none;will-change:transform,opacity}
630
+ .knobs-cmp-handle[data-on]{pointer-events:auto}
620
631
  .knobs-cmp-handle::before{content:"";position:absolute;top:0;bottom:0;left:11.5px;width:1px;background:color-mix(in srgb,var(--_ku-cmp-line) 72%,transparent);box-shadow:0 0 0 .5px rgba(0,0,0,.28),0 0 6px rgba(0,0,0,.25)}
621
632
  .knobs-compare[data-dragging] .knobs-cmp-handle::before{background:var(--_ku-cmp-line)}
622
633
  .knobs-cmp-grip{position:absolute;top:0;left:4px;width:16px;height:28px;border-radius:min(8px,calc(var(--_k-radius) * 999));background:var(--_k-bg-overlay);
@@ -648,6 +659,27 @@ var compareCss = `
648
659
  @media(prefers-reduced-motion:reduce){:is(.knobs-compare,.knobs-cmp-strip) :where(*,::before,::after){transition-duration:.12s!important}}
649
660
  `;
650
661
 
662
+ // src/ui/player-css.ts
663
+ var playerCss = `
664
+ :where(.knobs-player,.knobs-player *){box-sizing:border-box}
665
+ .knobs-ui.knobs-player{--_kp-stage:var(--_k-compare-stage);display:flex;flex-direction:column;gap:6px;width:100%;outline:none;
666
+ font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
667
+ :where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-ui.knobs-player,.knobs-ui.knobs-player[data-nested]{--_kp-stage:var(--k-compare-stage,transparent)}
668
+ .knobs-player-frame{position:relative;width:100%;aspect-ratio:var(--kp-aspect,16/9);overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_kp-stage);cursor:pointer}
669
+ .knobs-player[data-frame="flat"] .knobs-player-frame{border-radius:0}
670
+ .knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
671
+ opacity:0;filter:blur(4px);transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
672
+ .knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}
673
+ .knobs-player-glyph{position:absolute;left:50%;top:50%;display:grid;place-items:center;width:44px;height:44px;margin:-22px 0 0 -22px;border-radius:50%;color:var(--_k-fg);pointer-events:none;
674
+ opacity:0;filter:blur(4px);scale:.96;
675
+ background:color-mix(in srgb,var(--_k-bg) 52%,transparent);-webkit-backdrop-filter:blur(14px) saturate(1.6);backdrop-filter:blur(14px) saturate(1.6);
676
+ box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim);
677
+ transition:opacity calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy),filter calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy),scale calc(var(--_k-motion-snappy-ms) * 1ms) var(--_k-motion-snappy)}
678
+ .knobs-player-glyph svg{width:16px;height:16px}
679
+ .knobs-player-glyph[data-flash]{opacity:1;filter:none;scale:1}
680
+ .knobs-player-line{flex:none;min-height:0}
681
+ `;
682
+
651
683
  // src/ui/overlay-css.ts
652
684
  var overlayCss = `
653
685
  .knobs-layers{position:fixed;inset:0;z-index:2147480005;pointer-events:none}
@@ -1512,7 +1544,7 @@ var init_schema = __esm(() => {
1512
1544
  type: "length",
1513
1545
  group: "Type",
1514
1546
  source: value("20px"),
1515
- doc: "Section headings (a PR's title, under its kicker), weight 500."
1547
+ doc: "A page's title, weight 500."
1516
1548
  },
1517
1549
  {
1518
1550
  name: "leading-heading",
@@ -1528,7 +1560,7 @@ var init_schema = __esm(() => {
1528
1560
  type: "length",
1529
1561
  group: "Type",
1530
1562
  source: value("16px"),
1531
- doc: "A page's title, weight 500."
1563
+ doc: "Section titles (under their kicker), weight 500."
1532
1564
  },
1533
1565
  {
1534
1566
  name: "leading-title",
@@ -1560,7 +1592,7 @@ var init_schema = __esm(() => {
1560
1592
  type: "length",
1561
1593
  group: "Type",
1562
1594
  source: value("12px"),
1563
- doc: "Labels over media, weight 500; mono inside body text."
1595
+ doc: "Mono inside body text: code and paths."
1564
1596
  },
1565
1597
  {
1566
1598
  name: "leading-label",
@@ -3003,8 +3035,6 @@ var css2 = `
3003
3035
  .knobs-tl-hover{position:absolute;left:0;top:0;bottom:0;width:1px;margin-left:-.5px;background:var(--_k-border-strong);transform:translateX(-100px);pointer-events:none;opacity:0;transition:opacity .15s}
3004
3036
  .knobs-tl-track:hover .knobs-tl-hover{opacity:1}
3005
3037
  .knobs-timeline[data-scrubbing] .knobs-tl-hover{opacity:0}
3006
- @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
3007
- @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
3008
3038
  .knobs-tl-tag{position:absolute;left:0;bottom:2px;font-size:var(--_k-text-meta);line-height:16px;color:var(--_k-fg);font-variant-numeric:tabular-nums;font-feature-settings:"tnum";white-space:nowrap;pointer-events:none;opacity:0}
3009
3039
  .knobs-tl-tag[data-on]{opacity:1}
3010
3040
  /* The lane (variant: "lane"): one slim track of ticks under a compare or a video; markers are
@@ -3020,7 +3050,26 @@ var css2 = `
3020
3050
  .knobs-timeline[data-variant="lane"] .knobs-tl-head::before{top:0}
3021
3051
  .knobs-timeline[data-variant="lane"] .knobs-tl-head::after{content:none}
3022
3052
  .knobs-timeline[data-variant="lane"] .knobs-tl-tag{top:2px;bottom:auto}
3023
- `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + selectionPillCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
3053
+ /* The line (variant: "line"): a player's progress under its media. One 2px line, fg-3 with fg
3054
+ filling it, a segment per chapter; it thickens under the pointer and while scrubbing. The
3055
+ chapter and time under the pointer float over it; nothing else shows. */
3056
+ .knobs-ui.knobs-timeline[data-variant="line"]{height:auto;min-height:0;padding:0;gap:0}
3057
+ .knobs-timeline[data-variant="line"] .knobs-tl-transport{display:none}
3058
+ .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}
3059
+ .knobs-timeline[data-variant="line"] :is(.knobs-tl-ruler,.knobs-tl-lane,.knobs-tl-hover,.knobs-tl-head,.knobs-tl-rlabel){display:none}
3060
+ .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)}
3061
+ .knobs-tl-seg{position:absolute;left:0;top:0;height:100%;overflow:hidden;border-radius:1px;background:var(--_k-fg-3);will-change:transform}
3062
+ .knobs-tl-seg-fill{position:absolute;inset:0;background:var(--_k-fg);transform-origin:0 0;will-change:transform}
3063
+ .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}
3064
+ .knobs-timeline[data-variant="line"] .knobs-tl-range{top:3px;bottom:3px;border-radius:3px}
3065
+ .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)}
3066
+ .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);
3067
+ -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:inset 0 0 0 .5px var(--_k-border)}
3068
+ .knobs-tl-tag-label{color:var(--_k-fg-2)}
3069
+ .knobs-tl-tag-label:empty{display:none}
3070
+ @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
3071
+ @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
3072
+ `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + playerCss + selectionPillCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
3024
3073
  s.omega = m.omega;
3025
3074
  s.zeta = m.zeta;
3026
3075
  return s;
@@ -3143,19 +3192,19 @@ var init_diff_css = __esm(() => {
3143
3192
  @property --kd-fb{syntax:"<length>";inherits:false;initial-value:0px}
3144
3193
  :where(.knobs-ui){--kd-add:var(--_k-diff-add);--kd-del:var(--_k-diff-del);--kd-mod:var(--_k-diff-mod);--kd-move:var(--k-diff-move,var(--_k-info))}
3145
3194
  :where(.knobs-diff,.knobs-diff *,.knobs-diff-files,.knobs-diff-files *){box-sizing:border-box}
3146
- .knobs-ui.knobs-diff{--kd-bg:var(--k-diff-bg,var(--_kd-bg,var(--_k-bg)));--kd-solid:var(--kd-fill,var(--kd-bg));--kd-line:calc(var(--_k-font-size) * 1.5);
3195
+ .knobs-ui.knobs-diff{--kd-bg:var(--k-diff-bg,var(--_kd-bg,transparent));--kd-solid:var(--kd-fill,var(--kd-bg));--kd-line:calc(var(--_k-font-size) * 1.5);
3147
3196
  --kd-i:var(--k-diff-inset,8px);--kd-x:var(--kd-i);--kd-inner:max(3px,calc(var(--_ku-pane-radius) - var(--kd-i)));--kd-ctl:var(--kd-inner);
3148
3197
  --kd-head:calc(var(--_k-control-h) + 2 * var(--kd-i));--kd-fade:var(--k-diff-fade,20px);--kd-stick:0px;position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;
3149
3198
  background:var(--kd-bg);border-radius:var(--_ku-pane-radius);font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);text-align:left;
3150
3199
  letter-spacing:normal;word-spacing:normal;text-transform:none;-webkit-font-smoothing:antialiased}
3151
3200
  .knobs-ui.knobs-diff[data-height="auto"]{height:auto}
3152
- .knobs-diff::after{content:"";position:absolute;inset:0;z-index:3;border-radius:inherit;box-shadow:var(--_ku-pane-ring);pointer-events:none}
3201
+ .knobs-ui.knobs-diff:not([data-on="pane"])::after{content:"";position:absolute;inset:0;z-index:3;box-shadow:inset 0 -1px 0 var(--_k-border-subtle);pointer-events:none}
3153
3202
  .knobs-diff [hidden],.knobs-diff-files [hidden]{display:none!important}
3154
3203
  .knobs-diff svg{display:block;flex:none}
3155
- .knobs-ui.knobs-diff[data-on="surface"]{--kd-x:0px;--kd-head:var(--_k-control-h);--kd-ctl:var(--_k-radius)}
3156
- /* The header's hairline shows only once the code has scrolled under it. */
3204
+ .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)}
3205
+ /* A hairline under the header, firmer once the code has scrolled under it. */
3157
3206
  .knobs-diff-head{position:relative;z-index:1;flex:none;display:flex;align-items:center;gap:8px;height:var(--kd-head);padding:0 var(--kd-x);
3158
- box-shadow:inset 0 -1px transparent;transition:box-shadow .15s;user-select:none;-webkit-user-select:none}
3207
+ box-shadow:inset 0 -1px var(--_k-border-subtle);transition:box-shadow .15s;user-select:none;-webkit-user-select:none}
3159
3208
  .knobs-diff[data-scrolled]>.knobs-diff-head{box-shadow:inset 0 -1px var(--_k-border)}
3160
3209
  .knobs-diff-path{flex:0 1 auto;min-width:0;overflow:hidden;white-space:nowrap}
3161
3210
  .knobs-diff-dir,.knobs-diff-from{color:var(--_k-fg-3)}
@@ -3193,6 +3242,15 @@ var init_diff_css = __esm(() => {
3193
3242
  .knobs-ui .knobs-diff-viewed[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
3194
3243
  .knobs-diff-viewed svg{width:16px;height:16px}
3195
3244
  .knobs-diff-viewed[aria-pressed="true"] svg{color:var(--kd-add)}
3245
+ /* Narrow, the header gives up words before anything is cut off at its end: Viewed keeps its
3246
+ check, then the counts' bar goes, then the counts; the path fades at its end throughout. */
3247
+ .knobs-diff-head,.knobs-diff-part-head{container-type:inline-size}
3248
+ @container (max-width:520px){
3249
+ .knobs-diff-viewed-label{display:none}
3250
+ .knobs-ui .knobs-diff-viewed{width:var(--_k-control-h);padding:0;justify-content:center}
3251
+ .knobs-diff-counts .knobs-diff-bar{display:none}
3252
+ }
3253
+ @container (max-width:400px){.knobs-diff-counts{display:none}.knobs-diff-nav>span{min-width:2.6em}}
3196
3254
  .knobs-diff-path{transition:opacity .2s}
3197
3255
  .knobs-diff[data-viewed]>.knobs-diff-head .knobs-diff-path,.knobs-diff-part[data-viewed] .knobs-diff-path{opacity:.6}
3198
3256
  /* Split | Unified: the selection pill on the one picked, its bright copy the picked one's text; a
@@ -3257,24 +3315,26 @@ var init_diff_css = __esm(() => {
3257
3315
  --diffs-bg-deletion-number-override:color-mix(in srgb,var(--kd-del) 55%,var(--kd-solid));
3258
3316
  --diffs-bg-addition-emphasis-override:color-mix(in srgb,var(--kd-add) 22%,transparent);
3259
3317
  --diffs-bg-deletion-emphasis-override:color-mix(in srgb,var(--kd-del) 22%,transparent);
3260
- --diffs-bg-separator-override:color-mix(in srgb,var(--_k-fg) 4.5%,var(--kd-solid));
3261
- --diffs-bg-context-override:color-mix(in srgb,var(--_k-fg) 3%,var(--kd-solid));
3262
- --diffs-bg-buffer-override:color-mix(in srgb,var(--_k-fg) 3.5%,var(--kd-solid));
3318
+ /* Unchanged lines and the folds between hunks sit on the void; only a split's missing lines
3319
+ are faintly filled. */
3320
+ --diffs-bg-separator-override:var(--kd-solid);
3321
+ --diffs-bg-context-override:var(--kd-solid);
3322
+ --diffs-bg-buffer-override:color-mix(in srgb,var(--_k-fg) 2.5%,var(--kd-solid));
3263
3323
  --diffs-bg-selection-override:var(--_k-accent);
3264
3324
  --diffs-fg-number-override:var(--_k-fg-3);
3265
3325
  }
3266
- /* diffReview: one card, the file list a drawer at its left (its width springs; the list inside
3267
- is pinned to its right edge, so it slides out from under the diff), parted from the diff by a
3268
- hairline. The diff in it draws no card of its own. */
3269
- .knobs-ui.knobs-diff-review{position:relative;display:flex;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;border-radius:var(--_ku-pane-radius);background:var(--_ku-pane-bg);
3326
+ /* diffReview, on the void as the diff is: the file list a drawer at its left (its width springs;
3327
+ the list inside is pinned to its right edge, so it slides out from under the diff), parted
3328
+ from the diff by a hairline; one hairline runs under both headers and one under it all. */
3329
+ .knobs-ui.knobs-diff-review{position:relative;display:flex;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;
3270
3330
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);text-align:left;letter-spacing:normal;-webkit-font-smoothing:antialiased}
3271
- .knobs-diff-review::after{content:"";position:absolute;inset:0;z-index:5;border-radius:inherit;box-shadow:var(--_ku-pane-ring);pointer-events:none}
3331
+ .knobs-diff-review::after{content:"";position:absolute;inset:0;z-index:5;box-shadow:inset 0 -1px 0 var(--_k-border-subtle);pointer-events:none}
3272
3332
  :where(.knobs-diff-review,.knobs-diff-review *){box-sizing:border-box}
3273
- .knobs-diff-drawer{position:relative;flex:none;width:0;height:100%;overflow:hidden;box-shadow:inset -1px 0 var(--_k-border)}
3333
+ .knobs-diff-drawer{position:relative;flex:none;width:0;height:100%;overflow:hidden;box-shadow:inset -1px 0 var(--_k-border-subtle)}
3274
3334
  .knobs-diff-drawer[data-shut]{visibility:hidden}
3275
3335
  .knobs-diff-drawer-in{position:absolute;top:0;bottom:0;right:0;display:flex;flex-direction:column}
3276
3336
  .knobs-diff-drawer-head{flex:none;display:flex;align-items:center;gap:6px;height:calc(var(--_k-control-h) + 2 * var(--k-diff-inset,8px));padding:0 var(--k-diff-inset,8px) 0 calc(var(--_ku-row-x) + 8px);
3277
- user-select:none;-webkit-user-select:none}
3337
+ box-shadow:inset 0 -1px var(--_k-border-subtle);user-select:none;-webkit-user-select:none}
3278
3338
  .knobs-diff-drawer-title{font-weight:var(--_k-weight-title)}
3279
3339
  .knobs-diff-drawer-count{color:var(--_k-fg-3);font-size:var(--_k-text-meta);font-variant-numeric:tabular-nums}
3280
3340
  .knobs-diff-drawer-in>.knobs-diff-files{flex:1;min-height:0;height:auto}
@@ -3286,10 +3346,11 @@ var init_diff_css = __esm(() => {
3286
3346
  .knobs-ui .knobs-diff-toggle[aria-pressed]:not(:hover,:focus-visible){background:transparent}
3287
3347
  /* diffReview's file list: a filter over a compact tree, a status letter, the counts and their
3288
3348
  bar on each file; one highlight glides to the file on show. Viewed files dim, with a tick. */
3289
- .knobs-diff-files{--k-row:var(--k-diff-row,28px);display:flex;flex-direction:column;height:100%;min-height:0;padding-top:2px}
3349
+ .knobs-diff-files{--k-row:var(--k-diff-row,28px);display:flex;flex-direction:column;height:100%;min-height:0;padding-top:4px}
3290
3350
  .knobs-diff-filter{position:relative;flex:none;display:flex;align-items:center;gap:6px;height:26px;margin:0 var(--_ku-row-x) 4px;padding:0 4px 0 8px;
3291
- border-radius:max(6px,var(--_ku-row-radius));background:var(--_ku-wash);color:var(--_k-fg-3);box-shadow:inset 0 0 0 1px transparent;transition:background .15s,box-shadow .15s}
3292
- .knobs-diff-filter:focus-within{background:var(--_ku-wash-hi);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_k-accent) 45%,transparent)}
3351
+ 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}
3352
+ /* The filter is a line of text, no box; focused, a hairline ring. */
3353
+ .knobs-diff-filter:focus-within{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_k-accent) 45%,transparent)}
3293
3354
  .knobs-diff-filter svg{width:12px;height:12px;flex:none}
3294
3355
  .knobs-ui .knobs-diff-filter>input{flex:1;min-width:0;height:100%;font:var(--_k-weight) var(--_k-font-size)/1 var(--_k-font);color:var(--_k-fg);caret-color:var(--_k-accent);background:transparent}
3295
3356
  .knobs-ui .knobs-diff-filter>input::placeholder{color:var(--_k-fg-3)}
@@ -6214,7 +6275,10 @@ function codeDiff(container, options) {
6214
6275
  const viewedToggle = () => {
6215
6276
  const b = make("button", "knobs-diff-viewed");
6216
6277
  b.type = "button";
6217
- b.append(make("span", ""), "Viewed");
6278
+ const label = make("span", "knobs-diff-viewed-label");
6279
+ label.textContent = "Viewed";
6280
+ b.append(make("span", ""), label);
6281
+ b.setAttribute("aria-label", "Viewed");
6218
6282
  return b;
6219
6283
  };
6220
6284
  const paintViewed = (b, on) => {
@@ -8501,6 +8565,17 @@ function currentMarker(markers, t, fps) {
8501
8565
  best = m;
8502
8566
  return best;
8503
8567
  }
8568
+ function minutes(t) {
8569
+ const s = Math.max(0, Math.floor(t + 0.000001));
8570
+ const h = Math.floor(s / 3600);
8571
+ const m = Math.floor(s % 3600 / 60);
8572
+ const ss = String(s % 60).padStart(2, "0");
8573
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
8574
+ }
8575
+ function chapterBounds(times, duration) {
8576
+ const inside = times.filter((t) => t > 0.000001 && t < duration - 0.000001);
8577
+ return [0, ...[...new Set(inside)].sort((a, b) => a - b), duration];
8578
+ }
8504
8579
  var SECOND_STEPS, FRAME_STEPS, toFrame = (t, fps) => Math.round(t * fps), frameTime = (t, fps) => Math.round(t * fps) / fps, round = (t) => Math.round(t * 1e6) / 1e6, CATCH_UP;
8505
8580
  var init_timeline_model = __esm(() => {
8506
8581
  SECOND_STEPS = [0.1, 0.2, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600];
@@ -8518,7 +8593,8 @@ function timeline(container, options) {
8518
8593
  let speed = options.speed ?? 1;
8519
8594
  let repeat = options.repeat ?? true;
8520
8595
  const arrows = options.arrows ?? "frames";
8521
- const lane = options.variant === "lane";
8596
+ const line = options.variant === "line";
8597
+ const lane = line || options.variant === "lane";
8522
8598
  let time = Math.min(duration, Math.max(0, options.time ?? 0));
8523
8599
  let playing = false;
8524
8600
  let markers = [...options.markers ?? []];
@@ -8553,7 +8629,7 @@ function timeline(container, options) {
8553
8629
  root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight , . Home End [ ] I O M");
8554
8630
  applyLook2(root, options);
8555
8631
  if (lane)
8556
- root.dataset.variant = "lane";
8632
+ root.dataset.variant = line ? "line" : "lane";
8557
8633
  const transport = el("div", "knobs-tl-transport");
8558
8634
  const playBtn = el("button", "knobs-tl-play");
8559
8635
  playBtn.type = "button";
@@ -8566,6 +8642,10 @@ function timeline(container, options) {
8566
8642
  const hover = el("div", "knobs-tl-hover");
8567
8643
  const tag = el("span", "knobs-tl-tag");
8568
8644
  tag.setAttribute("aria-hidden", "true");
8645
+ const tagLabel = el("span", "knobs-tl-tag-label");
8646
+ const tagTime = el("span", "knobs-tl-tag-time");
8647
+ tag.append(tagLabel, tagTime);
8648
+ const segs = el("div", "knobs-tl-segs");
8569
8649
  const head = el("div", "knobs-tl-head");
8570
8650
  head.id = `${id}-head`;
8571
8651
  head.setAttribute("role", "slider");
@@ -8573,11 +8653,13 @@ function timeline(container, options) {
8573
8653
  head.setAttribute("aria-valuemin", "0");
8574
8654
  head.setAttribute("aria-valuemax", String(duration));
8575
8655
  track.append(canvas, markerLane, hover, head, tag);
8656
+ if (line)
8657
+ track.insertBefore(segs, markerLane);
8576
8658
  root.append(transport, track);
8577
8659
  container.append(root);
8578
8660
  let width = 1;
8579
8661
  let height = 1;
8580
- const fitView = () => clampView({ start: 0, span: duration }, duration, { pad: 0.02 });
8662
+ const fitView = () => clampView({ start: 0, span: duration }, duration, { pad: line ? 0 : 0.02 });
8581
8663
  let target = fitView();
8582
8664
  const vStart = new Spring(target.start, INSTANT2.omega, 1, 0.00001);
8583
8665
  const vSpan = new Spring(target.span, INSTANT2.omega, 1, 0.00001);
@@ -8586,13 +8668,13 @@ function timeline(container, options) {
8586
8668
  const xOf = (t) => (t - vStart.x) * pxPerSec();
8587
8669
  const tOf = (x) => vStart.x + x / pxPerSec();
8588
8670
  const minSpan = () => Math.max(4 / fps, 0.05);
8589
- const retarget = (next, motion = INSTANT2) => {
8590
- target = clampView(next, duration, { pad: 0.04, minSpan: minSpan() });
8671
+ const retarget = (next, motion = INSTANT2, hand = false) => {
8672
+ target = clampView(next, duration, { pad: line ? 0 : 0.04, minSpan: minSpan() });
8591
8673
  for (const s of [vStart, vSpan]) {
8592
8674
  s.omega = motion.omega;
8593
8675
  s.zeta = motion.zeta;
8594
8676
  }
8595
- if (reducedMotion(win) && motion === CALM) {
8677
+ if (hand || reducedMotion(win) && motion === CALM) {
8596
8678
  vStart.snap(target.start);
8597
8679
  vSpan.snap(target.span);
8598
8680
  } else {
@@ -8618,6 +8700,8 @@ function timeline(container, options) {
8618
8700
  };
8619
8701
  let drawn = "";
8620
8702
  const drawRuler = () => {
8703
+ if (line)
8704
+ return;
8621
8705
  const v = view();
8622
8706
  const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${height}:${units}:${fps}`;
8623
8707
  if (key === drawn)
@@ -8705,6 +8789,8 @@ function timeline(container, options) {
8705
8789
  n.title = `${m.label ? `${m.label} · ` : ""}${clock(m.time)}`;
8706
8790
  }
8707
8791
  placeMarkers();
8792
+ if (line)
8793
+ syncSegments();
8708
8794
  };
8709
8795
  const placeMarkers = () => {
8710
8796
  for (const m of markers) {
@@ -8713,6 +8799,36 @@ function timeline(container, options) {
8713
8799
  n.style.transform = `translateX(${xOf(m.time).toFixed(2)}px)`;
8714
8800
  }
8715
8801
  };
8802
+ let bounds = [];
8803
+ const segEls = [];
8804
+ const syncSegments = () => {
8805
+ bounds = chapterBounds(markers.map((m) => m.time), duration);
8806
+ while (segEls.length > bounds.length - 1)
8807
+ segEls.pop().el.remove();
8808
+ while (segEls.length < bounds.length - 1) {
8809
+ const n = el("i", "knobs-tl-seg");
8810
+ const fill = el("b", "knobs-tl-seg-fill");
8811
+ n.append(fill);
8812
+ segs.append(n);
8813
+ segEls.push({ el: n, fill });
8814
+ }
8815
+ placeSegments();
8816
+ };
8817
+ const placeSegments = () => {
8818
+ if (!line)
8819
+ return;
8820
+ const last = segEls.length - 1;
8821
+ segEls.forEach((s, i) => {
8822
+ const a = bounds[i];
8823
+ const b = bounds[i + 1];
8824
+ const x0 = xOf(a) + (i > 0 ? SEG_GAP / 2 : 0);
8825
+ const x1 = xOf(b) - (i < last ? SEG_GAP / 2 : 0);
8826
+ s.el.style.transform = `translateX(${x0.toFixed(2)}px)`;
8827
+ s.el.style.width = `${Math.max(0, x1 - x0).toFixed(2)}px`;
8828
+ const k = Math.min(1, Math.max(0, (shownHead - a) / Math.max(0.000000001, b - a)));
8829
+ s.fill.style.transform = `scaleX(${k.toFixed(4)})`;
8830
+ });
8831
+ };
8716
8832
  const syncRanges = () => {
8717
8833
  const keep = new Set(ranges.map((r) => r.id));
8718
8834
  for (const [k, n] of rangeEls)
@@ -8729,7 +8845,7 @@ function timeline(container, options) {
8729
8845
  b.dataset.edge = "end";
8730
8846
  const label = el("span", "knobs-tl-rlabel");
8731
8847
  n.append(label, a, b);
8732
- track.insertBefore(n, markerLane);
8848
+ track.insertBefore(n, line ? segs : markerLane);
8733
8849
  rangeEls.set(r.id, n);
8734
8850
  if (built)
8735
8851
  enter(n);
@@ -8781,6 +8897,7 @@ function timeline(container, options) {
8781
8897
  let currentId = null;
8782
8898
  const renderHead = () => {
8783
8899
  head.style.transform = `translateX(${xOf(shownHead).toFixed(2)}px)`;
8900
+ placeSegments();
8784
8901
  head.setAttribute("aria-valuenow", time.toFixed(3));
8785
8902
  head.setAttribute("aria-valuetext", `${time.toFixed(2)} seconds, frame ${toFrame(time, fps)}`);
8786
8903
  const cur = currentMarker(markers, time, fps)?.id ?? null;
@@ -9044,14 +9161,25 @@ function timeline(container, options) {
9044
9161
  setTime(s.t, "scrub", s.shown, press);
9045
9162
  root.toggleAttribute("data-stuck", s.stop !== null);
9046
9163
  };
9164
+ let pointerX = null;
9165
+ const zoomAtHand = (factor) => zoom(factor, pointerX === null ? time : target.start + pointerX / width * target.span);
9047
9166
  const showTag = (x) => {
9048
9167
  const at = Math.min(duration, Math.max(0, tOf(x)));
9049
- const text = `${at.toFixed(2)} s`;
9050
- if (tag.textContent !== text)
9051
- tag.textContent = text;
9168
+ const text = line ? minutes(at) : `${at.toFixed(2)} s`;
9169
+ if (tagTime.textContent !== text)
9170
+ tagTime.textContent = text;
9171
+ const label = line ? currentMarker(markers, at, fps)?.label ?? "" : "";
9172
+ if (tagLabel.textContent !== label)
9173
+ tagLabel.textContent = label;
9052
9174
  const cx = Math.min(width, Math.max(0, x));
9053
- const flip = cx > width - tag.offsetWidth - 12;
9054
- tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
9175
+ if (line) {
9176
+ const w = tag.offsetWidth;
9177
+ const left = Math.min(Math.max(0, width - w), Math.max(0, cx - w / 2));
9178
+ tag.style.transform = `translateX(${left.toFixed(1)}px)`;
9179
+ } else {
9180
+ const flip = cx > width - tag.offsetWidth - 12;
9181
+ tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
9182
+ }
9055
9183
  tag.dataset.on = "";
9056
9184
  };
9057
9185
  const hideTag = () => delete tag.dataset.on;
@@ -9112,6 +9240,7 @@ function timeline(container, options) {
9112
9240
  });
9113
9241
  track.addEventListener("pointermove", (e) => {
9114
9242
  const x = localX(e);
9243
+ pointerX = x;
9115
9244
  hover.style.transform = `translateX(${x.toFixed(1)}px)`;
9116
9245
  if (!drag || drag.kind === "scrub")
9117
9246
  showTag(x);
@@ -9185,6 +9314,7 @@ function timeline(container, options) {
9185
9314
  track.addEventListener("pointercancel", endDrag);
9186
9315
  track.addEventListener("lostpointercapture", endDrag);
9187
9316
  track.addEventListener("pointerleave", () => {
9317
+ pointerX = null;
9188
9318
  if (drag)
9189
9319
  return;
9190
9320
  hover.style.transform = "translateX(-100px)";
@@ -9192,7 +9322,7 @@ function timeline(container, options) {
9192
9322
  });
9193
9323
  track.addEventListener("dblclick", (e) => {
9194
9324
  if (lane)
9195
- return;
9325
+ return fit();
9196
9326
  const markerEl = e.target.closest(".knobs-tl-marker");
9197
9327
  if (markerEl)
9198
9328
  return rename(markerEl.dataset.id);
@@ -9203,18 +9333,40 @@ function timeline(container, options) {
9203
9333
  rename(m.id);
9204
9334
  });
9205
9335
  track.addEventListener("wheel", (e) => {
9206
- if (lane)
9336
+ const zooming = e.ctrlKey || e.metaKey;
9337
+ if (zooming && pinching)
9338
+ return e.preventDefault();
9339
+ const sideways = Math.abs(e.deltaX) > Math.abs(e.deltaY);
9340
+ if (lane && !zooming && !sideways)
9207
9341
  return;
9208
9342
  e.preventDefault();
9209
9343
  const scale = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? height : 1;
9210
- if (e.ctrlKey || e.metaKey) {
9344
+ if (zooming) {
9211
9345
  const at = target.start + localX(e) / width * target.span;
9212
- retarget(zoomView(target, Math.exp(-e.deltaY * scale * (e.ctrlKey ? 0.01 : 0.002)), at));
9346
+ const k = Math.exp(-e.deltaY * scale * (e.ctrlKey ? 0.01 : 0.002));
9347
+ retarget(zoomView(target, k, at), INSTANT2, lane);
9213
9348
  } else {
9214
- const d = (Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY) * scale;
9215
- retarget({ ...target, start: target.start + d / width * target.span });
9349
+ const d = (sideways ? e.deltaX : e.deltaY) * scale;
9350
+ retarget({ ...target, start: target.start + d / width * target.span }, INSTANT2, lane);
9216
9351
  }
9217
9352
  }, { passive: false });
9353
+ let pinchScale = 1;
9354
+ let pinching = false;
9355
+ const onGesture = (e) => {
9356
+ const g = e;
9357
+ e.preventDefault();
9358
+ if (e.type === "gesturestart")
9359
+ pinchScale = 1;
9360
+ pinching = e.type !== "gestureend";
9361
+ if (e.type !== "gesturechange")
9362
+ return;
9363
+ const s = g.scale ?? 1;
9364
+ const x = g.clientX !== undefined ? localX({ clientX: g.clientX }) : width / 2;
9365
+ retarget(zoomView(target, s / pinchScale, target.start + x / width * target.span), INSTANT2, true);
9366
+ pinchScale = s;
9367
+ };
9368
+ for (const type of ["gesturestart", "gesturechange", "gestureend"])
9369
+ track.addEventListener(type, onGesture);
9218
9370
  root.addEventListener("keydown", (e) => {
9219
9371
  if (e.target !== root || e.metaKey || e.ctrlKey) {
9220
9372
  if ((e.metaKey || e.ctrlKey) && (e.key === "=" || e.key === "-" || e.key === "0") && e.target === root) {
@@ -9222,7 +9374,7 @@ function timeline(container, options) {
9222
9374
  if (e.key === "0")
9223
9375
  fit();
9224
9376
  else
9225
- zoom(e.key === "=" ? 1.5 : 1 / 1.5);
9377
+ zoomAtHand(e.key === "=" ? 1.5 : 1 / 1.5);
9226
9378
  }
9227
9379
  return;
9228
9380
  }
@@ -9304,18 +9456,12 @@ function timeline(container, options) {
9304
9456
  return;
9305
9457
  case "=":
9306
9458
  case "+":
9307
- if (lane)
9308
- return;
9309
9459
  claim();
9310
- return zoom(1.5);
9460
+ return zoomAtHand(1.5);
9311
9461
  case "-":
9312
- if (lane)
9313
- return;
9314
9462
  claim();
9315
- return zoom(1 / 1.5);
9463
+ return zoomAtHand(1 / 1.5);
9316
9464
  case "0":
9317
- if (lane)
9318
- return;
9319
9465
  claim();
9320
9466
  return fit();
9321
9467
  }
@@ -9531,7 +9677,7 @@ function timeline(container, options) {
9531
9677
  };
9532
9678
  return api;
9533
9679
  }
9534
- var RULER = 22, INSTANT2, CALM, GLIDE, uid2 = 0, PLAY2 = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.6v8.8c0 .5.5.8.9.5l6.6-4.4c.4-.3.4-.8 0-1L5.9 3.1c-.4-.3-.9 0-.9.5Z" fill="currentColor"/></svg>', PAUSE = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="4" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/><rect x="9.2" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/></svg>';
9680
+ var RULER = 22, SEG_GAP = 3, INSTANT2, CALM, GLIDE, uid2 = 0, PLAY2 = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.6v8.8c0 .5.5.8.9.5l6.6-4.4c.4-.3.4-.8 0-1L5.9 3.1c-.4-.3-.9 0-.9.5Z" fill="currentColor"/></svg>', PAUSE = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="4" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/><rect x="9.2" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/></svg>';
9535
9681
  var init_timeline = __esm(() => {
9536
9682
  init_rgba();
9537
9683
  init_motion();
@@ -9603,15 +9749,28 @@ function compareLayout(box, before, after) {
9603
9749
  const shape = after.w > 0 && after.h > 0 ? after : before;
9604
9750
  const pair = letterbox({ w: shape.w * 2, h: shape.h }, box);
9605
9751
  const half = pair.w / 2;
9752
+ const fused = { x: pair.x + half / 2, y: pair.y, w: half, h: pair.h };
9606
9753
  return {
9607
9754
  side: {
9608
9755
  before: letterbox(before, { x: pair.x, y: pair.y, w: half, h: pair.h }),
9609
9756
  after: letterbox(after, { x: pair.x + half, y: pair.y, w: half, h: pair.h }),
9610
9757
  split: pair.x + half
9611
9758
  },
9612
- overlay: { before: letterbox(before, box), after: letterbox(after, box) }
9759
+ overlay: { before: letterbox(before, fused), after: letterbox(after, fused) }
9613
9760
  };
9614
9761
  }
9762
+ function fuseFrame(L, before, after, k, wipe) {
9763
+ const t = Math.min(1, Math.max(0, k));
9764
+ const o = L.overlay;
9765
+ const left = Math.min(o.before.x, o.after.x);
9766
+ 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);
9767
+ const cut = L.side.split + (line - L.side.split) * t;
9768
+ const contact = Math.max(0, Math.min(cut - after.x, before.x + before.w - cut));
9769
+ const feather = Math.min(contact, 1 + FUSE.feather * Math.sin(Math.PI * t));
9770
+ const overlap = before.x + before.w - after.x;
9771
+ const seam = t > 0.001 && overlap >= 0 ? Math.min(1, Math.max(0, overlap / FUSE.seamFade)) * Math.min(1, t * 4) : 0;
9772
+ return { cut, feather, seam, blur: t < 0.995 ? FUSE.blur * (1 - t) : 0 };
9773
+ }
9615
9774
  function clampZoom(r) {
9616
9775
  const w = Math.min(1, Math.max(1 / ZOOM_MAX, r.w));
9617
9776
  const u = Math.min(1 - w, Math.max(0, r.u));
@@ -9697,7 +9856,15 @@ function stripPeaks(diffs, o = {}) {
9697
9856
  });
9698
9857
  return out;
9699
9858
  }
9700
- var ALL_MODES, MODE_KEYS, modeKey = (mode) => MODE_KEYS[mode].toUpperCase(), VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), lastFrame = (duration, fps) => Math.max(0, Math.ceil(duration * fps - 0.0001) - 1), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, splitHeight = (w, media) => media.w > 0 && media.h > 0 ? w / 2 * (media.h / media.w) : w / 2 * (9 / 16), ZOOM_FIT, ZOOM_MAX = 16, panBy = (r, dx, dy, base) => clampZoom({
9859
+ var ALL_MODES, MODE_KEYS, modeKey = (mode) => MODE_KEYS[mode].toUpperCase(), VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), lastFrame = (duration, fps) => Math.max(0, Math.ceil(duration * fps - 0.0001) - 1), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, FUSE, labelFold = (fold, mark, blur = 3) => {
9860
+ const f = Math.min(1, Math.max(0, fold));
9861
+ const open = 1 - f;
9862
+ return {
9863
+ width: mark * open,
9864
+ opacity: Math.min(1, Math.max(0, (open - 0.2) / 0.8)),
9865
+ blur: blur * Math.min(1, Math.max(0, (f - 0.25) / 0.75))
9866
+ };
9867
+ }, splitHeight = (w, media) => media.w > 0 && media.h > 0 ? w / 2 * (media.h / media.w) : w / 2 * (9 / 16), ZOOM_FIT, ZOOM_MAX = 16, panBy = (r, dx, dy, base) => clampZoom({
9701
9868
  u: r.u - dx / Math.max(1, base.w) * r.w,
9702
9869
  v: r.v - dy / Math.max(1, base.h) * r.h,
9703
9870
  w: r.w
@@ -9720,6 +9887,7 @@ var init_compare_model = __esm(() => {
9720
9887
  MODE_KEYS = { side: "s", wipe: "w", difference: "d" };
9721
9888
  VIDEO = /\.(mp4|m4v|mov|webm|ogv|mkv)$/i;
9722
9889
  SEEK_IDLE = { seeking: false, pending: null };
9890
+ FUSE = { feather: 24, blur: 4, seamFade: 48 };
9723
9891
  ZOOM_FIT = { u: 0, v: 0, w: 1, h: 1 };
9724
9892
  });
9725
9893
 
@@ -10614,11 +10782,6 @@ fn pm(c: vec4f) -> vec4f { return vec4f(c.rgb * c.a, c.a); }
10614
10782
 
10615
10783
  fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
10616
10784
  switch mode {
10617
- case 1u: {
10618
- let s = max(u.wipe.y, 0.5);
10619
- let m = smoothstep(u.wipe.x - s, u.wipe.x + s, p.x);
10620
- return pm(vec4f(mix(a.rgb, b.rgb, m), mix(a.a, b.a, m)));
10621
- }
10622
10785
  case 2u: {
10623
10786
  let d = abs(a.rgb - b.rgb);
10624
10787
  let h = smoothstep(u.diff.x, u.diff.x + 0.06, max(d.r, max(d.g, d.b)) * u.diff.y) * min(a.a, b.a);
@@ -10627,8 +10790,10 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
10627
10790
  }
10628
10791
  default: {}
10629
10792
  }
10630
- // Side by side: each side in its own place, Before over After where they meet.
10631
- return vec4f(a.rgb * a.a + b.rgb * b.a * (1.0 - a.a), a.a + b.a * (1.0 - a.a));
10793
+ // Side by side, fusing and wiping: Before over After, cut at the seam with its feather.
10794
+ let s = max(u.wipe.y, 0.5);
10795
+ let ka = a.a * (1.0 - smoothstep(u.wipe.x - s, u.wipe.x + s, p.x));
10796
+ return vec4f(a.rgb * ka + b.rgb * b.a * (1.0 - ka), ka + b.a * (1.0 - ka));
10632
10797
  }
10633
10798
 
10634
10799
  @fragment fn fs(@builtin(position) q: vec4f) -> @location(0) vec4f {
@@ -11030,6 +11195,29 @@ var init_compare_strip = __esm(() => {
11030
11195
  DIFF = { w: 32, h: 18 };
11031
11196
  });
11032
11197
 
11198
+ // src/ui/player.ts
11199
+ function playGlyph(doc) {
11200
+ const element = doc.createElement("span");
11201
+ element.className = "knobs-player-glyph";
11202
+ element.setAttribute("aria-hidden", "true");
11203
+ let timer = 0;
11204
+ const win = doc.defaultView;
11205
+ return {
11206
+ element,
11207
+ flash(playing) {
11208
+ element.innerHTML = playing ? PLAY2 : PAUSE;
11209
+ element.setAttribute("data-flash", "");
11210
+ win.clearTimeout(timer);
11211
+ timer = win.setTimeout(() => element.removeAttribute("data-flash"), FLASH_MS);
11212
+ },
11213
+ dispose: () => win.clearTimeout(timer)
11214
+ };
11215
+ }
11216
+ var FLASH_MS = 450;
11217
+ var init_player = __esm(() => {
11218
+ init_timeline();
11219
+ });
11220
+
11033
11221
  // src/ui/compare.ts
11034
11222
  function compare(container, options) {
11035
11223
  const doc = container.ownerDocument;
@@ -11137,6 +11325,8 @@ function compare(container, options) {
11137
11325
  if (name.textContent !== text)
11138
11326
  name.textContent = text;
11139
11327
  };
11328
+ const seam = el("div", "knobs-cmp-seam");
11329
+ seam.setAttribute("aria-hidden", "true");
11140
11330
  const handle = el("div", "knobs-cmp-handle");
11141
11331
  handle.setAttribute("role", "slider");
11142
11332
  handle.setAttribute("aria-label", "Wipe");
@@ -11146,7 +11336,8 @@ function compare(container, options) {
11146
11336
  grip.setAttribute("aria-hidden", "true");
11147
11337
  handle.append(grip);
11148
11338
  const note = el("div", "knobs-cmp-note");
11149
- stage.append(canvas, split, labels[0], labels[1], handle, zoomPill, note);
11339
+ const glyph = playGlyph(doc);
11340
+ stage.append(canvas, split, seam, labels[0], labels[1], handle, zoomPill, note, glyph.element);
11150
11341
  root.append(bar, stage);
11151
11342
  const wantsStrip = options.strip === true;
11152
11343
  let tl = bound;
@@ -11161,7 +11352,7 @@ function compare(container, options) {
11161
11352
  root.append(host);
11162
11353
  tl = timeline(host, {
11163
11354
  ...look,
11164
- variant: "lane",
11355
+ variant: "line",
11165
11356
  duration: 1,
11166
11357
  fps: fps(),
11167
11358
  arrows: { step: 0.5, shift: 2 },
@@ -11182,12 +11373,13 @@ function compare(container, options) {
11182
11373
  let toMode = mode;
11183
11374
  const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
11184
11375
  let wipeCatching = false;
11185
- let gripY = null;
11376
+ const gripAt = new Spring(0.5, LAYOUT2.omega, LAYOUT2.zeta, 0.0001);
11186
11377
  let loupeHeld = false;
11187
11378
  let pointer = null;
11188
11379
  let drag = null;
11189
11380
  const touches = new Map;
11190
11381
  let pinch = null;
11382
+ let press = null;
11191
11383
  const zu = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
11192
11384
  const zv = new Spring(0, ZOOM.omega, ZOOM.zeta, 0.00001);
11193
11385
  const zw = new Spring(1, ZOOM.omega, ZOOM.zeta, 0.00001);
@@ -11474,6 +11666,8 @@ function compare(container, options) {
11474
11666
  for (const s of sides) {
11475
11667
  const st = s.el.style;
11476
11668
  st.clipPath = "";
11669
+ st.maskImage = "";
11670
+ st.setProperty("-webkit-mask-image", "");
11477
11671
  st.mixBlendMode = "";
11478
11672
  st.opacity = "";
11479
11673
  s.shown = null;
@@ -11700,43 +11894,65 @@ function compare(container, options) {
11700
11894
  viewSent = key;
11701
11895
  options.onView({ zoom: clampZoom(z), loop: l });
11702
11896
  };
11703
- const labelsIn = (m, r, wr, wx) => {
11704
- if (m === "wipe") {
11705
- const at = seamLabels(wx, wr.x, wr.x + wr.w, labelW[0], labelW[1], INSET);
11706
- return [at.before, at.after];
11707
- }
11708
- const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * lay.x;
11709
- return [
11710
- { x: r.before.x + INSET, opacity: 1 },
11711
- { x: ax, opacity: 1 }
11712
- ];
11897
+ const differenceWeight = () => {
11898
+ const f = Math.min(1, Math.max(0, fade.x));
11899
+ return (fromMode === "difference" ? 1 - f : 0) + (toMode === "difference" ? f : 0);
11713
11900
  };
11714
11901
  const placeGrip = (wr) => {
11715
- const gy = gripTop(gripY ?? wr.y + wr.h / 2, wr, GRIP.h) - wr.y;
11902
+ const gy = gripTop(wr.y + gripAt.x * wr.h, wr, GRIP.h) - wr.y;
11716
11903
  grip.style.transform = `translate3d(0,${gy.toFixed(2)}px,0)`;
11717
11904
  };
11905
+ let folded = 0;
11718
11906
  const draw = () => {
11719
11907
  dirty = false;
11908
+ if (!root.hasAttribute("data-ready") && sides.every((x) => x.shown || x.failed))
11909
+ root.setAttribute("data-ready", "");
11720
11910
  const r = layoutNow();
11721
11911
  const wr = wipeRect(r.L.overlay);
11722
- const wx = wipeX(wr, wipeLine.x);
11723
- const k = lay.x;
11724
- const a = labelsIn(fromMode, r, wr, wx);
11725
- const b = labelsIn(toMode, r, wr, wx);
11726
- const f = Math.min(1, Math.max(0, fade.x));
11912
+ const k = Math.min(1, Math.max(0, lay.x));
11913
+ const fu = fuseFrame(r.L, r.before, r.after, k, wipeLine.x);
11914
+ const dw = differenceWeight();
11915
+ const fusing = 1 - dw;
11916
+ folded = k * fusing;
11917
+ const fold = labelFold(folded, MARK.w);
11918
+ const w = labelW.map((x) => x - (MARK.w - fold.width) - MARK.gap * folded);
11919
+ const ride = seamLabels(fu.cut, r.before.x, r.after.x + r.after.w, w[0], w[1], INSET);
11920
+ const apart = [r.before.x + INSET, r.after.x + INSET];
11921
+ const corner = [r.before.x + INSET, r.after.x + r.after.w - labelW[1] - INSET];
11922
+ const rides = [ride.before, ride.after];
11727
11923
  const ys = [r.before.y + INSET, r.after.y + INSET];
11728
11924
  for (const i of [0, 1]) {
11729
- const x = a[i].x + (b[i].x - a[i].x) * f;
11730
- labels[i].style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
11731
- labels[i].style.opacity = (a[i].opacity + (b[i].opacity - a[i].opacity) * f).toFixed(3);
11925
+ const fused = apart[i] + (rides[i].x - apart[i]) * k;
11926
+ const x = fused + (corner[i] - fused) * dw;
11927
+ const opacity = (1 - k + k * rides[i].opacity) * fusing + dw;
11928
+ const l = labels[i];
11929
+ l.style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
11930
+ l.style.opacity = opacity.toFixed(3);
11931
+ const mark = l.firstElementChild;
11932
+ if (mark) {
11933
+ mark.style.width = `${fold.width.toFixed(2)}px`;
11934
+ mark.style.marginRight = `${(-MARK.gap * folded).toFixed(2)}px`;
11935
+ mark.style.opacity = fold.opacity.toFixed(3);
11936
+ mark.style.filter = fold.blur > 0.05 ? `blur(${fold.blur.toFixed(2)}px)` : "";
11937
+ }
11732
11938
  }
11733
11939
  const top = Math.min(r.L.side.before.y, r.L.side.after.y);
11734
11940
  split.style.transform = `translate(${r.L.side.split.toFixed(2)}px,${top.toFixed(2)}px)`;
11735
11941
  split.style.height = `${Math.max(0, r.L.side.after.h).toFixed(2)}px`;
11736
11942
  split.style.opacity = String(Math.max(0, 1 - k * 2));
11943
+ const seamOn = fu.seam * fusing;
11944
+ const blur = fu.blur > 0.01 ? `blur(${fu.blur.toFixed(2)}px)` : "none";
11945
+ seam.style.transform = `translate(${fu.cut.toFixed(2)}px,${wr.y.toFixed(2)}px) translateX(-50%)`;
11946
+ seam.style.width = `${(8 + 2 * fu.feather).toFixed(2)}px`;
11947
+ seam.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
11948
+ seam.style.opacity = (blur === "none" ? 0 : seamOn).toFixed(3);
11949
+ seam.style.backdropFilter = blur;
11950
+ seam.style.setProperty("-webkit-backdrop-filter", blur);
11737
11951
  handle.toggleAttribute("data-on", mode === "wipe");
11738
- handle.style.transform = `translate(${wx.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
11952
+ handle.style.transform = `translate(${fu.cut.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
11739
11953
  handle.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
11954
+ handle.style.opacity = seamOn.toFixed(3);
11955
+ handle.style.filter = blur;
11740
11956
  placeGrip(wr);
11741
11957
  handle.setAttribute("aria-valuenow", String(Math.round(wipe * 100)));
11742
11958
  handle.setAttribute("aria-valuetext", `${Math.round(wipe * 100)}% Before`);
@@ -11749,24 +11965,27 @@ function compare(container, options) {
11749
11965
  if (pending)
11750
11966
  return;
11751
11967
  if (renderer === "dom") {
11752
- const place = (n, rect, clip, cutAt) => {
11968
+ const cut = mode === "difference" ? null : fu;
11969
+ const place = (n, rect, clip, before) => {
11753
11970
  const st = n.style;
11754
11971
  st.transform = `translate(${rect.x.toFixed(2)}px,${rect.y.toFixed(2)}px)`;
11755
11972
  st.width = `${rect.w.toFixed(2)}px`;
11756
11973
  st.height = `${rect.h.toFixed(2)}px`;
11974
+ const edge = before && cut ? cut.cut + cut.feather : Number.POSITIVE_INFINITY;
11757
11975
  const t = Math.max(0, clip.y - rect.y);
11758
11976
  const l = Math.max(0, clip.x - rect.x);
11759
11977
  const b = Math.max(0, rect.y + rect.h - (clip.y + clip.h));
11760
- const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, cutAt));
11978
+ const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, edge));
11761
11979
  st.clipPath = t + l + b + right > 0.01 ? `inset(${t.toFixed(2)}px ${right.toFixed(2)}px ${b.toFixed(2)}px ${l.toFixed(2)}px)` : "";
11980
+ const mask = before && cut && cut.feather > 0.5 ? `linear-gradient(to right,#000 ${(cut.cut - cut.feather - rect.x).toFixed(2)}px,transparent ${(edge - rect.x).toFixed(2)}px)` : "";
11981
+ st.maskImage = mask;
11982
+ st.setProperty("-webkit-mask-image", mask);
11762
11983
  };
11763
- const cut = mode === "wipe" ? wx : Number.POSITIVE_INFINITY;
11764
11984
  for (const s of sides) {
11765
11985
  const rect = s.index === 0 ? r.zBefore : r.zAfter;
11766
11986
  const clip = s.index === 0 ? r.before : r.after;
11767
- const at = s.index === 0 ? cut : Number.POSITIVE_INFINITY;
11768
- place(s.el, rect, clip, at);
11769
- place(s.preview, rect, clip, at);
11987
+ place(s.el, rect, clip, s.index === 0);
11988
+ place(s.preview, rect, clip, s.index === 0);
11770
11989
  if (s.index !== 0)
11771
11990
  continue;
11772
11991
  for (const n of [s.el, s.preview])
@@ -11786,8 +12005,8 @@ function compare(container, options) {
11786
12005
  width: Math.max(1, Math.round(stageW * dpr)),
11787
12006
  height: Math.max(1, Math.round(stageH * dpr)),
11788
12007
  dpr,
11789
- wipeX: wx * dpr,
11790
- soft: 0.75 * dpr,
12008
+ wipeX: fu.cut * dpr,
12009
+ soft: Math.max(0.75, fu.feather) * dpr,
11791
12010
  from: MODE_INDEX[fromMode],
11792
12011
  to: MODE_INDEX[toMode],
11793
12012
  mix: Math.min(1, Math.max(0, fade.x)),
@@ -11814,6 +12033,7 @@ function compare(container, options) {
11814
12033
  zu.step(dt);
11815
12034
  zv.step(dt);
11816
12035
  zw.step(dt);
12036
+ gripAt.step(dt);
11817
12037
  const playing = isPlaying();
11818
12038
  if (playing) {
11819
12039
  const m = master();
@@ -11844,11 +12064,9 @@ function compare(container, options) {
11844
12064
  pushFrame(s);
11845
12065
  }
11846
12066
  }
11847
- const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching;
12067
+ const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching || !gripAt.resting;
11848
12068
  if (moving)
11849
12069
  dirty = true;
11850
- if (auto && !lay.resting)
11851
- fitHeight();
11852
12070
  if (dirty)
11853
12071
  draw();
11854
12072
  return playing || moving;
@@ -11874,13 +12092,7 @@ function compare(container, options) {
11874
12092
  zw.set(c.w);
11875
12093
  requestDraw();
11876
12094
  };
11877
- const autoHeight = (w) => {
11878
- const shape = sizeOf(sides[1]);
11879
- const side = splitHeight(w, shape);
11880
- const over = w * shape.h / shape.w;
11881
- const h = side + (over - side) * lay.x;
11882
- return Math.max(60, Math.min(win.innerHeight * 0.8, h));
11883
- };
12095
+ const autoHeight = (w) => Math.max(60, Math.min(win.innerHeight * 0.8, splitHeight(w, sizeOf(sides[1]))));
11884
12096
  const fitHeight = () => {
11885
12097
  const h = autoHeight(stageW);
11886
12098
  if (Math.abs(h - stageH) < 0.25)
@@ -11892,7 +12104,8 @@ function compare(container, options) {
11892
12104
  const readColors = () => {
11893
12105
  const c = tokenColor(win.getComputedStyle(root), "--_ku-cmp-diff", [0.49, 0.81, 0.61, 1]);
11894
12106
  accent = [c[0], c[1], c[2]];
11895
- labelW = [labels[0].offsetWidth, labels[1].offsetWidth];
12107
+ const unfold = MARK.w - labelFold(folded, MARK.w).width + MARK.gap * folded;
12108
+ labelW = [labels[0].offsetWidth + unfold, labels[1].offsetWidth + unfold];
11896
12109
  requestDraw();
11897
12110
  };
11898
12111
  const measure = () => {
@@ -11983,8 +12196,10 @@ function compare(container, options) {
11983
12196
  root.focus({ preventScroll: true });
11984
12197
  touches.set(e.pointerId, p);
11985
12198
  stage.setPointerCapture(e.pointerId);
12199
+ press = touches.size === 1 && mode !== "wipe" && master() ? p : null;
11986
12200
  if (touches.size >= 2) {
11987
12201
  e.preventDefault();
12202
+ press = null;
11988
12203
  return startPinch();
11989
12204
  }
11990
12205
  if (mode === "wipe") {
@@ -11994,7 +12209,7 @@ function compare(container, options) {
11994
12209
  if (inside && (grab.onLine || !zoomed())) {
11995
12210
  drag = { kind: "wipe", offset: grab.offset };
11996
12211
  root.dataset.dragging = "";
11997
- gripY = p.y;
12212
+ gripAt.snap(wr.h > 0 ? (p.y - wr.y) / wr.h : 0.5);
11998
12213
  e.preventDefault();
11999
12214
  if (!grab.onLine)
12000
12215
  setWipe(wipeAt(p.x, wr), true, true);
@@ -12012,6 +12227,8 @@ function compare(container, options) {
12012
12227
  const onMove = (e) => {
12013
12228
  const p = local(e);
12014
12229
  pointer = p;
12230
+ if (press && Math.hypot(p.x - press.x, p.y - press.y) > 4)
12231
+ press = null;
12015
12232
  if (touches.has(e.pointerId))
12016
12233
  touches.set(e.pointerId, p);
12017
12234
  if (pinch && touches.size >= 2) {
@@ -12025,14 +12242,12 @@ function compare(container, options) {
12025
12242
  pinch = { d, x: mid.x, y: mid.y };
12026
12243
  return handZoom(zoomAbout(panned, k, q.pu, q.pv));
12027
12244
  }
12028
- if (mode === "wipe" && drag?.kind !== "pan") {
12029
- gripY = p.y;
12030
- if (!drag)
12031
- placeGrip(wipeRect(layoutNow().L.overlay));
12032
- }
12033
- if (drag?.kind === "wipe")
12034
- setWipe(wipeAt(p.x, wipeRect(layoutNow().L.overlay), drag.offset));
12035
- else if (drag?.kind === "pan") {
12245
+ if (drag?.kind === "wipe") {
12246
+ const wr = wipeRect(layoutNow().L.overlay);
12247
+ gripAt.snap(wr.h > 0 ? (p.y - wr.y) / wr.h : 0.5);
12248
+ setWipe(wipeAt(p.x, wr, drag.offset));
12249
+ placeGrip(wr);
12250
+ } else if (drag?.kind === "pan") {
12036
12251
  const dx = p.x - drag.x;
12037
12252
  const dy = p.y - drag.y;
12038
12253
  drag.x = p.x;
@@ -12043,11 +12258,23 @@ function compare(container, options) {
12043
12258
  drawNow();
12044
12259
  };
12045
12260
  const endDrag = (e) => {
12261
+ if (press && e.type === "pointerup") {
12262
+ api.toggle();
12263
+ glyph.flash(isPlaying());
12264
+ }
12265
+ press = null;
12046
12266
  touches.delete(e.pointerId);
12047
12267
  if (touches.size < 2)
12048
12268
  pinch = null;
12049
12269
  if (!drag)
12050
12270
  return;
12271
+ if (drag.kind === "wipe") {
12272
+ if (reducedMotion(win))
12273
+ gripAt.snap(0.5);
12274
+ else
12275
+ gripAt.set(0.5);
12276
+ requestDraw();
12277
+ }
12051
12278
  drag = null;
12052
12279
  delete root.dataset.dragging;
12053
12280
  delete root.dataset.panning;
@@ -12197,6 +12424,7 @@ function compare(container, options) {
12197
12424
  old.el.replaceWith(s.el);
12198
12425
  old.preview.replaceWith(s.preview);
12199
12426
  sides[index] = s;
12427
+ root.toggleAttribute("data-video", sides.some((x) => x.kind === "video"));
12200
12428
  note.textContent = "";
12201
12429
  setLabel(index, labelOf(index));
12202
12430
  if (near)
@@ -12211,6 +12439,7 @@ function compare(container, options) {
12211
12439
  stage.insertBefore(sides[0].preview, labels[0]);
12212
12440
  setLabel(0, labelOf(0));
12213
12441
  setLabel(1, labelOf(1));
12442
+ root.toggleAttribute("data-video", sides.some((s) => s.kind === "video"));
12214
12443
  loupeBtn.setAttribute("aria-pressed", String(loupeOn));
12215
12444
  loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
12216
12445
  const modePill = modeList.length > 1 ? selectionPill(modes, {
@@ -12411,6 +12640,7 @@ function compare(container, options) {
12411
12640
  disposed = true;
12412
12641
  loop.stop();
12413
12642
  modePill?.dispose();
12643
+ glyph.dispose();
12414
12644
  win.clearTimeout(releaseTimer);
12415
12645
  ro.disconnect();
12416
12646
  nearIo?.disconnect();
@@ -12440,7 +12670,7 @@ function compare(container, options) {
12440
12670
  wake();
12441
12671
  return api;
12442
12672
  }
12443
- var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
12673
+ var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, MARK, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
12444
12674
  var init_compare = __esm(() => {
12445
12675
  init_motion();
12446
12676
  init_page_hints();
@@ -12451,15 +12681,17 @@ var init_compare = __esm(() => {
12451
12681
  init_icons();
12452
12682
  init_look();
12453
12683
  init_rgba();
12684
+ init_player();
12454
12685
  init_selection_pill();
12455
12686
  init_timeline();
12456
12687
  init_timeline_model();
12457
- LAYOUT2 = springMotion({ duration: 0.35, bounce: 0 });
12688
+ LAYOUT2 = springMotion(SPRINGS.default);
12458
12689
  FADE6 = springMotion(SPRINGS.snappy);
12459
12690
  FADE_REDUCED = springMotion(FADE);
12460
12691
  ZOOM = springMotion({ duration: 0.35, bounce: 0 });
12461
12692
  WIPE_CATCH = springMotion(CATCH_UP);
12462
12693
  GRIP = { w: 16, h: 28 };
12694
+ MARK = { w: 12, gap: 4 };
12463
12695
  LOUPE = { r: 88, zoom: 4 };
12464
12696
  MODE_LABEL = {
12465
12697
  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)