@kitlangton/knobs 0.4.0-next.2 → 0.4.0-next.3

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
@@ -570,12 +570,20 @@ var compareCss = `
570
570
  display:flex;flex-direction:column;gap:6px;width:100%;height:100%;min-height:160px;outline:none;user-select:none;-webkit-user-select:none;
571
571
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
572
572
  :where(.knobs-ui) :is(.knobs-cmp-mode,.knobs-cmp-toggle){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;margin:0;padding:0;outline:none;box-shadow:none}
573
- .knobs-cmp-bar{flex:none;display:flex;align-items:center;gap:2px;height:24px;padding:0 2px}
574
- .knobs-cmp-modes{display:flex;align-items:center;gap:2px}
575
- .knobs-ui .knobs-cmp-mode,.knobs-ui .knobs-cmp-toggle{height:22px;padding:0 7px;border-radius:max(0px,calc(var(--_k-radius) - 2px));color:var(--_k-fg-3);cursor:pointer;white-space:nowrap;
576
- transition:color .15s,background .15s}
577
- .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible),.knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash)}
578
- .knobs-ui .knobs-cmp-mode[aria-checked="true"],.knobs-ui .knobs-cmp-toggle[aria-pressed="true"]{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
573
+ .knobs-cmp-bar{flex:none;display:flex;align-items:center;gap:var(--_k-gap);height:var(--_k-control-h)}
574
+ /* The modes: one fill, the selection pill on the one picked; a mode isn't brighter for being
575
+ picked: the pill's bright copy is. */
576
+ .knobs-cmp-modes{position:relative;display:inline-flex;height:var(--_k-control-h);border-radius:var(--_k-radius);background:var(--_k-fill)}
577
+ .knobs-cmp-modes[hidden]{display:none}
578
+ .knobs-ui .knobs-cmp-mode{display:inline-flex;align-items:center;height:100%;padding:0 8px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:pointer;white-space:nowrap;
579
+ transition:color calc(var(--_k-hover-out) * 1ms)}
580
+ .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible){color:var(--_k-fg)}
581
+ /* Toggles: icon buttons, the accent's wash when on. */
582
+ .knobs-ui .knobs-cmp-toggle{display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:var(--_k-radius);color:var(--_k-fg-3);cursor:pointer;
583
+ transition:color calc(var(--_k-hover-out) * 1ms),background calc(var(--_k-hover-out) * 1ms)}
584
+ .knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash)}
585
+ .knobs-ui .knobs-cmp-toggle[aria-pressed="true"]{color:var(--_k-accent-fg);background:var(--_k-accent-soft)}
586
+ .knobs-cmp-toggle svg{display:block}
579
587
  .knobs-cmp-fill{flex:1}
580
588
  .knobs-cmp-toggle[hidden]{display:none}
581
589
  /* One surface: on a card (a walkthrough's step, a split's pane, anything .knobs-surface) or marked
@@ -590,7 +598,8 @@ var compareCss = `
590
598
  /* Unzoomed, a finger still scrolls the page up and down; zoomed, the stage takes every gesture. */
591
599
  .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:grab;touch-action:none}
592
600
  .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:grabbing!important}
593
- .knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:ew-resize!important}
601
+ /* Wiping, the grip stands in for the cursor. */
602
+ .knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:none!important}
594
603
  .knobs-cmp-sizer{position:absolute;left:0;right:0;top:0;height:0;visibility:hidden;pointer-events:none}
595
604
  .knobs-cmp-canvas{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
596
605
  .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}
@@ -598,18 +607,21 @@ var compareCss = `
598
607
  /* Glass: what's behind shows through, blurred, on the knobs tokens. */
599
608
  .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);
600
609
  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)))}
601
- .knobs-cmp-label{position:absolute;left:0;top:0;padding:1px 7px;border-radius:999px;font-size:var(--_k-text-meta);line-height:16px;color:var(--_k-fg-2);white-space:nowrap;pointer-events:none;will-change:transform;transition:color .15s}
602
- .knobs-cmp-label[data-lit]{color:var(--_k-fg)}
603
- .knobs-cmp-label[data-dim]{color:var(--_k-fg-3)}
604
- /* The wipe: a hairline down the line with a grip at its middle; a wider strip takes the hand and
605
- the keys (a slider). It fades with the mode. */
610
+ .knobs-cmp-label{position:absolute;left:0;top:0;padding:1px 7px;border-radius:999px;font-size:var(--_k-text-meta);line-height:16px;color:var(--_k-fg-2);white-space:nowrap;pointer-events:none;will-change:transform;transition:color calc(var(--_k-hover-out) * 1ms)}
611
+ /* The wipe: a hairline down the line with a raised grip at the pointer's height; a wider strip
612
+ takes the hand and the keys (a slider). It fades with the mode; wiping, line and grip brighten. */
606
613
  .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)}
607
614
  .knobs-cmp-handle[data-on]{opacity:1;pointer-events:auto;transition:opacity .22s cubic-bezier(.2,.8,.2,1)}
608
- .knobs-cmp-handle::before{content:"";position:absolute;top:0;bottom:0;left:11.5px;width:1px;background:var(--_ku-cmp-line);box-shadow:0 0 0 .5px rgba(0,0,0,.28),0 0 6px rgba(0,0,0,.25)}
609
- .knobs-cmp-handle::after{content:"";position:absolute;top:50%;left:9px;width:6px;height:28px;margin-top:-14px;border-radius:3px;background:var(--_ku-cmp-line);
610
- box-shadow:0 1px 3px rgba(0,0,0,.35),0 0 0 .5px rgba(0,0,0,.2);transition:scale .18s var(--_ku-ease),box-shadow .15s}
611
- .knobs-compare[data-dragging] .knobs-cmp-handle::after{scale:1.12}
612
- .knobs-cmp-handle:focus-visible::after,.knobs-compare[data-keyboard] .knobs-cmp-handle:focus::after{box-shadow:0 1px 3px rgba(0,0,0,.35),0 0 0 3px color-mix(in srgb,var(--_k-accent) 40%,transparent)}
615
+ .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)}
616
+ .knobs-compare[data-dragging] .knobs-cmp-handle::before{background:var(--_ku-cmp-line)}
617
+ .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);
618
+ box-shadow:var(--_k-shadow-overlay),inset 0 0 0 1px var(--_k-border),inset 0 1px 0 var(--_k-rim);will-change:transform;transition:box-shadow calc(var(--_k-hover-out) * 1ms)}
619
+ .knobs-cmp-grip::before,.knobs-cmp-grip::after{content:"";position:absolute;top:10px;width:1px;height:8px;background:var(--_k-fg-3);transition:background calc(var(--_k-hover-out) * 1ms)}
620
+ .knobs-cmp-grip::before{left:6px}
621
+ .knobs-cmp-grip::after{left:9px}
622
+ .knobs-compare[data-dragging] .knobs-cmp-grip{box-shadow:var(--_k-shadow-overlay),inset 0 0 0 1px var(--_k-border-strong),inset 0 1px 0 var(--_k-rim)}
623
+ .knobs-compare[data-dragging] .knobs-cmp-grip::before,.knobs-compare[data-dragging] .knobs-cmp-grip::after{background:var(--_k-fg)}
624
+ .knobs-cmp-handle:focus-visible .knobs-cmp-grip,.knobs-compare[data-keyboard] .knobs-cmp-handle:focus .knobs-cmp-grip{box-shadow:var(--_k-shadow-overlay),0 0 0 2px var(--_k-accent)}
613
625
  /* The split's hairline, down the middle side by side. */
614
626
  .knobs-cmp-split{position:absolute;left:0;top:0;width:1px;height:0;margin-left:-.5px;background:color-mix(in srgb,var(--_k-bg) 72%,transparent);pointer-events:none;will-change:transform,opacity}
615
627
  /* Zoomed: how far, and a way back, in the stage's corner; it fades in and out, nothing shifts. */
@@ -618,12 +630,12 @@ var compareCss = `
618
630
  opacity:0;scale:.96;pointer-events:none;transition:opacity .16s cubic-bezier(.4,0,1,1),scale .16s cubic-bezier(.4,0,1,1)}
619
631
  .knobs-cmp-zoom[data-on]{opacity:1;scale:1;pointer-events:auto;transition:opacity .2s cubic-bezier(.2,.8,.2,1),scale .2s cubic-bezier(.2,.8,.2,1)}
620
632
  :where(.knobs-ui) .knobs-cmp-zoom-reset{all:revert;box-sizing:border-box;font:inherit;letter-spacing:inherit;margin:0;border:0;outline:none;box-shadow:none;
621
- height:20px;padding:0 8px;border-radius:999px;background:transparent;color:var(--_k-fg-2);cursor:pointer;transition:color .15s,background .15s}
633
+ display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border-radius:999px;background:transparent;color:var(--_k-fg-2);cursor:pointer;transition:color .15s,background .15s}
622
634
  :where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){color:var(--_k-fg);background:color-mix(in srgb,var(--_k-fg) 12%,transparent)}
623
635
  .knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);font-size:var(--_k-text-meta);pointer-events:none}
624
636
  .knobs-cmp-note:empty{display:none}
625
637
  .knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
626
- .knobs-cmp-timeline-host{flex:none;height:56px;min-height:0}
638
+ .knobs-cmp-timeline-host{flex:none;min-height:0}
627
639
  /* The frame strip: Before over After per column, a difference bar under them, a dot where it peaks. */
628
640
  .knobs-ui.knobs-cmp-strip{--_ku-cmp-diff:var(--k-compare-diff,var(--_k-ok));position:relative;width:100%;height:100%;min-height:40px;overflow:hidden;cursor:col-resize;touch-action:none;
629
641
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg)}
@@ -766,7 +778,7 @@ var init_picker_css = __esm(() => {
766
778
  .knobs-picker-suggested{flex:none;color:var(--_k-fg-3);font-size:var(--_k-text-meta)}
767
779
  .knobs-picker-check{flex:none;display:flex;width:14px;height:14px;color:var(--_k-accent);opacity:0;scale:.6;filter:blur(2px);
768
780
  transition:opacity .11s ease-in,scale .11s ease-in,filter .11s ease-in}
769
- .knobs-picker-check svg{display:block;width:14px;height:14px}
781
+ .knobs-picker-check svg{display:block;width:12px;height:12px}
770
782
  .knobs-picker-option[data-chosen] .knobs-picker-check{opacity:1;scale:1;filter:none;transition:opacity .15s ease-out,scale var(--kp-snappy-ms) var(--kp-snappy),filter .15s ease-out}
771
783
  .knobs-picker-body{position:relative;overflow:visible}
772
784
  .knobs-picker-body[data-moving]{overflow:hidden}
@@ -870,6 +882,15 @@ var readoutCss = `
870
882
  @media(prefers-reduced-motion:reduce){.knobs-rd :where(*){transition:none!important}}
871
883
  `;
872
884
 
885
+ // src/ui/selection-pill-css.ts
886
+ var selectionPillCss = `
887
+ .knobs-pick-pill,.knobs-pick-copy{position:absolute;left:0;top:0;width:0;height:0;pointer-events:none;transition:opacity .1s linear}
888
+ .knobs-pick-pill{background:var(--_k-fill-selected);will-change:transform}
889
+ .knobs-pick-copy{overflow:hidden;will-change:transform}
890
+ .knobs-pick-copy-rows{position:absolute;left:0;top:0;width:0;height:0}
891
+ :is(.knobs-pick-pill,.knobs-pick-copy)[data-off]{opacity:0}
892
+ `;
893
+
873
894
  // src/tokens/color.ts
874
895
  function linearToOklch([r, g, b, a]) {
875
896
  const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
@@ -2718,11 +2739,9 @@ var css2 = `
2718
2739
  .knobs-tree:focus .knobs-tree-row[data-active][aria-selected="true"]::before{background:var(--_ku-select-focus)}
2719
2740
  .knobs-tree-row[data-drop="into"]::before{opacity:1;background:color-mix(in srgb,var(--_k-accent) 30%,transparent)}
2720
2741
  .knobs-tree-row[data-dragged]{opacity:.38}
2721
- /* glide: one highlight under the selected row that glides between rows (the row's own stays off). */
2722
- .knobs-tree-cursor{position:absolute;left:var(--_ku-row-x);right:var(--_ku-row-x);top:0;height:var(--_k-row);border-radius:var(--_ku-row-radius);background:var(--_ku-select);
2723
- opacity:0;pointer-events:none;will-change:transform;transition:opacity .15s ease-out,background .12s}
2724
- .knobs-tree-cursor[data-on]{opacity:1}
2725
- .knobs-tree:focus .knobs-tree-cursor{background:var(--_ku-select-focus)}
2742
+ /* glide: the selection pill on the selected row, in the selection's colour (the row's own stays off). */
2743
+ .knobs-tree>.knobs-pick-pill{background:var(--_ku-select)}
2744
+ .knobs-tree:focus>.knobs-pick-pill{background:var(--_ku-select-focus)}
2726
2745
  .knobs-tree[data-glide] .knobs-tree-row[aria-selected="true"]::before{opacity:0!important}
2727
2746
  .knobs-tree-row>*{position:relative}
2728
2747
  .knobs-tree-caret{flex:none;display:flex;align-items:center;justify-content:center;width:16px;height:100%;color:var(--_k-fg-3);transition:transform var(--_ku-ease-ms) var(--_ku-ease)}
@@ -2754,22 +2773,20 @@ var css2 = `
2754
2773
  box-shadow:0 0 0 1px var(--_k-border-strong),0 6px 18px rgba(0,0,0,calc(.35 * var(--_k-shadow-k,1)));pointer-events:none;white-space:nowrap;font:var(--_k-weight) var(--_k-font-size)/1.6 var(--_k-font)}
2755
2774
  .knobs-tree-empty{padding:10px var(--_k-inset);color:var(--_k-fg-3)}
2756
2775
 
2757
- /* Timeline: a transport (play, the time) beside a track: the ruler's canvas over a lane of
2758
- markers, ranges as soft bands, and the playhead, a hairline in the accent with a cap. */
2776
+ /* Timeline: play beside a track: the ruler's canvas over a lane of markers, ranges as soft bands,
2777
+ and the playhead, a hairline in the accent with a cap. No clock: the time shows under the
2778
+ pointer on the track (the tag), in seconds, while hovering or scrubbing. */
2759
2779
  :where(.knobs-timeline,.knobs-timeline *){box-sizing:border-box}
2760
2780
  .knobs-ui.knobs-timeline{display:flex;align-items:stretch;gap:10px;width:100%;height:100%;min-height:52px;padding:6px var(--_k-inset) 8px 8px;outline:none;user-select:none;-webkit-user-select:none;
2761
2781
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
2762
2782
  :where(.knobs-ui) :is(.knobs-tl-play,.knobs-tl-edit){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;margin:0;padding:0;outline:none;box-shadow:none}
2763
- .knobs-tl-transport{flex:none;display:flex;align-items:center;gap:8px;min-width:96px}
2783
+ .knobs-tl-transport{flex:none;display:flex;align-items:center}
2764
2784
  .knobs-ui .knobs-tl-play{flex:none;display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);min-width:22px;min-height:22px;border-radius:calc(var(--_k-radius) * 999);
2765
2785
  background:var(--_k-fill-selected);color:var(--_k-fg);cursor:pointer;transition:background .15s,transform .15s var(--_ku-ease)}
2766
2786
  .knobs-ui .knobs-tl-play:hover{background:color-mix(in srgb,var(--_k-fg) 16%,transparent)}
2767
2787
  .knobs-ui .knobs-tl-play:active{transform:scale(.94)}
2768
2788
  .knobs-tl-play svg{width:12px;height:12px}
2769
2789
  .knobs-timeline[data-playing] .knobs-tl-play{background:var(--_k-accent);color:var(--_k-bg)}
2770
- .knobs-tl-time{display:flex;flex-direction:column;line-height:1.2;font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}
2771
- .knobs-tl-now{font:var(--_k-weight) var(--_k-value-size)/1.2 var(--_k-value-font);font-variant-numeric:tabular-nums;font-feature-settings:"tnum";color:var(--_k-fg);white-space:nowrap}
2772
- .knobs-tl-sub{font-size:var(--_k-text-meta);color:var(--_k-fg-3);white-space:nowrap}
2773
2790
  .knobs-tl-track{position:relative;flex:1;min-width:0;overflow:hidden;border-radius:var(--_k-radius);background:var(--_k-fill);cursor:col-resize;touch-action:none;
2774
2791
  transition:background calc(var(--_k-hover-out) * 1ms)}
2775
2792
  .knobs-timeline:focus-visible .knobs-tl-track,.knobs-timeline[data-keyboard]:focus .knobs-tl-track{background:color-mix(in srgb,var(--_k-fill-selected) 80%,transparent)}
@@ -2793,7 +2810,8 @@ var css2 = `
2793
2810
  .knobs-tl-pin{position:absolute;top:13px;left:-5px;width:10px;height:10px;pointer-events:auto;cursor:grab}
2794
2811
  .knobs-tl-pin::after{content:"";position:absolute;inset:1.5px;border-radius:min(2px,var(--_k-radius));background:var(--knobs-tl-c);rotate:45deg;transition:background .15s,scale .2s var(--_ku-ease)}
2795
2812
  .knobs-tl-mlabel{position:absolute;top:26px;left:3px;max-width:140px;padding:0 6px;border-radius:max(0px,calc(var(--_k-radius) - 3px));background:var(--_k-fill-selected);color:var(--_k-fg);
2796
- font-size:var(--_k-text-meta);line-height:17px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:auto;cursor:grab;transition:background .15s,color .15s}
2813
+ font-size:var(--_k-text-meta);line-height:17px;white-space:nowrap;pointer-events:auto;cursor:grab;transition:background .15s,color .15s}
2814
+ .knobs-tl-mtext{display:block;overflow:hidden;white-space:nowrap}
2797
2815
  .knobs-tl-marker[data-selected]{--knobs-tl-c:var(--_k-accent)}
2798
2816
  .knobs-tl-marker[data-selected] .knobs-tl-mlabel{background:color-mix(in srgb,var(--_k-accent) 24%,var(--_k-fill-selected))}
2799
2817
  .knobs-tl-marker[data-selected] .knobs-tl-pin::after{scale:1.2}
@@ -2814,7 +2832,22 @@ var css2 = `
2814
2832
  .knobs-timeline[data-scrubbing] .knobs-tl-hover{opacity:0}
2815
2833
  @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
2816
2834
  @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
2817
- `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
2835
+ .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}
2836
+ .knobs-tl-tag[data-on]{opacity:1}
2837
+ /* The lane (variant: "lane"): one slim track of ticks under a compare or a video; markers are
2838
+ faint chapter ticks, the current one in text colour; nothing to edit. */
2839
+ .knobs-ui.knobs-timeline[data-variant="lane"]{height:auto;min-height:0;padding:0;gap:var(--_k-gap)}
2840
+ .knobs-timeline[data-variant="lane"] .knobs-tl-track{height:var(--_k-control-h)}
2841
+ .knobs-timeline[data-variant="lane"] .knobs-tl-ruler{height:100%}
2842
+ .knobs-timeline[data-variant="lane"] .knobs-tl-lane{top:0}
2843
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker{top:0}
2844
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker::before{top:0;bottom:50%;left:-.75px;width:1.5px;background:var(--_k-fg-3)}
2845
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker[data-current]::before{background:var(--_k-fg)}
2846
+ .knobs-timeline[data-variant="lane"] :is(.knobs-tl-pin,.knobs-tl-mlabel,.knobs-tl-rlabel){display:none}
2847
+ .knobs-timeline[data-variant="lane"] .knobs-tl-head::before{top:0}
2848
+ .knobs-timeline[data-variant="lane"] .knobs-tl-head::after{content:none}
2849
+ .knobs-timeline[data-variant="lane"] .knobs-tl-tag{top:2px;bottom:auto}
2850
+ `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + selectionPillCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
2818
2851
  s.omega = m.omega;
2819
2852
  s.zeta = m.zeta;
2820
2853
  return s;
@@ -2972,33 +3005,29 @@ var init_diff_css = __esm(() => {
2972
3005
  .knobs-diff-fill{flex:1}
2973
3006
  .knobs-diff-nav{flex:none;display:inline-flex;align-items:center;gap:1px;color:var(--_k-fg-3);font-size:var(--_k-text-meta);font-variant-numeric:tabular-nums}
2974
3007
  .knobs-diff-nav>span{min-width:3.6em;text-align:center}
2975
- :where(.knobs-ui) :is(.knobs-diff-btn,.knobs-diff-seg>button,.knobs-diff-viewed,.knobs-diff-filter>input,.knobs-diff-filter>button){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;border-radius:0;
3008
+ :where(.knobs-ui) :is(.knobs-diff-btn,.knobs-diff-choice,.knobs-diff-viewed,.knobs-diff-filter>input,.knobs-diff-filter>button){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;border-radius:0;
2976
3009
  margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
2977
- .knobs-ui .knobs-diff-btn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:calc(var(--_k-radius) * 999);color:var(--_k-fg-3);cursor:pointer;
3010
+ .knobs-ui .knobs-diff-btn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:calc(var(--_k-radius) * 999);color:var(--_k-fg-3);cursor:pointer;
2978
3011
  transition:background .15s,color .15s,opacity .15s}
2979
3012
  .knobs-ui .knobs-diff-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
2980
3013
  .knobs-ui .knobs-diff-btn[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
2981
3014
  .knobs-ui .knobs-diff-btn:disabled{opacity:.35;cursor:default;background:transparent}
2982
- .knobs-diff-btn svg{width:13px;height:13px}
3015
+ .knobs-diff-btn svg{width:16px;height:16px}
2983
3016
  /* Viewed: a check with its word, as on GitHub; the path dims once it's ticked. */
2984
3017
  .knobs-ui .knobs-diff-viewed{flex:none;display:inline-flex;align-items:center;gap:5px;height:var(--_k-control-h);padding:0 8px 0 6px;border-radius:var(--kd-ctl);color:var(--_k-fg-2);
2985
3018
  font-size:var(--_k-text-meta);font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:background .15s,color .15s}
2986
3019
  .knobs-ui .knobs-diff-viewed:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
2987
3020
  .knobs-ui .knobs-diff-viewed[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
2988
- .knobs-diff-viewed svg{width:13px;height:13px}
3021
+ .knobs-diff-viewed svg{width:16px;height:16px}
2989
3022
  .knobs-diff-viewed[aria-pressed="true"] svg{color:var(--kd-add)}
2990
3023
  .knobs-diff-path{transition:opacity .2s}
2991
3024
  .knobs-diff[data-viewed]>.knobs-diff-head .knobs-diff-path,.knobs-diff-part[data-viewed] .knobs-diff-path{opacity:.6}
2992
- /* Split | Unified: one pill that glides between the two, raised as a panel's segments are (a
2993
- lighter bg in dark, near white in light). */
2994
- .knobs-diff-seg{position:relative;flex:none;display:grid;grid-template-columns:1fr 1fr;height:var(--_k-control-h);padding:2px;border-radius:var(--kd-ctl);background:var(--_k-segmented-track)}
2995
- .knobs-ui .knobs-diff-seg>button{position:relative;z-index:1;padding:0 9px;border-radius:max(0px,calc(var(--kd-ctl) - 2px));color:var(--_k-fg-2);font-size:var(--_k-text-meta);
2996
- font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:color .15s}
2997
- .knobs-ui .knobs-diff-seg>button:is(:hover,:focus-visible),.knobs-ui .knobs-diff-seg>button[aria-checked="true"]{color:var(--_k-fg)}
2998
- .knobs-diff-pill{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:max(0px,calc(var(--kd-ctl) - 2px));background:color-mix(in srgb,var(--_k-accent) 18%,var(--_k-segmented-active));
2999
- box-shadow:0 1px 2px rgba(0,0,0,calc(.16 * var(--k-lift,1)));transition:transform var(--_ku-ease-ms) var(--_ku-ease)}
3000
- .knobs-diff-seg[data-value="unified"]>.knobs-diff-pill{transform:translateX(100%)}
3001
- .knobs-diff[data-frame="flat"] .knobs-diff-pill{box-shadow:none}
3025
+ /* Split | Unified: the selection pill on the one picked, its bright copy the picked one's text; a
3026
+ choice isn't brighter for being picked. Choices are styled by class, as the copy holds them too. */
3027
+ .knobs-diff-seg{position:relative;flex:none;display:grid;grid-template-columns:1fr 1fr;height:var(--_k-control-h);padding:2px;border-radius:var(--kd-ctl);background:var(--_k-fill)}
3028
+ .knobs-ui .knobs-diff-choice{padding:0 9px;border-radius:max(0px,calc(var(--kd-ctl) - 2px));color:var(--_k-fg-2);font-size:var(--_k-text-meta);
3029
+ font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:color calc(var(--_k-hover-out) * 1ms)}
3030
+ .knobs-ui .knobs-diff-choice:is(:hover,:focus-visible){color:var(--_k-fg)}
3002
3031
  /* The diff: one layer per file or layout on show, the old fading under the new. */
3003
3032
  .knobs-diff-body{position:relative;flex:1;min-height:0}
3004
3033
  .knobs-diff-probe{position:absolute;left:0;top:0;width:0;height:var(--kd-line);visibility:hidden;pointer-events:none}
@@ -3093,7 +3122,7 @@ var init_diff_css = __esm(() => {
3093
3122
  .knobs-ui .knobs-diff-filter>input::placeholder{color:var(--_k-fg-3)}
3094
3123
  .knobs-ui .knobs-diff-filter>button{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:999px;color:var(--_k-fg-3);cursor:pointer;transition:background .12s,color .12s,opacity .12s}
3095
3124
  .knobs-ui .knobs-diff-filter>button:hover{background:var(--_ku-wash-hi);color:var(--_k-fg)}
3096
- .knobs-diff-filter>button svg{width:10px;height:10px}
3125
+ .knobs-diff-filter>button svg{width:12px;height:12px}
3097
3126
  .knobs-diff-list{position:relative;flex:1;min-height:0}
3098
3127
  .knobs-diff-files .knobs-diff-counts{font-size:var(--_k-text-meta);align-items:center}
3099
3128
  .knobs-diff-files .knobs-diff-counts{gap:5px}
@@ -3860,6 +3889,283 @@ var init_rgba = __esm(() => {
3860
3889
  cache = new Map;
3861
3890
  });
3862
3891
 
3892
+ // src/ui/icons.ts
3893
+ function icon(name, options = {}) {
3894
+ const size = options.small ? 12 : 16;
3895
+ return `<svg class="knobs-icon" viewBox="0 0 256 256" width="${size}" height="${size}" fill="currentColor" aria-hidden="true" focusable="false"><path d="${PATHS[name]}"/></svg>`;
3896
+ }
3897
+ var PATHS, ICON_NAMES;
3898
+ var init_icons = __esm(() => {
3899
+ PATHS = {
3900
+ arrowLeft: "M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z",
3901
+ arrowUpRight: "M200,64V168a8,8,0,0,1-16,0V83.31L69.66,197.66a8,8,0,0,1-11.32-11.32L172.69,72H88a8,8,0,0,1,0-16H192A8,8,0,0,1,200,64Z",
3902
+ arrowSquareOut: "M224,104a8,8,0,0,1-16,0V59.32l-66.33,66.34a8,8,0,0,1-11.32-11.32L196.68,48H152a8,8,0,0,1,0-16h64a8,8,0,0,1,8,8Zm-40,24a8,8,0,0,0-8,8v72H48V80h72a8,8,0,0,0,0-16H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V136A8,8,0,0,0,184,128Z",
3903
+ arrowCounterClockwise: "M224,128a96,96,0,0,1-94.71,96H128A95.38,95.38,0,0,1,62.1,197.8a8,8,0,0,1,11-11.63A80,80,0,1,0,71.43,71.39a3.07,3.07,0,0,1-.26.25L44.59,96H72a8,8,0,0,1,0,16H24a8,8,0,0,1-8-8V56a8,8,0,0,1,16,0V85.8L60.25,60A96,96,0,0,1,224,128Z",
3904
+ arrowUDownLeft: "M232,112a64.07,64.07,0,0,1-64,64H51.31l34.35,34.34a8,8,0,0,1-11.32,11.32l-48-48a8,8,0,0,1,0-11.32l48-48a8,8,0,0,1,11.32,11.32L51.31,160H168a48,48,0,0,0,0-96H80a8,8,0,0,1,0-16h88A64.07,64.07,0,0,1,232,112Z",
3905
+ arrowsHorizontal: "M136,40V216a8,8,0,0,1-16,0V40a8,8,0,0,1,16,0ZM96,120H35.31l18.35-18.34A8,8,0,0,0,42.34,90.34l-32,32a8,8,0,0,0,0,11.32l32,32a8,8,0,0,0,11.32-11.32L35.31,136H96a8,8,0,0,0,0-16Zm149.66,2.34-32-32a8,8,0,0,0-11.32,11.32L220.69,120H160a8,8,0,0,0,0,16h60.69l-18.35,18.34a8,8,0,0,0,11.32,11.32l32-32A8,8,0,0,0,245.66,122.34Z",
3906
+ caretDown: "M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z",
3907
+ caretLeft: "M165.66,202.34a8,8,0,0,1-11.32,11.32l-80-80a8,8,0,0,1,0-11.32l80-80a8,8,0,0,1,11.32,11.32L91.31,128Z",
3908
+ caretRight: "M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z",
3909
+ caretUp: "M213.66,165.66a8,8,0,0,1-11.32,0L128,91.31,53.66,165.66a8,8,0,0,1-11.32-11.32l80-80a8,8,0,0,1,11.32,0l80,80A8,8,0,0,1,213.66,165.66Z",
3910
+ check: "M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z",
3911
+ checkCircle: "M173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z",
3912
+ circle: "M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Z",
3913
+ copy: "M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z",
3914
+ cornersIn: "M152,96V48a8,8,0,0,1,16,0V88h40a8,8,0,0,1,0,16H160A8,8,0,0,1,152,96ZM96,152H48a8,8,0,0,0,0,16H88v40a8,8,0,0,0,16,0V160A8,8,0,0,0,96,152Zm112,0H160a8,8,0,0,0-8,8v48a8,8,0,0,0,16,0V168h40a8,8,0,0,0,0-16ZM96,40a8,8,0,0,0-8,8V88H48a8,8,0,0,0,0,16H96a8,8,0,0,0,8-8V48A8,8,0,0,0,96,40Z",
3915
+ cornersOut: "M216,48V88a8,8,0,0,1-16,0V56H168a8,8,0,0,1,0-16h40A8,8,0,0,1,216,48ZM88,200H56V168a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H88a8,8,0,0,0,0-16Zm120-40a8,8,0,0,0-8,8v32H168a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V168A8,8,0,0,0,208,160ZM88,40H48a8,8,0,0,0-8,8V88a8,8,0,0,0,16,0V56H88a8,8,0,0,0,0-16Z",
3916
+ folder: "M216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72Zm0,128H40V64H93.33L123.2,86.4A8,8,0,0,0,128,88h88Z",
3917
+ keyboard: "M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192Zm-16-64a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,128Zm0-32a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,96ZM72,160a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16h8A8,8,0,0,1,72,160Zm96,0a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,160Zm40,0a8,8,0,0,1-8,8h-8a8,8,0,0,1,0-16h8A8,8,0,0,1,208,160Z",
3918
+ magnifyingGlass: "M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z",
3919
+ magnifyingGlassMinus: "M152,112a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h64A8,8,0,0,1,152,112Zm77.66,117.66a8,8,0,0,1-11.32,0l-50.06-50.07a88.11,88.11,0,1,1,11.31-11.31l50.07,50.06A8,8,0,0,1,229.66,229.66ZM112,184a72,72,0,1,0-72-72A72.08,72.08,0,0,0,112,184Z",
3920
+ magnifyingGlassPlus: "M152,112a8,8,0,0,1-8,8H120v24a8,8,0,0,1-16,0V120H80a8,8,0,0,1,0-16h24V80a8,8,0,0,1,16,0v24h24A8,8,0,0,1,152,112Zm77.66,117.66a8,8,0,0,1-11.32,0l-50.06-50.07a88.11,88.11,0,1,1,11.31-11.31l50.07,50.06A8,8,0,0,1,229.66,229.66ZM112,184a72,72,0,1,0-72-72A72.08,72.08,0,0,0,112,184Z",
3921
+ microphone: "M128,176a48.05,48.05,0,0,0,48-48V64a48,48,0,0,0-96,0v64A48.05,48.05,0,0,0,128,176ZM96,64a32,32,0,0,1,64,0v64a32,32,0,0,1-64,0Zm40,143.6V240a8,8,0,0,1-16,0V207.6A80.11,80.11,0,0,1,48,128a8,8,0,0,1,16,0,64,64,0,0,0,128,0,8,8,0,0,1,16,0A80.11,80.11,0,0,1,136,207.6Z",
3922
+ paperPlaneRight: "M231.87,114l-168-95.89A16,16,0,0,0,40.92,37.34L71.55,128,40.92,218.67A16,16,0,0,0,56,240a16.15,16.15,0,0,0,7.93-2.1l167.92-96.05a16,16,0,0,0,.05-27.89ZM56,224a.56.56,0,0,0,0-.12L85.74,136H144a8,8,0,0,0,0-16H85.74L56.06,32.16A.46.46,0,0,0,56,32l168,95.83Z",
3923
+ pencilSimple: "M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z",
3924
+ repeat: "M24,128A72.08,72.08,0,0,1,96,56H204.69L194.34,45.66a8,8,0,0,1,11.32-11.32l24,24a8,8,0,0,1,0,11.32l-24,24a8,8,0,0,1-11.32-11.32L204.69,72H96a56.06,56.06,0,0,0-56,56,8,8,0,0,1-16,0Zm200-8a8,8,0,0,0-8,8,56.06,56.06,0,0,1-56,56H51.31l10.35-10.34a8,8,0,0,0-11.32-11.32l-24,24a8,8,0,0,0,0,11.32l24,24a8,8,0,0,0,11.32-11.32L51.31,200H160a72.08,72.08,0,0,0,72-72A8,8,0,0,0,224,120Z",
3925
+ sidebar: "M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40ZM40,56H80V200H40ZM216,200H96V56H216V200Z",
3926
+ x: "M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"
3927
+ };
3928
+ ICON_NAMES = Object.keys(PATHS);
3929
+ });
3930
+
3931
+ // src/ui/selection-pill.ts
3932
+ function pillSpring(distance, base = SPRINGS.default) {
3933
+ return distance > 400 ? { duration: base.duration * 1.4, bounce: base.bounce } : base;
3934
+ }
3935
+ function pillBoxOf(row, container) {
3936
+ return {
3937
+ x: row.left - container.left - container.clientLeft + container.scrollLeft,
3938
+ y: row.top - container.top - container.clientTop + container.scrollTop,
3939
+ w: row.width,
3940
+ h: row.height
3941
+ };
3942
+ }
3943
+ function pillRadius(from, to, h, k) {
3944
+ const t = Math.abs(to.h - from.h) > 0.5 ? (h - from.h) / (to.h - from.h) : Number.isFinite(k) ? k : 1;
3945
+ return from.r + (to.r - from.r) * Math.min(1, Math.max(0, t));
3946
+ }
3947
+ function pillProgress(from, to, at) {
3948
+ const d = Math.hypot(to.x - from.x, to.y - from.y);
3949
+ if (d < 0.5)
3950
+ return 1;
3951
+ const p = ((at.x - from.x) * (to.x - from.x) + (at.y - from.y) * (to.y - from.y)) / (d * d);
3952
+ return Math.min(1, Math.max(0, p));
3953
+ }
3954
+ function planPill(state, next, flags = {}) {
3955
+ if (!next)
3956
+ return { kind: "hide" };
3957
+ if (!state.shown || !state.at || flags.first || flags.reduced || flags.snap)
3958
+ return { kind: "land", box: next };
3959
+ const t = state.target;
3960
+ if (t && t.x === next.x && t.y === next.y && t.w === next.w && t.h === next.h)
3961
+ return { kind: "stay" };
3962
+ const d = Math.hypot(next.x - state.at.x, next.y - state.at.y);
3963
+ return { kind: "travel", box: next, spring: pillSpring(d) };
3964
+ }
3965
+ function selectionPill(container, options) {
3966
+ const doc = container.ownerDocument;
3967
+ const win = doc.defaultView;
3968
+ injectUiStyles(doc);
3969
+ if (win.getComputedStyle(container).position === "static")
3970
+ container.style.position = "relative";
3971
+ const pill = doc.createElement("i");
3972
+ pill.className = "knobs-pick-pill";
3973
+ pill.setAttribute("aria-hidden", "true");
3974
+ pill.dataset.off = "";
3975
+ const copyOn = options.copy !== false;
3976
+ const copy = doc.createElement("div");
3977
+ copy.className = "knobs-pick-copy";
3978
+ copy.setAttribute("aria-hidden", "true");
3979
+ copy.inert = true;
3980
+ copy.dataset.off = "";
3981
+ const copyRows = doc.createElement("div");
3982
+ copyRows.className = "knobs-pick-copy-rows";
3983
+ copy.append(copyRows);
3984
+ container.prepend(pill);
3985
+ if (copyOn)
3986
+ container.append(copy);
3987
+ const { omega, zeta } = springMotion(SPRINGS.default);
3988
+ const springs = [0, 0, 0, 0].map(() => new Spring(0, omega, zeta, 0.01));
3989
+ const [sx, sy, sw, sh] = springs;
3990
+ let shown = false;
3991
+ let first = true;
3992
+ let target = null;
3993
+ let from = { x: 0, y: 0, w: 0, h: 0 };
3994
+ let fromR = 0;
3995
+ let toR = 0;
3996
+ let rowBoxes = [];
3997
+ const clones = new Map;
3998
+ let disposed = false;
3999
+ const radiusOf = (row) => options.radius?.(row) ?? (Number.parseFloat(win.getComputedStyle(row).borderTopLeftRadius) || 0);
4000
+ const rowsNow = () => options.rows ? [...options.rows()] : [...container.children].filter((n) => n !== pill && n !== copy);
4001
+ const measure = (row) => {
4002
+ const c = container.getBoundingClientRect();
4003
+ const r = row.getBoundingClientRect();
4004
+ return pillBoxOf(r, {
4005
+ left: c.left,
4006
+ top: c.top,
4007
+ scrollLeft: container.scrollLeft,
4008
+ scrollTop: container.scrollTop,
4009
+ clientLeft: container.clientLeft,
4010
+ clientTop: container.clientTop
4011
+ });
4012
+ };
4013
+ const at = () => ({ x: sx.x, y: sy.x, w: sw.x, h: sh.x });
4014
+ const syncCopy = (span) => {
4015
+ if (!copyOn)
4016
+ return;
4017
+ const keep = new Set;
4018
+ for (const { row, box } of rowBoxes) {
4019
+ if (!boxesMeet(box, span))
4020
+ continue;
4021
+ keep.add(row);
4022
+ let n = clones.get(row);
4023
+ if (!n) {
4024
+ n = row.cloneNode(true);
4025
+ n.removeAttribute("id");
4026
+ for (const d of n.querySelectorAll("[id]"))
4027
+ d.removeAttribute("id");
4028
+ clones.set(row, n);
4029
+ copyRows.append(n);
4030
+ }
4031
+ const s = n.style;
4032
+ s.position = "absolute";
4033
+ s.inset = "auto";
4034
+ s.left = `${box.x}px`;
4035
+ s.top = `${box.y}px`;
4036
+ s.width = `${box.w}px`;
4037
+ s.height = `${box.h}px`;
4038
+ s.margin = "0";
4039
+ s.transform = "none";
4040
+ s.clipPath = "";
4041
+ s.opacity = "";
4042
+ s.boxSizing = "border-box";
4043
+ s.color = options.color ?? "var(--_k-fg)";
4044
+ }
4045
+ for (const [row, n] of clones)
4046
+ if (!keep.has(row)) {
4047
+ n.remove();
4048
+ clones.delete(row);
4049
+ }
4050
+ };
4051
+ const paint = () => {
4052
+ const b = at();
4053
+ const x0 = grid(b.x);
4054
+ const y0 = grid(b.y);
4055
+ const w = grid(b.x + b.w) - x0;
4056
+ const h = grid(b.y + b.h) - y0;
4057
+ const r = target ? pillRadius({ h: from.h, r: fromR }, { h: target.h, r: toR }, b.h, pillProgress(from, target, b)) : toR;
4058
+ const radius = `${Math.max(0, Math.min(r, h / 2, w / 2)).toFixed(2)}px`;
4059
+ for (const n of copyOn ? [pill, copy] : [pill]) {
4060
+ n.style.transform = `translate3d(${x0}px,${y0}px,0)`;
4061
+ n.style.width = `${w}px`;
4062
+ n.style.height = `${h}px`;
4063
+ n.style.borderRadius = radius;
4064
+ }
4065
+ if (copyOn)
4066
+ copyRows.style.transform = `translate3d(${-x0}px,${-y0}px,0)`;
4067
+ };
4068
+ const loop = frameLoop(win, (dt) => {
4069
+ for (const s of springs)
4070
+ s.step(dt);
4071
+ paint();
4072
+ return springs.some((s) => !s.resting);
4073
+ });
4074
+ const show = (on) => {
4075
+ shown = on;
4076
+ pill.toggleAttribute("data-off", !on);
4077
+ copy.toggleAttribute("data-off", !on);
4078
+ container.toggleAttribute("data-pick-empty", !on);
4079
+ };
4080
+ const update = (o) => {
4081
+ if (disposed)
4082
+ return;
4083
+ const row = options.selected();
4084
+ const next = row?.isConnected ? measure(row) : null;
4085
+ const plan = planPill({ shown, at: shown ? at() : null, target }, next, {
4086
+ first,
4087
+ reduced: reducedMotion(win),
4088
+ ...o?.snap ? { snap: true } : {}
4089
+ });
4090
+ first = false;
4091
+ for (const n of clones.values())
4092
+ n.remove();
4093
+ clones.clear();
4094
+ rowBoxes = copyOn ? rowsNow().filter((r) => r.isConnected).map((r) => ({ row: r, box: measure(r) })) : [];
4095
+ if (plan.kind === "hide") {
4096
+ if (shown)
4097
+ show(false);
4098
+ return;
4099
+ }
4100
+ if (plan.kind === "stay") {
4101
+ syncCopy(boxUnion(at(), target));
4102
+ return;
4103
+ }
4104
+ const r = radiusOf(row);
4105
+ if (plan.kind === "land") {
4106
+ sx.snap(plan.box.x);
4107
+ sy.snap(plan.box.y);
4108
+ sw.snap(plan.box.w);
4109
+ sh.snap(plan.box.h);
4110
+ from = plan.box;
4111
+ fromR = r;
4112
+ toR = r;
4113
+ target = plan.box;
4114
+ syncCopy(plan.box);
4115
+ paint();
4116
+ loop.stop();
4117
+ if (!shown)
4118
+ show(true);
4119
+ return;
4120
+ }
4121
+ const m = springMotion(plan.spring);
4122
+ for (const s of springs) {
4123
+ s.omega = m.omega;
4124
+ s.zeta = m.zeta;
4125
+ }
4126
+ const now = at();
4127
+ fromR = target ? pillRadius({ h: from.h, r: fromR }, { h: target.h, r: toR }, now.h, pillProgress(from, target, now)) : r;
4128
+ from = now;
4129
+ toR = r;
4130
+ target = plan.box;
4131
+ sx.set(plan.box.x);
4132
+ sy.set(plan.box.y);
4133
+ sw.set(plan.box.w);
4134
+ sh.set(plan.box.h);
4135
+ syncCopy(boxUnion(now, plan.box));
4136
+ loop.wake();
4137
+ };
4138
+ const ro = "ResizeObserver" in win ? new win.ResizeObserver(() => update({ snap: true })) : null;
4139
+ ro?.observe(container);
4140
+ update();
4141
+ return {
4142
+ element: pill,
4143
+ update,
4144
+ dispose() {
4145
+ disposed = true;
4146
+ loop.stop();
4147
+ ro?.disconnect();
4148
+ pill.remove();
4149
+ copy.remove();
4150
+ container.removeAttribute("data-pick-empty");
4151
+ }
4152
+ };
4153
+ }
4154
+ var boxesMeet = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h, boxUnion = (a, b) => {
4155
+ const x = Math.min(a.x, b.x);
4156
+ const y = Math.min(a.y, b.y);
4157
+ return {
4158
+ x,
4159
+ y,
4160
+ w: Math.max(a.x + a.w, b.x + b.w) - x,
4161
+ h: Math.max(a.y + a.h, b.y + b.h) - y
4162
+ };
4163
+ }, grid = (v) => Math.round(v * 64) / 64;
4164
+ var init_selection_pill = __esm(() => {
4165
+ init_motion();
4166
+ init_look();
4167
+ });
4168
+
3863
4169
  // src/panel/hints.ts
3864
4170
  function extractWords(text) {
3865
4171
  return text.toLowerCase().match(/[a-z]+/g) ?? [];
@@ -4688,34 +4994,27 @@ function tree(container, options) {
4688
4994
  const empty = doc.createElement("div");
4689
4995
  empty.className = "knobs-tree-empty";
4690
4996
  empty.hidden = true;
4691
- const cursor = doc.createElement("div");
4692
- cursor.className = "knobs-tree-cursor";
4693
- root.append(space, cursor, line, empty);
4694
- const cursorY = new Spring(0, MOVE.omega, MOVE.zeta, 0.1);
4695
- let cursorOn = false;
4696
- const drawCursor = () => {
4697
- cursor.style.transform = `translate3d(0,${Math.round(cursorY.x * 2) / 2}px,0)`;
4997
+ root.append(space, line, empty);
4998
+ let pill = null;
4999
+ let picked = null;
5000
+ let travelUntil = 0;
5001
+ const pickedRow = () => {
5002
+ const id = sel.ids.length === 1 ? sel.ids[0] : null;
5003
+ const v = id === null ? undefined : views.get(id);
5004
+ return v && !v.leaving && rowIndex.has(v.id) ? v : null;
4698
5005
  };
4699
5006
  const placeCursor = (animate) => {
4700
- if (!options.glide)
5007
+ if (!pill)
4701
5008
  return;
4702
5009
  const id = sel.ids.length === 1 ? sel.ids[0] : null;
4703
- const i = id === null ? undefined : rowIndex.get(id);
4704
- root.toggleAttribute("data-glide", i !== undefined);
4705
- if (i === undefined) {
4706
- cursor.removeAttribute("data-on");
4707
- cursorOn = false;
4708
- return;
4709
- }
4710
- if (cursorOn && animate && !reducedMotion(win))
4711
- cursorY.set(i * h);
4712
- else
4713
- cursorY.snap(i * h);
4714
- cursorOn = true;
4715
- cursor.setAttribute("data-on", "");
4716
- drawCursor();
4717
- if (!cursorY.resting)
4718
- loop.wake();
5010
+ root.toggleAttribute("data-glide", id !== null && rowIndex.has(id));
5011
+ const fresh = id !== picked;
5012
+ picked = id;
5013
+ if (animate && fresh) {
5014
+ travelUntil = performance.now() + 450;
5015
+ pill.update();
5016
+ } else if (performance.now() >= travelUntil)
5017
+ pill.update({ snap: true });
4719
5018
  };
4720
5019
  container.append(root);
4721
5020
  const scrollbar = overlayScrollbar(root, space);
@@ -4744,6 +5043,12 @@ function tree(container, options) {
4744
5043
  };
4745
5044
  const views = new Map;
4746
5045
  const pool = [];
5046
+ if (options.glide)
5047
+ pill = selectionPill(root, {
5048
+ rows: () => [...views.values()].filter((v) => !v.leaving).map((v) => v.el),
5049
+ selected: () => pickedRow()?.el ?? null,
5050
+ radius: (row) => Number.parseFloat(win.getComputedStyle(row, "::before").borderTopLeftRadius) || 0
5051
+ });
4747
5052
  const makeView = (id) => {
4748
5053
  const v = pool.pop() ?? createView();
4749
5054
  v.id = id;
@@ -4924,12 +5229,6 @@ function tree(container, options) {
4924
5229
  function frame(dt) {
4925
5230
  let moving = false;
4926
5231
  let bottom = rows.length * h;
4927
- if (options.glide && !cursorY.resting) {
4928
- cursorY.step(dt);
4929
- drawCursor();
4930
- if (!cursorY.resting)
4931
- moving = true;
4932
- }
4933
5232
  for (const v of views.values()) {
4934
5233
  v.y.step(dt);
4935
5234
  if (!v.y.resting)
@@ -4949,6 +5248,8 @@ function tree(container, options) {
4949
5248
  }
4950
5249
  setSpace(moving ? bottom : rows.length * h);
4951
5250
  trim();
5251
+ if (pill && moving && performance.now() >= travelUntil)
5252
+ pill.update({ snap: true });
4952
5253
  return moving;
4953
5254
  }
4954
5255
  const trim = () => {
@@ -5577,6 +5878,7 @@ function tree(container, options) {
5577
5878
  autoScroll.stop();
5578
5879
  ro.disconnect();
5579
5880
  scrollbar.dispose();
5881
+ pill?.dispose();
5580
5882
  dragging?.ghost.remove();
5581
5883
  root.remove();
5582
5884
  }
@@ -5595,6 +5897,7 @@ var init_tree = __esm(() => {
5595
5897
  init_motion();
5596
5898
  init_page_hints();
5597
5899
  init_look();
5900
+ init_selection_pill();
5598
5901
  init_tree_model();
5599
5902
  MOVE = springMotion(SPRINGS.default);
5600
5903
  });
@@ -5731,7 +6034,7 @@ function codeDiff(container, options) {
5731
6034
  };
5732
6035
  const nav = make("span", "knobs-diff-nav");
5733
6036
  const prev = button("Previous file", PREV);
5734
- const next = button("Next file", ICONS.caret);
6037
+ const next = button("Next file", NEXT);
5735
6038
  const place = make("span", "");
5736
6039
  nav.append(prev, place, next);
5737
6040
  const wrapButton = button("Wrap lines", WRAP);
@@ -5752,9 +6055,8 @@ function codeDiff(container, options) {
5752
6055
  const seg = make("div", "knobs-diff-seg");
5753
6056
  seg.setAttribute("role", "radiogroup");
5754
6057
  seg.setAttribute("aria-label", "Layout");
5755
- const pill = make("i", "knobs-diff-pill");
5756
6058
  const choice = (value, label) => {
5757
- const b = make("button", "");
6059
+ const b = make("button", "knobs-diff-choice");
5758
6060
  b.type = "button";
5759
6061
  b.setAttribute("role", "radio");
5760
6062
  b.dataset.value = value;
@@ -5763,7 +6065,7 @@ function codeDiff(container, options) {
5763
6065
  };
5764
6066
  const splitButton = choice("split", "Split");
5765
6067
  const unifiedButton = choice("unified", "Unified");
5766
- seg.append(pill, splitButton, unifiedButton);
6068
+ seg.append(splitButton, unifiedButton);
5767
6069
  if (options.lead)
5768
6070
  head.append(options.lead);
5769
6071
  head.append(path, stat, make("span", "knobs-diff-fill"), nav, wrapButton, seg, viewedButton);
@@ -5774,6 +6076,11 @@ function codeDiff(container, options) {
5774
6076
  body.append(note, probe);
5775
6077
  root.append(head, body);
5776
6078
  container.append(root);
6079
+ const segPill = selectionPill(seg, {
6080
+ rows: () => [splitButton, unifiedButton],
6081
+ selected: () => layout === "split" ? splitButton : unifiedButton
6082
+ });
6083
+ let pilled = layout;
5777
6084
  const fileNow = () => files.find((f) => f.name === current) ?? null;
5778
6085
  const paintHead = () => {
5779
6086
  const file = fileNow();
@@ -5806,6 +6113,10 @@ function codeDiff(container, options) {
5806
6113
  next.disabled = at >= files.length - 1;
5807
6114
  wrapButton.setAttribute("aria-pressed", String(wrap));
5808
6115
  seg.dataset.value = layout;
6116
+ if (pilled !== layout) {
6117
+ pilled = layout;
6118
+ segPill.update();
6119
+ }
5809
6120
  splitButton.setAttribute("aria-checked", String(layout === "split"));
5810
6121
  unifiedButton.setAttribute("aria-checked", String(layout === "unified"));
5811
6122
  splitButton.tabIndex = layout === "split" ? 0 : -1;
@@ -6640,6 +6951,7 @@ function codeDiff(container, options) {
6640
6951
  dispose() {
6641
6952
  disposed = true;
6642
6953
  loop.stop();
6954
+ segPill.dispose();
6643
6955
  sizes.disconnect();
6644
6956
  attaching.disconnect();
6645
6957
  resized.disconnect();
@@ -6995,7 +7307,7 @@ function diffReview(container, options) {
6995
7307
  }
6996
7308
  };
6997
7309
  }
6998
- var lib, load = () => lib ??= import("@pierre/diffs"), SYNTAX, IN, OUT, FADE4, HEIGHT, DRAWER, BLUR = 3, WRAP, PREV, UNVIEWED, VIEWED, SEARCH, CLEAR2, SIDEBAR, SEPARATOR = 32;
7310
+ var lib, load = () => lib ??= import("@pierre/diffs"), SYNTAX, IN, OUT, FADE4, HEIGHT, DRAWER, BLUR = 3, WRAP, PREV, NEXT, UNVIEWED, VIEWED, SEARCH, CLEAR2, SIDEBAR, SEPARATOR = 32;
6999
7311
  var init_diff = __esm(() => {
7000
7312
  init_glide();
7001
7313
  init_motion();
@@ -7003,6 +7315,8 @@ var init_diff = __esm(() => {
7003
7315
  init_diff_model();
7004
7316
  init_look();
7005
7317
  init_rgba();
7318
+ init_icons();
7319
+ init_selection_pill();
7006
7320
  init_tree();
7007
7321
  SYNTAX = { dark: "pierre-dark", light: "pierre-light" };
7008
7322
  IN = springMotion(SPRINGS.default);
@@ -7010,13 +7324,14 @@ var init_diff = __esm(() => {
7010
7324
  FADE4 = springMotion(FADE);
7011
7325
  HEIGHT = springMotion(SPRINGS.calm);
7012
7326
  DRAWER = springMotion(SPRINGS.calm);
7013
- WRAP = svg('<path d="M2.5 4h11"/><path d="M2.5 8h9a2 2 0 0 1 0 4H8.5"/><path d="M10 10.5 8.5 12l1.5 1.5"/><path d="M2.5 12h3"/>');
7014
- PREV = svg('<path d="M10 3.5 5.5 8l4.5 4.5"/>');
7015
- UNVIEWED = svg('<circle cx="8" cy="8" r="5.5"/>');
7016
- VIEWED = svg('<circle cx="8" cy="8" r="5.5"/><path d="m5.6 8.1 1.7 1.7 3.2-3.5"/>');
7017
- SEARCH = svg('<circle cx="7" cy="7" r="4.5"/><path d="m10.5 10.5 3 3"/>');
7018
- CLEAR2 = svg('<path d="m5 5 6 6M11 5l-6 6"/>');
7019
- SIDEBAR = svg('<rect x="2" y="3" width="12" height="10" rx="2"/><path d="M6.5 3v10"/>');
7327
+ WRAP = icon("arrowUDownLeft");
7328
+ PREV = icon("caretLeft");
7329
+ NEXT = icon("caretRight");
7330
+ UNVIEWED = icon("circle");
7331
+ VIEWED = icon("checkCircle");
7332
+ SEARCH = icon("magnifyingGlass", { small: true });
7333
+ CLEAR2 = icon("x", { small: true });
7334
+ SIDEBAR = icon("sidebar");
7020
7335
  });
7021
7336
 
7022
7337
  // src/ui/layer.ts
@@ -7999,10 +8314,25 @@ function advance(t, dt, o) {
7999
8314
  }
8000
8315
  return { t: hi, ended: true, wrapped: false };
8001
8316
  }
8002
- 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;
8317
+ function moveHead(head, t, motion, catching, minGap = 0) {
8318
+ if (motion === "jump" && Math.abs(t - head.x) > minGap)
8319
+ return { x: head.x, target: t, catching: true };
8320
+ if (catching)
8321
+ return { x: head.x + (t - head.target), target: t, catching: true };
8322
+ return { x: t, target: t, catching: false };
8323
+ }
8324
+ function currentMarker(markers, t, fps) {
8325
+ let best = null;
8326
+ for (const m of markers)
8327
+ if (m.time <= t + 0.5 / fps && (!best || m.time >= best.time))
8328
+ best = m;
8329
+ return best;
8330
+ }
8331
+ 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;
8003
8332
  var init_timeline_model = __esm(() => {
8004
8333
  SECOND_STEPS = [0.1, 0.2, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600];
8005
8334
  FRAME_STEPS = [1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 9000, 18000];
8335
+ CATCH_UP = { duration: 0.15, bounce: 0 };
8006
8336
  });
8007
8337
 
8008
8338
  // src/ui/timeline.ts
@@ -8015,6 +8345,7 @@ function timeline(container, options) {
8015
8345
  let speed = options.speed ?? 1;
8016
8346
  let repeat = options.repeat ?? true;
8017
8347
  const arrows = options.arrows ?? "frames";
8348
+ const lane = options.variant === "lane";
8018
8349
  let time = Math.min(duration, Math.max(0, options.time ?? 0));
8019
8350
  let playing = false;
8020
8351
  let markers = [...options.markers ?? []];
@@ -8048,27 +8379,27 @@ function timeline(container, options) {
8048
8379
  root.setAttribute("aria-label", options.label ?? "Timeline");
8049
8380
  root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight , . Home End [ ] I O M");
8050
8381
  applyLook2(root, options);
8382
+ if (lane)
8383
+ root.dataset.variant = "lane";
8051
8384
  const transport = el("div", "knobs-tl-transport");
8052
8385
  const playBtn = el("button", "knobs-tl-play");
8053
8386
  playBtn.type = "button";
8054
8387
  playBtn.tabIndex = -1;
8055
- const readout = el("div", "knobs-tl-time");
8056
- const now = el("span", "knobs-tl-now");
8057
- const sub = el("span", "knobs-tl-sub");
8058
- readout.append(now, sub);
8059
- transport.append(playBtn, readout);
8388
+ transport.append(playBtn);
8060
8389
  const track = el("div", "knobs-tl-track");
8061
8390
  const canvas = el("canvas", "knobs-tl-ruler");
8062
8391
  canvas.setAttribute("aria-hidden", "true");
8063
- const lane = el("div", "knobs-tl-lane");
8392
+ const markerLane = el("div", "knobs-tl-lane");
8064
8393
  const hover = el("div", "knobs-tl-hover");
8394
+ const tag = el("span", "knobs-tl-tag");
8395
+ tag.setAttribute("aria-hidden", "true");
8065
8396
  const head = el("div", "knobs-tl-head");
8066
8397
  head.id = `${id}-head`;
8067
8398
  head.setAttribute("role", "slider");
8068
8399
  head.setAttribute("aria-label", "Playhead");
8069
8400
  head.setAttribute("aria-valuemin", "0");
8070
8401
  head.setAttribute("aria-valuemax", String(duration));
8071
- track.append(canvas, lane, hover, head);
8402
+ track.append(canvas, markerLane, hover, head, tag);
8072
8403
  root.append(transport, track);
8073
8404
  container.append(root);
8074
8405
  let width = 1;
@@ -8115,13 +8446,14 @@ function timeline(container, options) {
8115
8446
  let drawn = "";
8116
8447
  const drawRuler = () => {
8117
8448
  const v = view();
8118
- const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${units}:${fps}`;
8449
+ const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${height}:${units}:${fps}`;
8119
8450
  if (key === drawn)
8120
8451
  return;
8121
8452
  drawn = key;
8122
8453
  const dpr = win.devicePixelRatio || 1;
8123
8454
  const w = Math.max(1, Math.round(width * dpr));
8124
- const h = Math.round(RULER * dpr);
8455
+ const rh = lane ? height : RULER;
8456
+ const h = Math.round(rh * dpr);
8125
8457
  if (canvas.width !== w || canvas.height !== h) {
8126
8458
  canvas.width = w;
8127
8459
  canvas.height = h;
@@ -8130,8 +8462,19 @@ function timeline(container, options) {
8130
8462
  if (!ctx)
8131
8463
  return;
8132
8464
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
8133
- ctx.clearRect(0, 0, width, RULER);
8465
+ ctx.clearRect(0, 0, width, rh);
8134
8466
  const ticks = rulerTicks(v, width, { fps, units });
8467
+ if (lane) {
8468
+ ctx.fillStyle = colors.tick;
8469
+ for (const t of ticks.minor)
8470
+ if (t >= -0.000000001 && t <= duration + 0.000000001)
8471
+ ctx.fillRect(xOf(t) - 0.5, rh - 4, 1, 4);
8472
+ ctx.fillStyle = colors.major;
8473
+ for (const m of ticks.major)
8474
+ if (m.t >= -0.000000001 && m.t <= duration + 0.000000001)
8475
+ ctx.fillRect(xOf(m.t) - 0.5, rh - 7, 1, 7);
8476
+ return;
8477
+ }
8135
8478
  const px = (t) => Math.round(xOf(t)) + 0.5;
8136
8479
  ctx.fillStyle = colors.line;
8137
8480
  ctx.fillRect(0, RULER - 1, width, 1);
@@ -8171,18 +8514,21 @@ function timeline(container, options) {
8171
8514
  n.dataset.id = m.id;
8172
8515
  const pin = el("span", "knobs-tl-pin");
8173
8516
  const label = el("span", "knobs-tl-mlabel");
8517
+ label.append(el("span", "knobs-tl-mtext knobs-scroll-fade-x"));
8174
8518
  n.append(pin, label);
8175
- lane.append(n);
8519
+ markerLane.append(n);
8176
8520
  markerEls.set(m.id, n);
8177
8521
  if (built)
8178
8522
  enter(n);
8179
8523
  }
8180
8524
  const label = n.querySelector(".knobs-tl-mlabel");
8181
- if (label.textContent !== (m.label ?? ""))
8182
- label.textContent = m.label ?? "";
8525
+ const text = label.firstElementChild;
8526
+ if (text.textContent !== (m.label ?? ""))
8527
+ text.textContent = m.label ?? "";
8183
8528
  label.hidden = !m.label;
8184
8529
  n.style.setProperty("--knobs-tl-color", m.color ?? "");
8185
8530
  n.toggleAttribute("data-selected", selected?.kind === "marker" && selected.id === m.id);
8531
+ n.toggleAttribute("data-current", m.id === currentId);
8186
8532
  n.title = `${m.label ? `${m.label} · ` : ""}${clock(m.time)}`;
8187
8533
  }
8188
8534
  placeMarkers();
@@ -8210,7 +8556,7 @@ function timeline(container, options) {
8210
8556
  b.dataset.edge = "end";
8211
8557
  const label = el("span", "knobs-tl-rlabel");
8212
8558
  n.append(label, a, b);
8213
- track.insertBefore(n, lane);
8559
+ track.insertBefore(n, markerLane);
8214
8560
  rangeEls.set(r.id, n);
8215
8561
  if (built)
8216
8562
  enter(n);
@@ -8259,16 +8605,17 @@ function timeline(container, options) {
8259
8605
  };
8260
8606
  const clock = (t) => units === "frames" ? `f${toFrame(t, fps)}` : `${timeLabel(t, "seconds", fps, 0.01)}`;
8261
8607
  let shownHead = time;
8608
+ let currentId = null;
8262
8609
  const renderHead = () => {
8263
8610
  head.style.transform = `translateX(${xOf(shownHead).toFixed(2)}px)`;
8264
- const main = units === "frames" ? `f${toFrame(time, fps)}` : `${time.toFixed(2)}s`;
8265
- const minor = units === "frames" ? `${time.toFixed(2)}s` : `f${toFrame(time, fps)}`;
8266
- if (now.textContent !== main)
8267
- now.textContent = main;
8268
- if (sub.textContent !== minor)
8269
- sub.textContent = minor;
8270
8611
  head.setAttribute("aria-valuenow", time.toFixed(3));
8271
8612
  head.setAttribute("aria-valuetext", `${time.toFixed(2)} seconds, frame ${toFrame(time, fps)}`);
8613
+ const cur = currentMarker(markers, time, fps)?.id ?? null;
8614
+ if (cur !== currentId) {
8615
+ markerEls.get(currentId ?? "")?.removeAttribute("data-current");
8616
+ currentId = cur;
8617
+ markerEls.get(cur ?? "")?.setAttribute("data-current", "");
8618
+ }
8272
8619
  };
8273
8620
  let viewSent = "";
8274
8621
  const render = () => {
@@ -8294,18 +8641,27 @@ function timeline(container, options) {
8294
8641
  };
8295
8642
  const caret = new Spring(time, GLIDE.omega, GLIDE.zeta, 0.00001);
8296
8643
  let gliding = false;
8297
- const setTime = (t, source, shown = t, glide = false) => {
8298
- const next = Math.min(duration, Math.max(0, t));
8299
- if (glide && !reducedMotion(win)) {
8644
+ const headTo = (shown, motion) => {
8645
+ const from = gliding ? { x: caret.x, target: caret.target } : { x: shownHead, target: shownHead };
8646
+ const jump = motion === "jump" && !reducedMotion(win);
8647
+ const next = moveHead(from, shown, jump ? "jump" : "follow", gliding, 2 / pxPerSec());
8648
+ if (next.catching) {
8300
8649
  if (!gliding)
8301
- caret.snap(shownHead);
8302
- caret.set(next);
8650
+ caret.snap(from.x);
8651
+ caret.x = next.x;
8652
+ caret.target = next.target;
8303
8653
  gliding = true;
8654
+ shownHead = caret.x;
8304
8655
  loop.wake();
8305
8656
  } else {
8306
8657
  gliding = false;
8307
- shownHead = shown;
8658
+ caret.snap(next.x);
8659
+ shownHead = next.x;
8308
8660
  }
8661
+ };
8662
+ const setTime = (t, source, shown = t, glide = false) => {
8663
+ const next = Math.min(duration, Math.max(0, t));
8664
+ headTo(Math.min(duration, Math.max(0, shown)), glide ? "jump" : "follow");
8309
8665
  const moved = next !== time;
8310
8666
  time = next;
8311
8667
  renderHead();
@@ -8346,8 +8702,7 @@ function timeline(container, options) {
8346
8702
  options.onCross?.(m);
8347
8703
  }
8348
8704
  time = step.t;
8349
- shownHead = time;
8350
- gliding = false;
8705
+ headTo(time, step.wrapped ? "jump" : "follow");
8351
8706
  emitSeek(time, "play");
8352
8707
  keepInView();
8353
8708
  if (step.ended) {
@@ -8360,7 +8715,7 @@ function timeline(container, options) {
8360
8715
  shownHead = caret.step(dt);
8361
8716
  if (caret.resting) {
8362
8717
  gliding = false;
8363
- shownHead = time;
8718
+ shownHead = caret.target;
8364
8719
  }
8365
8720
  }
8366
8721
  vStart.step(dt);
@@ -8373,9 +8728,9 @@ function timeline(container, options) {
8373
8728
  return;
8374
8729
  const r = loopRange();
8375
8730
  if (r && (time < r.start || time >= r.end - 0.5 / fps))
8376
- setTime(r.start, "code");
8731
+ setTime(r.start, "code", r.start, true);
8377
8732
  else if (!r && time >= duration - 0.5 / fps)
8378
- setTime(0, "code");
8733
+ setTime(0, "code", 0, true);
8379
8734
  playing = true;
8380
8735
  syncPlay();
8381
8736
  emitPlay(true);
@@ -8504,24 +8859,35 @@ function timeline(container, options) {
8504
8859
  };
8505
8860
  const localX = (e) => e.clientX - track.getBoundingClientRect().left;
8506
8861
  let drag = null;
8507
- const scrubTo = (x) => {
8862
+ const scrubTo = (x, press = false) => {
8508
8863
  const shownX = softClamp(x, xOf(0), xOf(duration), 10);
8509
8864
  const s = scrubTime(tOf(shownX), {
8510
8865
  duration,
8511
8866
  fps,
8512
8867
  pxPerSec: pxPerSec(),
8513
- stops: snapMarkers ? markers.map((m) => m.time) : [],
8868
+ stops: snapMarkers && !lane ? markers.map((m) => m.time) : [],
8514
8869
  frames: snapFrames
8515
8870
  });
8516
- setTime(s.t, "scrub", s.shown);
8871
+ setTime(s.t, "scrub", s.shown, press);
8517
8872
  root.toggleAttribute("data-stuck", s.stop !== null);
8518
8873
  };
8874
+ const showTag = (x) => {
8875
+ const at = Math.min(duration, Math.max(0, tOf(x)));
8876
+ const text = `${at.toFixed(2)} s`;
8877
+ if (tag.textContent !== text)
8878
+ tag.textContent = text;
8879
+ const cx = Math.min(width, Math.max(0, x));
8880
+ const flip = cx > width - tag.offsetWidth - 12;
8881
+ tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
8882
+ tag.dataset.on = "";
8883
+ };
8884
+ const hideTag = () => delete tag.dataset.on;
8519
8885
  track.addEventListener("pointerdown", (e) => {
8520
8886
  if (e.button !== 0 || editing)
8521
8887
  return;
8522
8888
  root.focus({ preventScroll: true });
8523
8889
  const t = e.target;
8524
- const markerEl = t.closest(".knobs-tl-marker");
8890
+ const markerEl = lane ? null : t.closest(".knobs-tl-marker");
8525
8891
  const edgeEl = t.closest(".knobs-tl-edge");
8526
8892
  const rangeEl = t.closest(".knobs-tl-range");
8527
8893
  track.setPointerCapture(e.pointerId);
@@ -8566,13 +8932,16 @@ function timeline(container, options) {
8566
8932
  root.dataset.scrubbing = "";
8567
8933
  for (const l of listeners)
8568
8934
  l.onScrub?.(true);
8569
- scrubTo(localX(e));
8935
+ scrubTo(localX(e), true);
8936
+ showTag(localX(e));
8570
8937
  }
8571
8938
  e.preventDefault();
8572
8939
  });
8573
8940
  track.addEventListener("pointermove", (e) => {
8574
8941
  const x = localX(e);
8575
8942
  hover.style.transform = `translateX(${x.toFixed(1)}px)`;
8943
+ if (!drag || drag.kind === "scrub")
8944
+ showTag(x);
8576
8945
  if (!drag)
8577
8946
  return;
8578
8947
  if (drag.kind === "scrub")
@@ -8618,8 +8987,10 @@ function timeline(container, options) {
8618
8987
  delete root.dataset.dragging;
8619
8988
  root.removeAttribute("data-stuck");
8620
8989
  if (d.kind === "scrub") {
8621
- shownHead = time;
8990
+ headTo(time, "follow");
8622
8991
  renderHead();
8992
+ if (!track.matches(":hover"))
8993
+ hideTag();
8623
8994
  for (const l of listeners)
8624
8995
  l.onScrub?.(false);
8625
8996
  if (d.wasPlaying)
@@ -8632,7 +9003,7 @@ function timeline(container, options) {
8632
9003
  } else {
8633
9004
  const m = markers.find((x) => x.id === d.id);
8634
9005
  if (m)
8635
- setTime(m.time, "marker");
9006
+ setTime(m.time, "marker", m.time, true);
8636
9007
  }
8637
9008
  } else if (d.kind === "edge" || d.moved)
8638
9009
  emitRanges();
@@ -8641,10 +9012,14 @@ function timeline(container, options) {
8641
9012
  track.addEventListener("pointercancel", endDrag);
8642
9013
  track.addEventListener("lostpointercapture", endDrag);
8643
9014
  track.addEventListener("pointerleave", () => {
8644
- if (!drag)
8645
- hover.style.transform = "translateX(-100px)";
9015
+ if (drag)
9016
+ return;
9017
+ hover.style.transform = "translateX(-100px)";
9018
+ hideTag();
8646
9019
  });
8647
9020
  track.addEventListener("dblclick", (e) => {
9021
+ if (lane)
9022
+ return;
8648
9023
  const markerEl = e.target.closest(".knobs-tl-marker");
8649
9024
  if (markerEl)
8650
9025
  return rename(markerEl.dataset.id);
@@ -8655,6 +9030,8 @@ function timeline(container, options) {
8655
9030
  rename(m.id);
8656
9031
  });
8657
9032
  track.addEventListener("wheel", (e) => {
9033
+ if (lane)
9034
+ return;
8658
9035
  e.preventDefault();
8659
9036
  const scale = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? height : 1;
8660
9037
  if (e.ctrlKey || e.metaKey) {
@@ -8718,7 +9095,7 @@ function timeline(container, options) {
8718
9095
  if (!m)
8719
9096
  return;
8720
9097
  select({ kind: "marker", id: m.id });
8721
- return setTime(m.time, "marker");
9098
+ return setTime(m.time, "marker", m.time, true);
8722
9099
  }
8723
9100
  case "i":
8724
9101
  case "I":
@@ -8730,11 +9107,13 @@ function timeline(container, options) {
8730
9107
  return setEdge("end", time);
8731
9108
  case "m":
8732
9109
  case "M":
9110
+ if (lane)
9111
+ return;
8733
9112
  claim();
8734
9113
  addMarker(time);
8735
9114
  return;
8736
9115
  case "Enter":
8737
- if (selected?.kind === "marker") {
9116
+ if (selected?.kind === "marker" && !lane) {
8738
9117
  claim();
8739
9118
  rename(selected.id);
8740
9119
  }
@@ -8752,19 +9131,25 @@ function timeline(container, options) {
8752
9131
  return;
8753
9132
  case "=":
8754
9133
  case "+":
9134
+ if (lane)
9135
+ return;
8755
9136
  claim();
8756
9137
  return zoom(1.5);
8757
9138
  case "-":
9139
+ if (lane)
9140
+ return;
8758
9141
  claim();
8759
9142
  return zoom(1 / 1.5);
8760
9143
  case "0":
9144
+ if (lane)
9145
+ return;
8761
9146
  claim();
8762
9147
  return fit();
8763
9148
  }
8764
9149
  });
8765
9150
  const unregisterHints = options.hints === false ? undefined : hints.register(root, () => [
8766
9151
  { el: playBtn, label: playing ? "Pause" : "Play", activate: toggle },
8767
- ...markers.flatMap((m) => {
9152
+ ...(lane ? [] : markers).flatMap((m) => {
8768
9153
  const n = markerEls.get(m.id);
8769
9154
  if (!n || n.dataset.leaving !== undefined)
8770
9155
  return [];
@@ -8776,7 +9161,7 @@ function timeline(container, options) {
8776
9161
  ...label && !label.hidden ? { labelEl: label } : {},
8777
9162
  activate: () => {
8778
9163
  select({ kind: "marker", id: m.id });
8779
- setTime(m.time, "marker");
9164
+ setTime(m.time, "marker", m.time, true);
8780
9165
  root.focus({ preventScroll: true });
8781
9166
  }
8782
9167
  }
@@ -8788,7 +9173,7 @@ function timeline(container, options) {
8788
9173
  activate: () => root.focus({ preventScroll: true })
8789
9174
  }
8790
9175
  ], { listen: false, rank: 1 });
8791
- const menu = contextMenu(track, (e) => {
9176
+ const menu = lane ? null : contextMenu(track, (e) => {
8792
9177
  const t = e?.target ?? null;
8793
9178
  const markerEl = t?.closest(".knobs-tl-marker");
8794
9179
  const rangeEl = t?.closest(".knobs-tl-range");
@@ -8803,7 +9188,7 @@ function timeline(container, options) {
8803
9188
  run: () => {
8804
9189
  const m = markers.find((x) => x.id === mid);
8805
9190
  if (m)
8806
- setTime(m.time, "marker");
9191
+ setTime(m.time, "marker", m.time, true);
8807
9192
  }
8808
9193
  },
8809
9194
  { label: "Rename…", shortcut: "Enter", run: () => rename(mid) },
@@ -8967,7 +9352,7 @@ function timeline(container, options) {
8967
9352
  listeners.clear();
8968
9353
  loop.stop();
8969
9354
  ro.disconnect();
8970
- menu.dispose();
9355
+ menu?.dispose();
8971
9356
  root.remove();
8972
9357
  }
8973
9358
  };
@@ -8984,10 +9369,21 @@ var init_timeline = __esm(() => {
8984
9369
  init_timeline_model();
8985
9370
  INSTANT2 = springMotion(SPRINGS.instant);
8986
9371
  CALM = springMotion(SPRINGS.calm);
8987
- GLIDE = springMotion(SPRINGS.snappy);
9372
+ GLIDE = springMotion(CATCH_UP);
8988
9373
  });
8989
9374
 
8990
9375
  // src/ui/compare-model.ts
9376
+ function compareModes(modes, mode) {
9377
+ const list = [...new Set((modes ?? ["side", "wipe"]).filter((m) => ALL_MODES.includes(m)))];
9378
+ const offered = list.length ? list : ["side", "wipe"];
9379
+ return { modes: offered, mode: mode && offered.includes(mode) ? mode : offered[0] };
9380
+ }
9381
+ function modeForKey(key, modes) {
9382
+ const k = key.toLowerCase();
9383
+ if (/^[1-9]$/.test(k))
9384
+ return modes[Number(k) - 1] ?? null;
9385
+ return modes.find((m) => MODE_KEYS[m] === k) ?? null;
9386
+ }
8991
9387
  function mediaKind(src, kind) {
8992
9388
  if (kind)
8993
9389
  return kind;
@@ -9066,6 +9462,18 @@ function wipeGrab(x, rect, wipe, slop = 12) {
9066
9462
  const onLine = Math.abs(x - line) <= slop;
9067
9463
  return { onLine, offset: onLine ? x - line : 0 };
9068
9464
  }
9465
+ function seamLabels(x, left, right, wb, wa, gap = 8) {
9466
+ const room = (space, w) => Math.min(1, Math.max(0, (space - w - 2 * gap) / gap));
9467
+ return {
9468
+ before: { x: x - gap - wb, opacity: room(x - left, wb) },
9469
+ after: { x: x + gap, opacity: room(right - x, wa) }
9470
+ };
9471
+ }
9472
+ function gripTop(y, rect, grip, margin = 8) {
9473
+ const lo = rect.y + margin;
9474
+ const hi = rect.y + rect.h - margin - grip;
9475
+ return hi < lo ? rect.y + (rect.h - grip) / 2 : Math.min(hi, Math.max(lo, y - grip / 2));
9476
+ }
9069
9477
  function stripColumns(view, width, o) {
9070
9478
  const { fps } = o;
9071
9479
  const total = Math.max(1, Math.round(o.duration * fps));
@@ -9116,7 +9524,7 @@ function stripPeaks(diffs, o = {}) {
9116
9524
  });
9117
9525
  return out;
9118
9526
  }
9119
- var 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({
9527
+ 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({
9120
9528
  u: r.u - dx / Math.max(1, base.w) * r.w,
9121
9529
  v: r.v - dy / Math.max(1, base.h) * r.h,
9122
9530
  w: r.w
@@ -9135,6 +9543,8 @@ var VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), last
9135
9543
  h: a.h + (b.h - a.h) * k
9136
9544
  }), wipeX = (rect, wipe) => rect.x + Math.min(1, Math.max(0, wipe)) * rect.w, wipeAt = (x, rect, offset = 0) => rect.w <= 0 ? 0.5 : Math.min(1, Math.max(0, (x - offset - rect.x) / rect.w)), wipeKey = (wipe, dir, big = false) => Math.min(1, Math.max(0, Math.round((wipe + dir * (big ? 0.1 : 0.01)) * 1000) / 1000));
9137
9545
  var init_compare_model = __esm(() => {
9546
+ ALL_MODES = ["side", "wipe", "difference"];
9547
+ MODE_KEYS = { side: "s", wipe: "w", difference: "d" };
9138
9548
  VIDEO = /\.(mp4|m4v|mov|webm|ogv|mkv)$/i;
9139
9549
  SEEK_IDLE = { seeking: false, pending: null };
9140
9550
  ZOOM_FIT = { u: 0, v: 0, w: 1, h: 1 };
@@ -9934,8 +10344,8 @@ async function createCompareGpu(canvas, onLost) {
9934
10344
  r(0, f.before);
9935
10345
  r(4, f.after);
9936
10346
  data.set([f.width, f.height, f.dpr, 0], 8);
9937
- data.set([f.wipeX, f.soft, 0, f.phase ? 1 : 0], 12);
9938
- data.set([f.from, f.to, f.mix, f.flick ? 1 : 0], 16);
10347
+ data.set([f.wipeX, f.soft, 0, 0], 12);
10348
+ data.set([f.from, f.to, f.mix, 0], 16);
9939
10349
  data.set([f.threshold, f.gain, ready[0], ready[1]], 20);
9940
10350
  data.set([f.accent[0], f.accent[1], f.accent[2], 1], 24);
9941
10351
  const l = f.loupe;
@@ -9977,8 +10387,8 @@ var MODE_INDEX, WGSL = `
9977
10387
  struct U {
9978
10388
  ra: vec4f, rb: vec4f,
9979
10389
  view: vec4f, // w, h, dpr, _
9980
- wipe: vec4f, // x, soft, _, phase
9981
- modes: vec4f, // from, to, mix, flick
10390
+ wipe: vec4f, // x, soft, _, _
10391
+ modes: vec4f, // from, to, mix, _
9982
10392
  diff: vec4f, // threshold, gain, readyA, readyB
9983
10393
  accent: vec4f,
9984
10394
  loupe: vec4f, // x, y, r, zoom (0: none)
@@ -10029,21 +10439,14 @@ fn takeB(p: vec2f, r: vec4f, c0: vec4f, near: bool) -> vec4f {
10029
10439
 
10030
10440
  fn pm(c: vec4f) -> vec4f { return vec4f(c.rgb * c.a, c.a); }
10031
10441
 
10032
- fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
10033
- let flick = u.modes.w > 0.5;
10442
+ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
10034
10443
  switch mode {
10035
- case 0u: {
10036
- let bb = select(b, vec4f(a2.rgb, b.a), flick);
10037
- return vec4f(a.rgb * a.a + bb.rgb * bb.a * (1.0 - a.a), a.a + bb.a * (1.0 - a.a));
10038
- }
10039
10444
  case 1u: {
10040
- if (flick) { return pm(a); }
10041
10445
  let s = max(u.wipe.y, 0.5);
10042
10446
  let m = smoothstep(u.wipe.x - s, u.wipe.x + s, p.x);
10043
10447
  return pm(vec4f(mix(a.rgb, b.rgb, m), mix(a.a, b.a, m)));
10044
10448
  }
10045
10449
  case 2u: {
10046
- if (flick) { return pm(a); }
10047
10450
  let d = abs(a.rgb - b.rgb);
10048
10451
  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);
10049
10452
  let c = mix(vec3f(luma(b.rgb) * 0.35), u.accent.rgb, h);
@@ -10051,8 +10454,8 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
10051
10454
  }
10052
10455
  default: {}
10053
10456
  }
10054
- // Flicker: After, or Before while X is held or on the flicker's other beat.
10055
- return pm(select(b, a, flick || u.wipe.w > 0.5));
10457
+ // Side by side: each side in its own place, Before over After where they meet.
10458
+ return vec4f(a.rgb * a.a + b.rgb * b.a * (1.0 - a.a), a.a + b.a * (1.0 - a.a));
10056
10459
  }
10057
10460
 
10058
10461
  @fragment fn fs(@builtin(position) q: vec4f) -> @location(0) vec4f {
@@ -10070,16 +10473,14 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
10070
10473
  }
10071
10474
  let a = takeA(p, u.ra, u.ca, near);
10072
10475
  let b = takeB(p, u.rb, u.cb, near);
10073
- // Before in After's place (X held, side by side): After's rect, as if Before were zoomed there.
10074
- let a2 = takeA(p, u.rb, u.cb, near);
10075
- var o = mix(composite(u32(u.modes.x), p, a, b, a2), composite(u32(u.modes.y), p, a, b, a2), u.modes.z);
10476
+ var o = mix(composite(u32(u.modes.x), p, a, b), composite(u32(u.modes.y), p, a, b), u.modes.z);
10076
10477
  if (near) { o = o + vec4f(0.05, 0.05, 0.06, 1.0) * (1.0 - o.a); }
10077
10478
  o = mix(o, vec4f(0.92), ring * 0.9);
10078
10479
  return o;
10079
10480
  }
10080
10481
  `, TEXTURE_BINDING = 4, COPY_DST = 2, RENDER_ATTACHMENT = 16, shared = null, lostListeners;
10081
10482
  var init_compare_gpu = __esm(() => {
10082
- MODE_INDEX = { side: 0, wipe: 1, difference: 2, flicker: 3 };
10483
+ MODE_INDEX = { side: 0, wipe: 1, difference: 2 };
10083
10484
  lostListeners = new Set;
10084
10485
  });
10085
10486
 
@@ -10465,7 +10866,9 @@ function compare(container, options) {
10465
10866
  ...options.theme ? { theme: options.theme } : {},
10466
10867
  ...options.appearance ? { appearance: options.appearance } : {}
10467
10868
  };
10468
- let mode = options.mode ?? "side";
10869
+ const offered = compareModes(options.modes, options.mode);
10870
+ const modeList = offered.modes;
10871
+ let mode = offered.mode;
10469
10872
  let wipe = Math.min(1, Math.max(0, options.wipe ?? 0.5));
10470
10873
  let loupeOn = options.loupe ?? false;
10471
10874
  let threshold = options.threshold ?? 4 / 255;
@@ -10483,7 +10886,7 @@ function compare(container, options) {
10483
10886
  root.setAttribute("role", "group");
10484
10887
  root.setAttribute("aria-roledescription", "compare");
10485
10888
  root.setAttribute("aria-label", options.label ?? "Compare");
10486
- root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight , . 1 2 3 4 S W D X Z L 0 = -");
10889
+ root.setAttribute("aria-keyshortcuts", `Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight , . ${modeList.map((m, i) => `${i + 1} ${modeKey(m)}`).join(" ")} Z L 0 = -`);
10487
10890
  applyLook2(root, options);
10488
10891
  root.dataset.mode = mode;
10489
10892
  if (auto)
@@ -10492,35 +10895,37 @@ function compare(container, options) {
10492
10895
  const modes = el("div", "knobs-cmp-modes");
10493
10896
  modes.setAttribute("role", "radiogroup");
10494
10897
  modes.setAttribute("aria-label", "Mode");
10495
- const modeBtns = MODES2.map((m, i) => {
10898
+ modes.hidden = modeList.length < 2;
10899
+ const modeBtns = modeList.map((m, i) => {
10496
10900
  const b = el("button", "knobs-cmp-mode");
10497
10901
  b.type = "button";
10498
10902
  b.tabIndex = -1;
10499
- b.textContent = m.label;
10500
- b.title = `${m.label} · ${i + 1}${m.key ? ` or ${m.key}` : " (hold X to flick)"}`;
10903
+ b.textContent = MODE_LABEL[m].short;
10904
+ b.title = `${MODE_LABEL[m].long} · ${i + 1} or ${modeKey(m)}`;
10501
10905
  b.setAttribute("role", "radio");
10502
- b.setAttribute("aria-keyshortcuts", m.key ? `${i + 1} ${m.key}` : `${i + 1}`);
10906
+ b.setAttribute("aria-keyshortcuts", `${i + 1} ${modeKey(m)}`);
10503
10907
  b.addEventListener("click", () => {
10504
- setMode(m.mode);
10908
+ setMode(m);
10505
10909
  root.focus({ preventScroll: true });
10506
10910
  });
10507
10911
  modes.append(b);
10508
10912
  return b;
10509
10913
  });
10510
- const loupeBtn = el("button", "knobs-cmp-toggle");
10511
- loupeBtn.type = "button";
10512
- loupeBtn.tabIndex = -1;
10513
- loupeBtn.textContent = "Loupe";
10514
- loupeBtn.title = "Loupe · hold Z";
10914
+ const toggleBtn = (name, label, title) => {
10915
+ const b = el("button", "knobs-cmp-toggle");
10916
+ b.type = "button";
10917
+ b.tabIndex = -1;
10918
+ b.innerHTML = icon(name);
10919
+ b.setAttribute("aria-label", label);
10920
+ b.title = title;
10921
+ return b;
10922
+ };
10923
+ const loupeBtn = toggleBtn("magnifyingGlass", "Loupe", "Loupe · hold Z");
10515
10924
  loupeBtn.addEventListener("click", () => {
10516
10925
  setLoupe(!loupeOn);
10517
10926
  root.focus({ preventScroll: true });
10518
10927
  });
10519
- const loopBtn = el("button", "knobs-cmp-toggle");
10520
- loopBtn.type = "button";
10521
- loopBtn.tabIndex = -1;
10522
- loopBtn.textContent = "Loop";
10523
- loopBtn.title = "Loop · L (drag its ends on the timeline)";
10928
+ const loopBtn = toggleBtn("repeat", "Loop", "Loop · L (drag its ends on the timeline)");
10524
10929
  loopBtn.setAttribute("aria-keyshortcuts", "L");
10525
10930
  loopBtn.addEventListener("click", () => {
10526
10931
  api.setLoop(!loopNow());
@@ -10537,7 +10942,8 @@ function compare(container, options) {
10537
10942
  const zoomReset = el("button", "knobs-cmp-zoom-reset");
10538
10943
  zoomReset.type = "button";
10539
10944
  zoomReset.tabIndex = -1;
10540
- zoomReset.textContent = "Reset";
10945
+ zoomReset.innerHTML = icon("arrowCounterClockwise", { small: true });
10946
+ zoomReset.setAttribute("aria-label", "Reset zoom");
10541
10947
  zoomReset.title = "Fit · 0 or double-click";
10542
10948
  zoomReset.addEventListener("click", () => {
10543
10949
  api.setZoom(null);
@@ -10552,6 +10958,9 @@ function compare(container, options) {
10552
10958
  handle.setAttribute("aria-label", "Wipe");
10553
10959
  handle.setAttribute("aria-valuemin", "0");
10554
10960
  handle.setAttribute("aria-valuemax", "100");
10961
+ const grip = el("span", "knobs-cmp-grip");
10962
+ grip.setAttribute("aria-hidden", "true");
10963
+ handle.append(grip);
10555
10964
  const note = el("div", "knobs-cmp-note");
10556
10965
  stage.append(canvas, split, labels[0], labels[1], handle, zoomPill, note);
10557
10966
  root.append(bar, stage);
@@ -10568,6 +10977,7 @@ function compare(container, options) {
10568
10977
  root.append(host);
10569
10978
  tl = timeline(host, {
10570
10979
  ...look,
10980
+ variant: "lane",
10571
10981
  duration: 1,
10572
10982
  fps: fps(),
10573
10983
  arrows: { step: 0.5, shift: 2 },
@@ -10586,9 +10996,9 @@ function compare(container, options) {
10586
10996
  const fade = new Spring(1, FADE6.omega, FADE6.zeta, 0.001);
10587
10997
  let fromMode = mode;
10588
10998
  let toMode = mode;
10589
- let phase = false;
10590
- let flickerTimer = 0;
10591
- let flickHeld = false;
10999
+ const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
11000
+ let wipeCatching = false;
11001
+ let gripY = null;
10592
11002
  let loupeHeld = false;
10593
11003
  let pointer = null;
10594
11004
  let drag = null;
@@ -10906,25 +11316,16 @@ function compare(container, options) {
10906
11316
  gpu = g;
10907
11317
  setRenderer("gpu");
10908
11318
  };
10909
- const syncFlicker = () => {
10910
- win.clearInterval(flickerTimer);
10911
- flickerTimer = 0;
10912
- phase = false;
10913
- if (mode === "flicker")
10914
- flickerTimer = win.setInterval(() => {
10915
- phase = !phase;
10916
- requestDraw();
10917
- }, FLICKER_MS);
10918
- };
10919
11319
  const syncModeUi = () => {
10920
11320
  root.dataset.mode = mode;
10921
- MODES2.forEach((m, i) => {
10922
- modeBtns[i].setAttribute("aria-checked", String(m.mode === mode));
11321
+ modeList.forEach((m, i) => {
11322
+ modeBtns[i].setAttribute("aria-checked", String(m === mode));
10923
11323
  });
10924
11324
  handle.tabIndex = mode === "wipe" ? 0 : -1;
11325
+ modePill?.update();
10925
11326
  };
10926
11327
  const setMode = (next, emit = true) => {
10927
- if (next === mode)
11328
+ if (next === mode || !modeList.includes(next))
10928
11329
  return;
10929
11330
  mode = next;
10930
11331
  const reduced = reducedMotion(win);
@@ -10940,19 +11341,29 @@ function compare(container, options) {
10940
11341
  else
10941
11342
  lay.set(next === "side" ? 0 : 1);
10942
11343
  syncModeUi();
10943
- syncFlicker();
10944
11344
  if (emit)
10945
11345
  options.onMode?.(next);
10946
11346
  requestDraw();
10947
11347
  };
10948
- const setWipe = (next, emit = true) => {
11348
+ const setWipe = (next, emit = true, jump = false) => {
10949
11349
  const w = Math.min(1, Math.max(0, next));
10950
- if (w === wipe)
11350
+ if (w === wipe && !jump)
10951
11351
  return;
10952
11352
  wipe = w;
11353
+ const minGap = 2 / Math.max(1, wipeRect(layoutNow().L.overlay).w);
11354
+ const head = moveHead({ x: wipeLine.x, target: wipeLine.target }, w, jump && !reducedMotion(win) ? "jump" : "follow", wipeCatching, minGap);
11355
+ wipeCatching = head.catching;
11356
+ if (head.catching) {
11357
+ wipeLine.x = head.x;
11358
+ wipeLine.target = head.target;
11359
+ } else
11360
+ wipeLine.snap(head.x);
10953
11361
  if (emit)
10954
11362
  options.onWipe?.(w);
10955
- drawNow();
11363
+ if (head.catching)
11364
+ requestDraw();
11365
+ else
11366
+ drawNow();
10956
11367
  };
10957
11368
  const setLoupe = (on) => {
10958
11369
  loupeOn = on;
@@ -11105,28 +11516,44 @@ function compare(container, options) {
11105
11516
  viewSent = key;
11106
11517
  options.onView({ zoom: clampZoom(z), loop: l });
11107
11518
  };
11519
+ const labelsIn = (m, r, wr, wx) => {
11520
+ if (m === "wipe") {
11521
+ const at = seamLabels(wx, wr.x, wr.x + wr.w, labelW[0], labelW[1], INSET);
11522
+ return [at.before, at.after];
11523
+ }
11524
+ const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * lay.x;
11525
+ return [
11526
+ { x: r.before.x + INSET, opacity: 1 },
11527
+ { x: ax, opacity: 1 }
11528
+ ];
11529
+ };
11530
+ const placeGrip = (wr) => {
11531
+ const gy = gripTop(gripY ?? wr.y + wr.h / 2, wr, GRIP.h) - wr.y;
11532
+ grip.style.transform = `translate3d(0,${gy.toFixed(2)}px,0)`;
11533
+ };
11108
11534
  const draw = () => {
11109
11535
  dirty = false;
11110
11536
  const r = layoutNow();
11111
11537
  const wr = wipeRect(r.L.overlay);
11112
- const wx = wipeX(wr, wipe);
11538
+ const wx = wipeX(wr, wipeLine.x);
11113
11539
  const k = lay.x;
11114
- const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * k;
11115
- labels[0].style.transform = `translate(${(r.before.x + INSET).toFixed(2)}px,${(r.before.y + INSET).toFixed(2)}px)`;
11116
- labels[1].style.transform = `translate(${ax.toFixed(2)}px,${(r.after.y + INSET).toFixed(2)}px)`;
11117
- const showsBefore = flickHeld || mode === "flicker" && phase;
11118
- const flickering = flickHeld || mode === "flicker";
11119
- labels[0].toggleAttribute("data-lit", flickering && showsBefore);
11120
- labels[1].toggleAttribute("data-lit", flickering && !showsBefore);
11121
- labels[0].toggleAttribute("data-dim", flickering && !showsBefore || mode === "wipe" && wipe < 0.02);
11122
- labels[1].toggleAttribute("data-dim", flickering && showsBefore || mode === "wipe" && wipe > 0.98);
11540
+ const a = labelsIn(fromMode, r, wr, wx);
11541
+ const b = labelsIn(toMode, r, wr, wx);
11542
+ const f = Math.min(1, Math.max(0, fade.x));
11543
+ const ys = [r.before.y + INSET, r.after.y + INSET];
11544
+ for (const i of [0, 1]) {
11545
+ const x = a[i].x + (b[i].x - a[i].x) * f;
11546
+ labels[i].style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
11547
+ labels[i].style.opacity = (a[i].opacity + (b[i].opacity - a[i].opacity) * f).toFixed(3);
11548
+ }
11123
11549
  const top = Math.min(r.L.side.before.y, r.L.side.after.y);
11124
11550
  split.style.transform = `translate(${r.L.side.split.toFixed(2)}px,${top.toFixed(2)}px)`;
11125
11551
  split.style.height = `${Math.max(0, r.L.side.after.h).toFixed(2)}px`;
11126
11552
  split.style.opacity = String(Math.max(0, 1 - k * 2));
11127
- handle.toggleAttribute("data-on", mode === "wipe" && !flickHeld);
11553
+ handle.toggleAttribute("data-on", mode === "wipe");
11128
11554
  handle.style.transform = `translate(${wx.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
11129
11555
  handle.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
11556
+ placeGrip(wr);
11130
11557
  handle.setAttribute("aria-valuenow", String(Math.round(wipe * 100)));
11131
11558
  handle.setAttribute("aria-valuetext", `${Math.round(wipe * 100)}% Before`);
11132
11559
  const pct = `${Math.round(100 / zw.x)}%`;
@@ -11149,7 +11576,7 @@ function compare(container, options) {
11149
11576
  const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, cutAt));
11150
11577
  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)` : "";
11151
11578
  };
11152
- const cut = mode === "wipe" && !flickHeld ? wx : Number.POSITIVE_INFINITY;
11579
+ const cut = mode === "wipe" ? wx : Number.POSITIVE_INFINITY;
11153
11580
  for (const s of sides) {
11154
11581
  const rect = s.index === 0 ? r.zBefore : r.zAfter;
11155
11582
  const clip = s.index === 0 ? r.before : r.after;
@@ -11158,10 +11585,8 @@ function compare(container, options) {
11158
11585
  place(s.preview, rect, clip, at);
11159
11586
  if (s.index !== 0)
11160
11587
  continue;
11161
- for (const n of [s.el, s.preview]) {
11162
- n.style.mixBlendMode = mode === "difference" && !flickHeld ? "difference" : "";
11163
- n.style.opacity = mode === "flicker" && !showsBefore ? "0" : "";
11164
- }
11588
+ for (const n of [s.el, s.preview])
11589
+ n.style.mixBlendMode = mode === "difference" ? "difference" : "";
11165
11590
  }
11166
11591
  return;
11167
11592
  }
@@ -11182,8 +11607,6 @@ function compare(container, options) {
11182
11607
  from: MODE_INDEX[fromMode],
11183
11608
  to: MODE_INDEX[toMode],
11184
11609
  mix: Math.min(1, Math.max(0, fade.x)),
11185
- flick: flickHeld,
11186
- phase,
11187
11610
  threshold,
11188
11611
  gain: 1,
11189
11612
  accent,
@@ -11199,6 +11622,11 @@ function compare(container, options) {
11199
11622
  const loop = frameLoop(win, (dt) => {
11200
11623
  lay.step(dt);
11201
11624
  fade.step(dt);
11625
+ if (wipeCatching) {
11626
+ wipeLine.step(dt);
11627
+ if (wipeLine.resting)
11628
+ wipeCatching = false;
11629
+ }
11202
11630
  zu.step(dt);
11203
11631
  zv.step(dt);
11204
11632
  zw.step(dt);
@@ -11232,7 +11660,7 @@ function compare(container, options) {
11232
11660
  pushFrame(s);
11233
11661
  }
11234
11662
  }
11235
- const moving = !lay.resting || !fade.resting || !zoomResting();
11663
+ const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching;
11236
11664
  if (moving)
11237
11665
  dirty = true;
11238
11666
  if (auto && !lay.resting)
@@ -11375,16 +11803,19 @@ function compare(container, options) {
11375
11803
  e.preventDefault();
11376
11804
  return startPinch();
11377
11805
  }
11378
- if (mode === "wipe" && !flickHeld) {
11806
+ if (mode === "wipe") {
11379
11807
  const wr = wipeRect(layoutNow().L.overlay);
11380
11808
  const inside = p.x >= wr.x - SLOP && p.x <= wr.x + wr.w + SLOP && p.y >= wr.y && p.y <= wr.y + wr.h;
11381
- const grab = wipeGrab(p.x, wr, wipe, SLOP);
11809
+ const grab = wipeGrab(p.x, wr, wipeLine.x, SLOP);
11382
11810
  if (inside && (grab.onLine || !zoomed())) {
11383
11811
  drag = { kind: "wipe", offset: grab.offset };
11384
11812
  root.dataset.dragging = "";
11813
+ gripY = p.y;
11385
11814
  e.preventDefault();
11386
11815
  if (!grab.onLine)
11387
- setWipe(wipeAt(p.x, wr));
11816
+ setWipe(wipeAt(p.x, wr), true, true);
11817
+ else
11818
+ drawNow();
11388
11819
  return;
11389
11820
  }
11390
11821
  }
@@ -11410,6 +11841,11 @@ function compare(container, options) {
11410
11841
  pinch = { d, x: mid.x, y: mid.y };
11411
11842
  return handZoom(zoomAbout(panned, k, q.pu, q.pv));
11412
11843
  }
11844
+ if (mode === "wipe" && drag?.kind !== "pan") {
11845
+ gripY = p.y;
11846
+ if (!drag)
11847
+ placeGrip(wipeRect(layoutNow().L.overlay));
11848
+ }
11413
11849
  if (drag?.kind === "wipe")
11414
11850
  setWipe(wipeAt(p.x, wipeRect(layoutNow().L.overlay), drag.offset));
11415
11851
  else if (drag?.kind === "pan") {
@@ -11482,12 +11918,6 @@ function compare(container, options) {
11482
11918
  const z = { u: zu.target, v: zv.target, w: zw.target, h: zw.target };
11483
11919
  glideZoom(zoomAbout(z, factor, 0.5, 0.5));
11484
11920
  };
11485
- const holdFlick = (on) => {
11486
- if (flickHeld === on)
11487
- return;
11488
- flickHeld = on;
11489
- drawNow();
11490
- };
11491
11921
  const holdLoupe = (on) => {
11492
11922
  if (loupeHeld === on)
11493
11923
  return;
@@ -11533,25 +11963,17 @@ function compare(container, options) {
11533
11963
  case "s":
11534
11964
  case "w":
11535
11965
  case "d":
11536
- if (e.altKey || e.shiftKey || key === "s" && claimsHintKey(doc, key))
11537
- return;
11538
- claim();
11539
- return setMode(KEY_MODE[key]);
11540
11966
  case "1":
11541
11967
  case "2":
11542
- case "3":
11543
- case "4":
11544
- if (e.altKey || e.shiftKey)
11968
+ case "3": {
11969
+ if (e.altKey || e.shiftKey || key === "s" && claimsHintKey(doc, key))
11545
11970
  return;
11546
- claim();
11547
- return setMode(MODES2[Number(key) - 1].mode);
11548
- case "x":
11549
- if (e.altKey)
11971
+ const m = modeForKey(key, modeList);
11972
+ if (!m)
11550
11973
  return;
11551
11974
  claim();
11552
- if (!e.repeat)
11553
- holdFlick(true);
11554
- return;
11975
+ return setMode(m);
11976
+ }
11555
11977
  case "z":
11556
11978
  if (e.altKey)
11557
11979
  return;
@@ -11607,8 +12029,11 @@ function compare(container, options) {
11607
12029
  labels[1].textContent = labelOf(1);
11608
12030
  loupeBtn.setAttribute("aria-pressed", String(loupeOn));
11609
12031
  loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
12032
+ const modePill = modeList.length > 1 ? selectionPill(modes, {
12033
+ rows: () => modeBtns,
12034
+ selected: () => modeBtns[modeList.indexOf(mode)] ?? null
12035
+ }) : null;
11610
12036
  syncModeUi();
11611
- syncFlicker();
11612
12037
  const unlisten = tl?.listen({
11613
12038
  onSeek: (t, source) => {
11614
12039
  if (source === "play") {
@@ -11666,24 +12091,20 @@ function compare(container, options) {
11666
12091
  });
11667
12092
  root.addEventListener("keydown", onKey);
11668
12093
  root.addEventListener("keyup", (e) => {
11669
- const key = e.key.toLowerCase();
11670
- if (key === "x")
11671
- holdFlick(false);
11672
- else if (key === "z")
12094
+ if (e.key.toLowerCase() === "z")
11673
12095
  holdLoupe(false);
11674
12096
  });
11675
12097
  root.addEventListener("focusout", (e) => {
11676
12098
  if (root.contains(e.relatedTarget))
11677
12099
  return;
11678
- holdFlick(false);
11679
12100
  holdLoupe(false);
11680
12101
  });
11681
12102
  const unregisterHints = options.hints === false ? undefined : hints.register(root, () => [
11682
- ...MODES2.map((m, i) => ({
12103
+ ...modeList.length > 1 ? modeList.map((m, i) => ({
11683
12104
  el: modeBtns[i],
11684
- label: m.label,
11685
- activate: () => setMode(m.mode)
11686
- })),
12105
+ label: MODE_LABEL[m].long,
12106
+ activate: () => setMode(m)
12107
+ })) : [],
11687
12108
  { el: loopBtn, label: "Loop", activate: () => api.setLoop(!loopNow()) },
11688
12109
  ...loupeBtn.hidden ? [] : [{ el: loupeBtn, label: "Loupe", activate: () => setLoupe(!loupeOn) }],
11689
12110
  ...zoomed() ? [{ el: zoomReset, label: "Reset zoom", activate: () => api.setZoom(null) }] : [],
@@ -11708,6 +12129,7 @@ function compare(container, options) {
11708
12129
  get mode() {
11709
12130
  return mode;
11710
12131
  },
12132
+ modes: modeList,
11711
12133
  get wipe() {
11712
12134
  return wipe;
11713
12135
  },
@@ -11763,7 +12185,7 @@ function compare(container, options) {
11763
12185
  tl?.update({ fps: next.fps });
11764
12186
  requestDraw();
11765
12187
  },
11766
- seek: (t) => tl ? tl.seek(t) : ownSeek(t),
12188
+ seek: (t, o) => tl ? tl.seek(t, o?.glide ? { glide: true } : undefined) : ownSeek(t),
11767
12189
  seekBy(d) {
11768
12190
  const total = duration || tl?.duration || 0;
11769
12191
  const t = Math.min(total, Math.max(0, now() + d));
@@ -11804,7 +12226,7 @@ function compare(container, options) {
11804
12226
  dispose() {
11805
12227
  disposed = true;
11806
12228
  loop.stop();
11807
- win.clearInterval(flickerTimer);
12229
+ modePill?.dispose();
11808
12230
  win.clearTimeout(releaseTimer);
11809
12231
  ro.disconnect();
11810
12232
  nearIo?.disconnect();
@@ -11834,7 +12256,7 @@ function compare(container, options) {
11834
12256
  wake();
11835
12257
  return api;
11836
12258
  }
11837
- var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, FLICKER_MS = 400, SLOP = 12, INSET = 8, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODES2, KEY_MODE;
12259
+ var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
11838
12260
  var init_compare = __esm(() => {
11839
12261
  init_motion();
11840
12262
  init_page_hints();
@@ -11842,21 +12264,24 @@ var init_compare = __esm(() => {
11842
12264
  init_compare_gpu();
11843
12265
  init_compare_model();
11844
12266
  init_compare_strip();
12267
+ init_icons();
11845
12268
  init_look();
11846
12269
  init_rgba();
12270
+ init_selection_pill();
11847
12271
  init_timeline();
12272
+ init_timeline_model();
11848
12273
  LAYOUT2 = springMotion({ duration: 0.35, bounce: 0 });
11849
12274
  FADE6 = springMotion(SPRINGS.snappy);
11850
12275
  FADE_REDUCED = springMotion(FADE);
11851
12276
  ZOOM = springMotion({ duration: 0.35, bounce: 0 });
12277
+ WIPE_CATCH = springMotion(CATCH_UP);
12278
+ GRIP = { w: 16, h: 28 };
11852
12279
  LOUPE = { r: 88, zoom: 4 };
11853
- MODES2 = [
11854
- { mode: "side", label: "Side by side", key: "S" },
11855
- { mode: "wipe", label: "Wipe", key: "W" },
11856
- { mode: "difference", label: "Difference", key: "D" },
11857
- { mode: "flicker", label: "Flicker", key: "" }
11858
- ];
11859
- KEY_MODE = { s: "side", w: "wipe", d: "difference" };
12280
+ MODE_LABEL = {
12281
+ side: { short: "Side", long: "Side by side" },
12282
+ wipe: { short: "Wipe", long: "Wipe" },
12283
+ difference: { short: "Difference", long: "Difference" }
12284
+ };
11860
12285
  });
11861
12286
 
11862
12287
  // src/ui/solid.ts
@@ -14757,6 +15182,7 @@ init_diff();
14757
15182
  // src/ui/picker.ts
14758
15183
  init_motion();
14759
15184
  init_look();
15185
+ init_icons();
14760
15186
 
14761
15187
  // src/ui/picker-model.ts
14762
15188
  function digitPick(ids, key) {
@@ -14811,7 +15237,7 @@ var CLOSE3 = springMotion(CLOSE);
14811
15237
  var CARD = springMotion(SPRINGS.default);
14812
15238
  var FADE5 = springMotion(FADE);
14813
15239
  var DRIFT = 6;
14814
- var CHECK = svg('<path d="m3.5 8.4 3 3 6-6.6"/>');
15240
+ var CHECK = icon("check", { small: true });
14815
15241
  function optionPicker(container, options) {
14816
15242
  const doc = container.ownerDocument;
14817
15243
  const win = doc.defaultView;
@@ -17667,7 +18093,7 @@ function mountTable(host, m, steps) {
17667
18093
  context: () => ({ kind: "table" })
17668
18094
  };
17669
18095
  }
17670
- var grid = (v) => Math.round(v * 64) / 64;
18096
+ var grid2 = (v) => Math.round(v * 64) / 64;
17671
18097
  function chapterPills(host, chapters, io) {
17672
18098
  const doc = host.ownerDocument;
17673
18099
  const win = doc.defaultView;
@@ -17695,9 +18121,9 @@ function chapterPills(host, chapters, io) {
17695
18121
  const y = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
17696
18122
  const w = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
17697
18123
  const draw = () => {
17698
- const left = grid(x.x);
17699
- mark.style.transform = `translate(${left}px,${grid(y.x)}px)`;
17700
- mark.style.width = `${grid(x.x + w.x) - left}px`;
18124
+ const left = grid2(x.x);
18125
+ mark.style.transform = `translate(${left}px,${grid2(y.x)}px)`;
18126
+ mark.style.width = `${grid2(x.x + w.x) - left}px`;
17701
18127
  };
17702
18128
  const loop = frameLoop(win, (dt) => {
17703
18129
  x.step(dt);
@@ -18301,7 +18727,7 @@ var lookOf2 = (o) => ({
18301
18727
  ...o.preset ? { preset: o.preset } : {},
18302
18728
  ...o.theme ? { theme: o.theme } : {}
18303
18729
  });
18304
- var grid2 = (v) => Math.round(v * 64) / 64;
18730
+ var grid3 = (v) => Math.round(v * 64) / 64;
18305
18731
  var ICON = {
18306
18732
  full: svg('<path d="M3 6.5V3h3.5M13 6.5V3H9.5M3 9.5V13h3.5M13 9.5V13H9.5"/>'),
18307
18733
  exit: svg('<path d="M6.5 3v3.5H3M9.5 3v3.5H13M6.5 13V9.5H3M9.5 13V9.5H13"/>'),
@@ -18481,8 +18907,8 @@ function walkthrough(container, options) {
18481
18907
  if (mark) {
18482
18908
  pillY.step(dt);
18483
18909
  pillH.step(dt);
18484
- mark.style.transform = `translateY(${grid2(pillY.x)}px)`;
18485
- mark.style.height = `${grid2(pillY.x + pillH.x) - grid2(pillY.x)}px`;
18910
+ mark.style.transform = `translateY(${grid3(pillY.x)}px)`;
18911
+ mark.style.height = `${grid3(pillY.x + pillH.x) - grid3(pillY.x)}px`;
18486
18912
  more ||= !pillY.resting || !pillH.resting;
18487
18913
  }
18488
18914
  if (scroll.to && scroll.el) {
@@ -18642,10 +19068,10 @@ function walkthrough(container, options) {
18642
19068
  const a = f.hold.getBoundingClientRect();
18643
19069
  const b = fullRect();
18644
19070
  const k = reduced() ? Math.round(f.p.target) : f.p.x;
18645
- const left = grid2(a.left + (b.x - a.left) * k);
18646
- const top = grid2(a.top + (b.y - a.top) * k);
18647
- const right = grid2(a.right + (b.x + b.w - a.right) * k);
18648
- const bottom = grid2(a.bottom + (b.y + b.h - a.bottom) * k);
19071
+ const left = grid3(a.left + (b.x - a.left) * k);
19072
+ const top = grid3(a.top + (b.y - a.top) * k);
19073
+ const right = grid3(a.right + (b.x + b.w - a.right) * k);
19074
+ const bottom = grid3(a.bottom + (b.y + b.h - a.bottom) * k);
18649
19075
  const s = f.v.card.style;
18650
19076
  s.left = `${left}px`;
18651
19077
  s.top = `${top}px`;