@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-react.js CHANGED
@@ -571,12 +571,20 @@ var compareCss = `
571
571
  display:flex;flex-direction:column;gap:6px;width:100%;height:100%;min-height:160px;outline:none;user-select:none;-webkit-user-select:none;
572
572
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
573
573
  :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}
574
- .knobs-cmp-bar{flex:none;display:flex;align-items:center;gap:2px;height:24px;padding:0 2px}
575
- .knobs-cmp-modes{display:flex;align-items:center;gap:2px}
576
- .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;
577
- transition:color .15s,background .15s}
578
- .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)}
579
- .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)}
574
+ .knobs-cmp-bar{flex:none;display:flex;align-items:center;gap:var(--_k-gap);height:var(--_k-control-h)}
575
+ /* The modes: one fill, the selection pill on the one picked; a mode isn't brighter for being
576
+ picked: the pill's bright copy is. */
577
+ .knobs-cmp-modes{position:relative;display:inline-flex;height:var(--_k-control-h);border-radius:var(--_k-radius);background:var(--_k-fill)}
578
+ .knobs-cmp-modes[hidden]{display:none}
579
+ .knobs-ui .knobs-cmp-mode{display:inline-flex;align-items:center;height:100%;padding:0 8px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:pointer;white-space:nowrap;
580
+ transition:color calc(var(--_k-hover-out) * 1ms)}
581
+ .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible){color:var(--_k-fg)}
582
+ /* Toggles: icon buttons, the accent's wash when on. */
583
+ .knobs-ui .knobs-cmp-toggle{display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:var(--_k-radius);color:var(--_k-fg-3);cursor:pointer;
584
+ transition:color calc(var(--_k-hover-out) * 1ms),background calc(var(--_k-hover-out) * 1ms)}
585
+ .knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash)}
586
+ .knobs-ui .knobs-cmp-toggle[aria-pressed="true"]{color:var(--_k-accent-fg);background:var(--_k-accent-soft)}
587
+ .knobs-cmp-toggle svg{display:block}
580
588
  .knobs-cmp-fill{flex:1}
581
589
  .knobs-cmp-toggle[hidden]{display:none}
582
590
  /* One surface: on a card (a walkthrough's step, a split's pane, anything .knobs-surface) or marked
@@ -591,7 +599,8 @@ var compareCss = `
591
599
  /* Unzoomed, a finger still scrolls the page up and down; zoomed, the stage takes every gesture. */
592
600
  .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:grab;touch-action:none}
593
601
  .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:grabbing!important}
594
- .knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:ew-resize!important}
602
+ /* Wiping, the grip stands in for the cursor. */
603
+ .knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:none!important}
595
604
  .knobs-cmp-sizer{position:absolute;left:0;right:0;top:0;height:0;visibility:hidden;pointer-events:none}
596
605
  .knobs-cmp-canvas{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
597
606
  .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}
@@ -599,18 +608,21 @@ var compareCss = `
599
608
  /* Glass: what's behind shows through, blurred, on the knobs tokens. */
600
609
  .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);
601
610
  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)))}
602
- .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}
603
- .knobs-cmp-label[data-lit]{color:var(--_k-fg)}
604
- .knobs-cmp-label[data-dim]{color:var(--_k-fg-3)}
605
- /* The wipe: a hairline down the line with a grip at its middle; a wider strip takes the hand and
606
- the keys (a slider). It fades with the mode. */
611
+ .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)}
612
+ /* The wipe: a hairline down the line with a raised grip at the pointer's height; a wider strip
613
+ takes the hand and the keys (a slider). It fades with the mode; wiping, line and grip brighten. */
607
614
  .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)}
608
615
  .knobs-cmp-handle[data-on]{opacity:1;pointer-events:auto;transition:opacity .22s cubic-bezier(.2,.8,.2,1)}
609
- .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)}
610
- .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);
611
- 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}
612
- .knobs-compare[data-dragging] .knobs-cmp-handle::after{scale:1.12}
613
- .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)}
616
+ .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)}
617
+ .knobs-compare[data-dragging] .knobs-cmp-handle::before{background:var(--_ku-cmp-line)}
618
+ .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);
619
+ 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)}
620
+ .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)}
621
+ .knobs-cmp-grip::before{left:6px}
622
+ .knobs-cmp-grip::after{left:9px}
623
+ .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)}
624
+ .knobs-compare[data-dragging] .knobs-cmp-grip::before,.knobs-compare[data-dragging] .knobs-cmp-grip::after{background:var(--_k-fg)}
625
+ .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)}
614
626
  /* The split's hairline, down the middle side by side. */
615
627
  .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}
616
628
  /* Zoomed: how far, and a way back, in the stage's corner; it fades in and out, nothing shifts. */
@@ -619,12 +631,12 @@ var compareCss = `
619
631
  opacity:0;scale:.96;pointer-events:none;transition:opacity .16s cubic-bezier(.4,0,1,1),scale .16s cubic-bezier(.4,0,1,1)}
620
632
  .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)}
621
633
  :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;
622
- height:20px;padding:0 8px;border-radius:999px;background:transparent;color:var(--_k-fg-2);cursor:pointer;transition:color .15s,background .15s}
634
+ 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}
623
635
  :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)}
624
636
  .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}
625
637
  .knobs-cmp-note:empty{display:none}
626
638
  .knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
627
- .knobs-cmp-timeline-host{flex:none;height:56px;min-height:0}
639
+ .knobs-cmp-timeline-host{flex:none;min-height:0}
628
640
  /* The frame strip: Before over After per column, a difference bar under them, a dot where it peaks. */
629
641
  .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;
630
642
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg)}
@@ -767,7 +779,7 @@ var init_picker_css = __esm(() => {
767
779
  .knobs-picker-suggested{flex:none;color:var(--_k-fg-3);font-size:var(--_k-text-meta)}
768
780
  .knobs-picker-check{flex:none;display:flex;width:14px;height:14px;color:var(--_k-accent);opacity:0;scale:.6;filter:blur(2px);
769
781
  transition:opacity .11s ease-in,scale .11s ease-in,filter .11s ease-in}
770
- .knobs-picker-check svg{display:block;width:14px;height:14px}
782
+ .knobs-picker-check svg{display:block;width:12px;height:12px}
771
783
  .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}
772
784
  .knobs-picker-body{position:relative;overflow:visible}
773
785
  .knobs-picker-body[data-moving]{overflow:hidden}
@@ -871,6 +883,15 @@ var readoutCss = `
871
883
  @media(prefers-reduced-motion:reduce){.knobs-rd :where(*){transition:none!important}}
872
884
  `;
873
885
 
886
+ // src/ui/selection-pill-css.ts
887
+ var selectionPillCss = `
888
+ .knobs-pick-pill,.knobs-pick-copy{position:absolute;left:0;top:0;width:0;height:0;pointer-events:none;transition:opacity .1s linear}
889
+ .knobs-pick-pill{background:var(--_k-fill-selected);will-change:transform}
890
+ .knobs-pick-copy{overflow:hidden;will-change:transform}
891
+ .knobs-pick-copy-rows{position:absolute;left:0;top:0;width:0;height:0}
892
+ :is(.knobs-pick-pill,.knobs-pick-copy)[data-off]{opacity:0}
893
+ `;
894
+
874
895
  // src/tokens/color.ts
875
896
  function linearToOklch([r, g, b, a]) {
876
897
  const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
@@ -2719,11 +2740,9 @@ var css2 = `
2719
2740
  .knobs-tree:focus .knobs-tree-row[data-active][aria-selected="true"]::before{background:var(--_ku-select-focus)}
2720
2741
  .knobs-tree-row[data-drop="into"]::before{opacity:1;background:color-mix(in srgb,var(--_k-accent) 30%,transparent)}
2721
2742
  .knobs-tree-row[data-dragged]{opacity:.38}
2722
- /* glide: one highlight under the selected row that glides between rows (the row's own stays off). */
2723
- .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);
2724
- opacity:0;pointer-events:none;will-change:transform;transition:opacity .15s ease-out,background .12s}
2725
- .knobs-tree-cursor[data-on]{opacity:1}
2726
- .knobs-tree:focus .knobs-tree-cursor{background:var(--_ku-select-focus)}
2743
+ /* glide: the selection pill on the selected row, in the selection's colour (the row's own stays off). */
2744
+ .knobs-tree>.knobs-pick-pill{background:var(--_ku-select)}
2745
+ .knobs-tree:focus>.knobs-pick-pill{background:var(--_ku-select-focus)}
2727
2746
  .knobs-tree[data-glide] .knobs-tree-row[aria-selected="true"]::before{opacity:0!important}
2728
2747
  .knobs-tree-row>*{position:relative}
2729
2748
  .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)}
@@ -2755,22 +2774,20 @@ var css2 = `
2755
2774
  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)}
2756
2775
  .knobs-tree-empty{padding:10px var(--_k-inset);color:var(--_k-fg-3)}
2757
2776
 
2758
- /* Timeline: a transport (play, the time) beside a track: the ruler's canvas over a lane of
2759
- markers, ranges as soft bands, and the playhead, a hairline in the accent with a cap. */
2777
+ /* Timeline: play beside a track: the ruler's canvas over a lane of markers, ranges as soft bands,
2778
+ and the playhead, a hairline in the accent with a cap. No clock: the time shows under the
2779
+ pointer on the track (the tag), in seconds, while hovering or scrubbing. */
2760
2780
  :where(.knobs-timeline,.knobs-timeline *){box-sizing:border-box}
2761
2781
  .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;
2762
2782
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
2763
2783
  :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}
2764
- .knobs-tl-transport{flex:none;display:flex;align-items:center;gap:8px;min-width:96px}
2784
+ .knobs-tl-transport{flex:none;display:flex;align-items:center}
2765
2785
  .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);
2766
2786
  background:var(--_k-fill-selected);color:var(--_k-fg);cursor:pointer;transition:background .15s,transform .15s var(--_ku-ease)}
2767
2787
  .knobs-ui .knobs-tl-play:hover{background:color-mix(in srgb,var(--_k-fg) 16%,transparent)}
2768
2788
  .knobs-ui .knobs-tl-play:active{transform:scale(.94)}
2769
2789
  .knobs-tl-play svg{width:12px;height:12px}
2770
2790
  .knobs-timeline[data-playing] .knobs-tl-play{background:var(--_k-accent);color:var(--_k-bg)}
2771
- .knobs-tl-time{display:flex;flex-direction:column;line-height:1.2;font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}
2772
- .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}
2773
- .knobs-tl-sub{font-size:var(--_k-text-meta);color:var(--_k-fg-3);white-space:nowrap}
2774
2791
  .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;
2775
2792
  transition:background calc(var(--_k-hover-out) * 1ms)}
2776
2793
  .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)}
@@ -2794,7 +2811,8 @@ var css2 = `
2794
2811
  .knobs-tl-pin{position:absolute;top:13px;left:-5px;width:10px;height:10px;pointer-events:auto;cursor:grab}
2795
2812
  .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)}
2796
2813
  .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);
2797
- 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}
2814
+ font-size:var(--_k-text-meta);line-height:17px;white-space:nowrap;pointer-events:auto;cursor:grab;transition:background .15s,color .15s}
2815
+ .knobs-tl-mtext{display:block;overflow:hidden;white-space:nowrap}
2798
2816
  .knobs-tl-marker[data-selected]{--knobs-tl-c:var(--_k-accent)}
2799
2817
  .knobs-tl-marker[data-selected] .knobs-tl-mlabel{background:color-mix(in srgb,var(--_k-accent) 24%,var(--_k-fill-selected))}
2800
2818
  .knobs-tl-marker[data-selected] .knobs-tl-pin::after{scale:1.2}
@@ -2815,7 +2833,22 @@ var css2 = `
2815
2833
  .knobs-timeline[data-scrubbing] .knobs-tl-hover{opacity:0}
2816
2834
  @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
2817
2835
  @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
2818
- `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
2836
+ .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}
2837
+ .knobs-tl-tag[data-on]{opacity:1}
2838
+ /* The lane (variant: "lane"): one slim track of ticks under a compare or a video; markers are
2839
+ faint chapter ticks, the current one in text colour; nothing to edit. */
2840
+ .knobs-ui.knobs-timeline[data-variant="lane"]{height:auto;min-height:0;padding:0;gap:var(--_k-gap)}
2841
+ .knobs-timeline[data-variant="lane"] .knobs-tl-track{height:var(--_k-control-h)}
2842
+ .knobs-timeline[data-variant="lane"] .knobs-tl-ruler{height:100%}
2843
+ .knobs-timeline[data-variant="lane"] .knobs-tl-lane{top:0}
2844
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker{top:0}
2845
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker::before{top:0;bottom:50%;left:-.75px;width:1.5px;background:var(--_k-fg-3)}
2846
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker[data-current]::before{background:var(--_k-fg)}
2847
+ .knobs-timeline[data-variant="lane"] :is(.knobs-tl-pin,.knobs-tl-mlabel,.knobs-tl-rlabel){display:none}
2848
+ .knobs-timeline[data-variant="lane"] .knobs-tl-head::before{top:0}
2849
+ .knobs-timeline[data-variant="lane"] .knobs-tl-head::after{content:none}
2850
+ .knobs-timeline[data-variant="lane"] .knobs-tl-tag{top:2px;bottom:auto}
2851
+ `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + selectionPillCss) + rolling_css_default, lookDefaults, tune = (s, m) => {
2819
2852
  s.omega = m.omega;
2820
2853
  s.zeta = m.zeta;
2821
2854
  return s;
@@ -2973,33 +3006,29 @@ var init_diff_css = __esm(() => {
2973
3006
  .knobs-diff-fill{flex:1}
2974
3007
  .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}
2975
3008
  .knobs-diff-nav>span{min-width:3.6em;text-align:center}
2976
- :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;
3009
+ :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;
2977
3010
  margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
2978
- .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;
3011
+ .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;
2979
3012
  transition:background .15s,color .15s,opacity .15s}
2980
3013
  .knobs-ui .knobs-diff-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
2981
3014
  .knobs-ui .knobs-diff-btn[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
2982
3015
  .knobs-ui .knobs-diff-btn:disabled{opacity:.35;cursor:default;background:transparent}
2983
- .knobs-diff-btn svg{width:13px;height:13px}
3016
+ .knobs-diff-btn svg{width:16px;height:16px}
2984
3017
  /* Viewed: a check with its word, as on GitHub; the path dims once it's ticked. */
2985
3018
  .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);
2986
3019
  font-size:var(--_k-text-meta);font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:background .15s,color .15s}
2987
3020
  .knobs-ui .knobs-diff-viewed:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
2988
3021
  .knobs-ui .knobs-diff-viewed[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
2989
- .knobs-diff-viewed svg{width:13px;height:13px}
3022
+ .knobs-diff-viewed svg{width:16px;height:16px}
2990
3023
  .knobs-diff-viewed[aria-pressed="true"] svg{color:var(--kd-add)}
2991
3024
  .knobs-diff-path{transition:opacity .2s}
2992
3025
  .knobs-diff[data-viewed]>.knobs-diff-head .knobs-diff-path,.knobs-diff-part[data-viewed] .knobs-diff-path{opacity:.6}
2993
- /* Split | Unified: one pill that glides between the two, raised as a panel's segments are (a
2994
- lighter bg in dark, near white in light). */
2995
- .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)}
2996
- .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);
2997
- font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:color .15s}
2998
- .knobs-ui .knobs-diff-seg>button:is(:hover,:focus-visible),.knobs-ui .knobs-diff-seg>button[aria-checked="true"]{color:var(--_k-fg)}
2999
- .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));
3000
- box-shadow:0 1px 2px rgba(0,0,0,calc(.16 * var(--k-lift,1)));transition:transform var(--_ku-ease-ms) var(--_ku-ease)}
3001
- .knobs-diff-seg[data-value="unified"]>.knobs-diff-pill{transform:translateX(100%)}
3002
- .knobs-diff[data-frame="flat"] .knobs-diff-pill{box-shadow:none}
3026
+ /* Split | Unified: the selection pill on the one picked, its bright copy the picked one's text; a
3027
+ choice isn't brighter for being picked. Choices are styled by class, as the copy holds them too. */
3028
+ .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)}
3029
+ .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);
3030
+ font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:color calc(var(--_k-hover-out) * 1ms)}
3031
+ .knobs-ui .knobs-diff-choice:is(:hover,:focus-visible){color:var(--_k-fg)}
3003
3032
  /* The diff: one layer per file or layout on show, the old fading under the new. */
3004
3033
  .knobs-diff-body{position:relative;flex:1;min-height:0}
3005
3034
  .knobs-diff-probe{position:absolute;left:0;top:0;width:0;height:var(--kd-line);visibility:hidden;pointer-events:none}
@@ -3094,7 +3123,7 @@ var init_diff_css = __esm(() => {
3094
3123
  .knobs-ui .knobs-diff-filter>input::placeholder{color:var(--_k-fg-3)}
3095
3124
  .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}
3096
3125
  .knobs-ui .knobs-diff-filter>button:hover{background:var(--_ku-wash-hi);color:var(--_k-fg)}
3097
- .knobs-diff-filter>button svg{width:10px;height:10px}
3126
+ .knobs-diff-filter>button svg{width:12px;height:12px}
3098
3127
  .knobs-diff-list{position:relative;flex:1;min-height:0}
3099
3128
  .knobs-diff-files .knobs-diff-counts{font-size:var(--_k-text-meta);align-items:center}
3100
3129
  .knobs-diff-files .knobs-diff-counts{gap:5px}
@@ -3861,6 +3890,283 @@ var init_rgba = __esm(() => {
3861
3890
  cache = new Map;
3862
3891
  });
3863
3892
 
3893
+ // src/ui/icons.ts
3894
+ function icon(name, options = {}) {
3895
+ const size = options.small ? 12 : 16;
3896
+ 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>`;
3897
+ }
3898
+ var PATHS, ICON_NAMES;
3899
+ var init_icons = __esm(() => {
3900
+ PATHS = {
3901
+ 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",
3902
+ 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",
3903
+ 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",
3904
+ 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",
3905
+ 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",
3906
+ 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",
3907
+ 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",
3908
+ 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",
3909
+ 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",
3910
+ 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",
3911
+ 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",
3912
+ 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",
3913
+ 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",
3914
+ 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",
3915
+ 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",
3916
+ 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",
3917
+ 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",
3918
+ 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",
3919
+ 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",
3920
+ 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",
3921
+ 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",
3922
+ 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",
3923
+ 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",
3924
+ 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",
3925
+ 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",
3926
+ 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",
3927
+ 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"
3928
+ };
3929
+ ICON_NAMES = Object.keys(PATHS);
3930
+ });
3931
+
3932
+ // src/ui/selection-pill.ts
3933
+ function pillSpring(distance, base = SPRINGS.default) {
3934
+ return distance > 400 ? { duration: base.duration * 1.4, bounce: base.bounce } : base;
3935
+ }
3936
+ function pillBoxOf(row, container) {
3937
+ return {
3938
+ x: row.left - container.left - container.clientLeft + container.scrollLeft,
3939
+ y: row.top - container.top - container.clientTop + container.scrollTop,
3940
+ w: row.width,
3941
+ h: row.height
3942
+ };
3943
+ }
3944
+ function pillRadius(from, to, h, k) {
3945
+ const t = Math.abs(to.h - from.h) > 0.5 ? (h - from.h) / (to.h - from.h) : Number.isFinite(k) ? k : 1;
3946
+ return from.r + (to.r - from.r) * Math.min(1, Math.max(0, t));
3947
+ }
3948
+ function pillProgress(from, to, at) {
3949
+ const d = Math.hypot(to.x - from.x, to.y - from.y);
3950
+ if (d < 0.5)
3951
+ return 1;
3952
+ const p = ((at.x - from.x) * (to.x - from.x) + (at.y - from.y) * (to.y - from.y)) / (d * d);
3953
+ return Math.min(1, Math.max(0, p));
3954
+ }
3955
+ function planPill(state, next, flags = {}) {
3956
+ if (!next)
3957
+ return { kind: "hide" };
3958
+ if (!state.shown || !state.at || flags.first || flags.reduced || flags.snap)
3959
+ return { kind: "land", box: next };
3960
+ const t = state.target;
3961
+ if (t && t.x === next.x && t.y === next.y && t.w === next.w && t.h === next.h)
3962
+ return { kind: "stay" };
3963
+ const d = Math.hypot(next.x - state.at.x, next.y - state.at.y);
3964
+ return { kind: "travel", box: next, spring: pillSpring(d) };
3965
+ }
3966
+ function selectionPill(container, options) {
3967
+ const doc = container.ownerDocument;
3968
+ const win = doc.defaultView;
3969
+ injectUiStyles(doc);
3970
+ if (win.getComputedStyle(container).position === "static")
3971
+ container.style.position = "relative";
3972
+ const pill = doc.createElement("i");
3973
+ pill.className = "knobs-pick-pill";
3974
+ pill.setAttribute("aria-hidden", "true");
3975
+ pill.dataset.off = "";
3976
+ const copyOn = options.copy !== false;
3977
+ const copy = doc.createElement("div");
3978
+ copy.className = "knobs-pick-copy";
3979
+ copy.setAttribute("aria-hidden", "true");
3980
+ copy.inert = true;
3981
+ copy.dataset.off = "";
3982
+ const copyRows = doc.createElement("div");
3983
+ copyRows.className = "knobs-pick-copy-rows";
3984
+ copy.append(copyRows);
3985
+ container.prepend(pill);
3986
+ if (copyOn)
3987
+ container.append(copy);
3988
+ const { omega, zeta } = springMotion(SPRINGS.default);
3989
+ const springs = [0, 0, 0, 0].map(() => new Spring(0, omega, zeta, 0.01));
3990
+ const [sx, sy, sw, sh] = springs;
3991
+ let shown = false;
3992
+ let first = true;
3993
+ let target = null;
3994
+ let from = { x: 0, y: 0, w: 0, h: 0 };
3995
+ let fromR = 0;
3996
+ let toR = 0;
3997
+ let rowBoxes = [];
3998
+ const clones = new Map;
3999
+ let disposed = false;
4000
+ const radiusOf = (row) => options.radius?.(row) ?? (Number.parseFloat(win.getComputedStyle(row).borderTopLeftRadius) || 0);
4001
+ const rowsNow = () => options.rows ? [...options.rows()] : [...container.children].filter((n) => n !== pill && n !== copy);
4002
+ const measure = (row) => {
4003
+ const c = container.getBoundingClientRect();
4004
+ const r = row.getBoundingClientRect();
4005
+ return pillBoxOf(r, {
4006
+ left: c.left,
4007
+ top: c.top,
4008
+ scrollLeft: container.scrollLeft,
4009
+ scrollTop: container.scrollTop,
4010
+ clientLeft: container.clientLeft,
4011
+ clientTop: container.clientTop
4012
+ });
4013
+ };
4014
+ const at = () => ({ x: sx.x, y: sy.x, w: sw.x, h: sh.x });
4015
+ const syncCopy = (span) => {
4016
+ if (!copyOn)
4017
+ return;
4018
+ const keep = new Set;
4019
+ for (const { row, box } of rowBoxes) {
4020
+ if (!boxesMeet(box, span))
4021
+ continue;
4022
+ keep.add(row);
4023
+ let n = clones.get(row);
4024
+ if (!n) {
4025
+ n = row.cloneNode(true);
4026
+ n.removeAttribute("id");
4027
+ for (const d of n.querySelectorAll("[id]"))
4028
+ d.removeAttribute("id");
4029
+ clones.set(row, n);
4030
+ copyRows.append(n);
4031
+ }
4032
+ const s = n.style;
4033
+ s.position = "absolute";
4034
+ s.inset = "auto";
4035
+ s.left = `${box.x}px`;
4036
+ s.top = `${box.y}px`;
4037
+ s.width = `${box.w}px`;
4038
+ s.height = `${box.h}px`;
4039
+ s.margin = "0";
4040
+ s.transform = "none";
4041
+ s.clipPath = "";
4042
+ s.opacity = "";
4043
+ s.boxSizing = "border-box";
4044
+ s.color = options.color ?? "var(--_k-fg)";
4045
+ }
4046
+ for (const [row, n] of clones)
4047
+ if (!keep.has(row)) {
4048
+ n.remove();
4049
+ clones.delete(row);
4050
+ }
4051
+ };
4052
+ const paint = () => {
4053
+ const b = at();
4054
+ const x0 = grid(b.x);
4055
+ const y0 = grid(b.y);
4056
+ const w = grid(b.x + b.w) - x0;
4057
+ const h = grid(b.y + b.h) - y0;
4058
+ const r = target ? pillRadius({ h: from.h, r: fromR }, { h: target.h, r: toR }, b.h, pillProgress(from, target, b)) : toR;
4059
+ const radius = `${Math.max(0, Math.min(r, h / 2, w / 2)).toFixed(2)}px`;
4060
+ for (const n of copyOn ? [pill, copy] : [pill]) {
4061
+ n.style.transform = `translate3d(${x0}px,${y0}px,0)`;
4062
+ n.style.width = `${w}px`;
4063
+ n.style.height = `${h}px`;
4064
+ n.style.borderRadius = radius;
4065
+ }
4066
+ if (copyOn)
4067
+ copyRows.style.transform = `translate3d(${-x0}px,${-y0}px,0)`;
4068
+ };
4069
+ const loop = frameLoop(win, (dt) => {
4070
+ for (const s of springs)
4071
+ s.step(dt);
4072
+ paint();
4073
+ return springs.some((s) => !s.resting);
4074
+ });
4075
+ const show = (on) => {
4076
+ shown = on;
4077
+ pill.toggleAttribute("data-off", !on);
4078
+ copy.toggleAttribute("data-off", !on);
4079
+ container.toggleAttribute("data-pick-empty", !on);
4080
+ };
4081
+ const update = (o) => {
4082
+ if (disposed)
4083
+ return;
4084
+ const row = options.selected();
4085
+ const next = row?.isConnected ? measure(row) : null;
4086
+ const plan = planPill({ shown, at: shown ? at() : null, target }, next, {
4087
+ first,
4088
+ reduced: reducedMotion(win),
4089
+ ...o?.snap ? { snap: true } : {}
4090
+ });
4091
+ first = false;
4092
+ for (const n of clones.values())
4093
+ n.remove();
4094
+ clones.clear();
4095
+ rowBoxes = copyOn ? rowsNow().filter((r) => r.isConnected).map((r) => ({ row: r, box: measure(r) })) : [];
4096
+ if (plan.kind === "hide") {
4097
+ if (shown)
4098
+ show(false);
4099
+ return;
4100
+ }
4101
+ if (plan.kind === "stay") {
4102
+ syncCopy(boxUnion(at(), target));
4103
+ return;
4104
+ }
4105
+ const r = radiusOf(row);
4106
+ if (plan.kind === "land") {
4107
+ sx.snap(plan.box.x);
4108
+ sy.snap(plan.box.y);
4109
+ sw.snap(plan.box.w);
4110
+ sh.snap(plan.box.h);
4111
+ from = plan.box;
4112
+ fromR = r;
4113
+ toR = r;
4114
+ target = plan.box;
4115
+ syncCopy(plan.box);
4116
+ paint();
4117
+ loop.stop();
4118
+ if (!shown)
4119
+ show(true);
4120
+ return;
4121
+ }
4122
+ const m = springMotion(plan.spring);
4123
+ for (const s of springs) {
4124
+ s.omega = m.omega;
4125
+ s.zeta = m.zeta;
4126
+ }
4127
+ const now = at();
4128
+ fromR = target ? pillRadius({ h: from.h, r: fromR }, { h: target.h, r: toR }, now.h, pillProgress(from, target, now)) : r;
4129
+ from = now;
4130
+ toR = r;
4131
+ target = plan.box;
4132
+ sx.set(plan.box.x);
4133
+ sy.set(plan.box.y);
4134
+ sw.set(plan.box.w);
4135
+ sh.set(plan.box.h);
4136
+ syncCopy(boxUnion(now, plan.box));
4137
+ loop.wake();
4138
+ };
4139
+ const ro = "ResizeObserver" in win ? new win.ResizeObserver(() => update({ snap: true })) : null;
4140
+ ro?.observe(container);
4141
+ update();
4142
+ return {
4143
+ element: pill,
4144
+ update,
4145
+ dispose() {
4146
+ disposed = true;
4147
+ loop.stop();
4148
+ ro?.disconnect();
4149
+ pill.remove();
4150
+ copy.remove();
4151
+ container.removeAttribute("data-pick-empty");
4152
+ }
4153
+ };
4154
+ }
4155
+ 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) => {
4156
+ const x = Math.min(a.x, b.x);
4157
+ const y = Math.min(a.y, b.y);
4158
+ return {
4159
+ x,
4160
+ y,
4161
+ w: Math.max(a.x + a.w, b.x + b.w) - x,
4162
+ h: Math.max(a.y + a.h, b.y + b.h) - y
4163
+ };
4164
+ }, grid = (v) => Math.round(v * 64) / 64;
4165
+ var init_selection_pill = __esm(() => {
4166
+ init_motion();
4167
+ init_look();
4168
+ });
4169
+
3864
4170
  // src/panel/hints.ts
3865
4171
  function extractWords(text) {
3866
4172
  return text.toLowerCase().match(/[a-z]+/g) ?? [];
@@ -4689,34 +4995,27 @@ function tree(container, options) {
4689
4995
  const empty = doc.createElement("div");
4690
4996
  empty.className = "knobs-tree-empty";
4691
4997
  empty.hidden = true;
4692
- const cursor = doc.createElement("div");
4693
- cursor.className = "knobs-tree-cursor";
4694
- root.append(space, cursor, line, empty);
4695
- const cursorY = new Spring(0, MOVE.omega, MOVE.zeta, 0.1);
4696
- let cursorOn = false;
4697
- const drawCursor = () => {
4698
- cursor.style.transform = `translate3d(0,${Math.round(cursorY.x * 2) / 2}px,0)`;
4998
+ root.append(space, line, empty);
4999
+ let pill = null;
5000
+ let picked = null;
5001
+ let travelUntil = 0;
5002
+ const pickedRow = () => {
5003
+ const id = sel.ids.length === 1 ? sel.ids[0] : null;
5004
+ const v = id === null ? undefined : views.get(id);
5005
+ return v && !v.leaving && rowIndex.has(v.id) ? v : null;
4699
5006
  };
4700
5007
  const placeCursor = (animate) => {
4701
- if (!options.glide)
5008
+ if (!pill)
4702
5009
  return;
4703
5010
  const id = sel.ids.length === 1 ? sel.ids[0] : null;
4704
- const i = id === null ? undefined : rowIndex.get(id);
4705
- root.toggleAttribute("data-glide", i !== undefined);
4706
- if (i === undefined) {
4707
- cursor.removeAttribute("data-on");
4708
- cursorOn = false;
4709
- return;
4710
- }
4711
- if (cursorOn && animate && !reducedMotion(win))
4712
- cursorY.set(i * h);
4713
- else
4714
- cursorY.snap(i * h);
4715
- cursorOn = true;
4716
- cursor.setAttribute("data-on", "");
4717
- drawCursor();
4718
- if (!cursorY.resting)
4719
- loop.wake();
5011
+ root.toggleAttribute("data-glide", id !== null && rowIndex.has(id));
5012
+ const fresh = id !== picked;
5013
+ picked = id;
5014
+ if (animate && fresh) {
5015
+ travelUntil = performance.now() + 450;
5016
+ pill.update();
5017
+ } else if (performance.now() >= travelUntil)
5018
+ pill.update({ snap: true });
4720
5019
  };
4721
5020
  container.append(root);
4722
5021
  const scrollbar = overlayScrollbar(root, space);
@@ -4745,6 +5044,12 @@ function tree(container, options) {
4745
5044
  };
4746
5045
  const views = new Map;
4747
5046
  const pool = [];
5047
+ if (options.glide)
5048
+ pill = selectionPill(root, {
5049
+ rows: () => [...views.values()].filter((v) => !v.leaving).map((v) => v.el),
5050
+ selected: () => pickedRow()?.el ?? null,
5051
+ radius: (row) => Number.parseFloat(win.getComputedStyle(row, "::before").borderTopLeftRadius) || 0
5052
+ });
4748
5053
  const makeView = (id) => {
4749
5054
  const v = pool.pop() ?? createView();
4750
5055
  v.id = id;
@@ -4925,12 +5230,6 @@ function tree(container, options) {
4925
5230
  function frame(dt) {
4926
5231
  let moving = false;
4927
5232
  let bottom = rows.length * h;
4928
- if (options.glide && !cursorY.resting) {
4929
- cursorY.step(dt);
4930
- drawCursor();
4931
- if (!cursorY.resting)
4932
- moving = true;
4933
- }
4934
5233
  for (const v of views.values()) {
4935
5234
  v.y.step(dt);
4936
5235
  if (!v.y.resting)
@@ -4950,6 +5249,8 @@ function tree(container, options) {
4950
5249
  }
4951
5250
  setSpace(moving ? bottom : rows.length * h);
4952
5251
  trim();
5252
+ if (pill && moving && performance.now() >= travelUntil)
5253
+ pill.update({ snap: true });
4953
5254
  return moving;
4954
5255
  }
4955
5256
  const trim = () => {
@@ -5578,6 +5879,7 @@ function tree(container, options) {
5578
5879
  autoScroll.stop();
5579
5880
  ro.disconnect();
5580
5881
  scrollbar.dispose();
5882
+ pill?.dispose();
5581
5883
  dragging?.ghost.remove();
5582
5884
  root.remove();
5583
5885
  }
@@ -5596,6 +5898,7 @@ var init_tree = __esm(() => {
5596
5898
  init_motion();
5597
5899
  init_page_hints();
5598
5900
  init_look();
5901
+ init_selection_pill();
5599
5902
  init_tree_model();
5600
5903
  MOVE = springMotion(SPRINGS.default);
5601
5904
  });
@@ -5732,7 +6035,7 @@ function codeDiff(container, options) {
5732
6035
  };
5733
6036
  const nav = make("span", "knobs-diff-nav");
5734
6037
  const prev = button("Previous file", PREV);
5735
- const next = button("Next file", ICONS.caret);
6038
+ const next = button("Next file", NEXT);
5736
6039
  const place = make("span", "");
5737
6040
  nav.append(prev, place, next);
5738
6041
  const wrapButton = button("Wrap lines", WRAP);
@@ -5753,9 +6056,8 @@ function codeDiff(container, options) {
5753
6056
  const seg = make("div", "knobs-diff-seg");
5754
6057
  seg.setAttribute("role", "radiogroup");
5755
6058
  seg.setAttribute("aria-label", "Layout");
5756
- const pill = make("i", "knobs-diff-pill");
5757
6059
  const choice = (value, label) => {
5758
- const b = make("button", "");
6060
+ const b = make("button", "knobs-diff-choice");
5759
6061
  b.type = "button";
5760
6062
  b.setAttribute("role", "radio");
5761
6063
  b.dataset.value = value;
@@ -5764,7 +6066,7 @@ function codeDiff(container, options) {
5764
6066
  };
5765
6067
  const splitButton = choice("split", "Split");
5766
6068
  const unifiedButton = choice("unified", "Unified");
5767
- seg.append(pill, splitButton, unifiedButton);
6069
+ seg.append(splitButton, unifiedButton);
5768
6070
  if (options.lead)
5769
6071
  head.append(options.lead);
5770
6072
  head.append(path, stat, make("span", "knobs-diff-fill"), nav, wrapButton, seg, viewedButton);
@@ -5775,6 +6077,11 @@ function codeDiff(container, options) {
5775
6077
  body.append(note, probe);
5776
6078
  root.append(head, body);
5777
6079
  container.append(root);
6080
+ const segPill = selectionPill(seg, {
6081
+ rows: () => [splitButton, unifiedButton],
6082
+ selected: () => layout === "split" ? splitButton : unifiedButton
6083
+ });
6084
+ let pilled = layout;
5778
6085
  const fileNow = () => files.find((f) => f.name === current) ?? null;
5779
6086
  const paintHead = () => {
5780
6087
  const file = fileNow();
@@ -5807,6 +6114,10 @@ function codeDiff(container, options) {
5807
6114
  next.disabled = at >= files.length - 1;
5808
6115
  wrapButton.setAttribute("aria-pressed", String(wrap));
5809
6116
  seg.dataset.value = layout;
6117
+ if (pilled !== layout) {
6118
+ pilled = layout;
6119
+ segPill.update();
6120
+ }
5810
6121
  splitButton.setAttribute("aria-checked", String(layout === "split"));
5811
6122
  unifiedButton.setAttribute("aria-checked", String(layout === "unified"));
5812
6123
  splitButton.tabIndex = layout === "split" ? 0 : -1;
@@ -6641,6 +6952,7 @@ function codeDiff(container, options) {
6641
6952
  dispose() {
6642
6953
  disposed = true;
6643
6954
  loop.stop();
6955
+ segPill.dispose();
6644
6956
  sizes.disconnect();
6645
6957
  attaching.disconnect();
6646
6958
  resized.disconnect();
@@ -6996,7 +7308,7 @@ function diffReview(container, options) {
6996
7308
  }
6997
7309
  };
6998
7310
  }
6999
- var lib, load = () => lib ??= import("@pierre/diffs"), SYNTAX, IN, OUT, FADE4, HEIGHT, DRAWER, BLUR = 3, WRAP, PREV, UNVIEWED, VIEWED, SEARCH, CLEAR2, SIDEBAR, SEPARATOR = 32;
7311
+ 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;
7000
7312
  var init_diff = __esm(() => {
7001
7313
  init_glide();
7002
7314
  init_motion();
@@ -7004,6 +7316,8 @@ var init_diff = __esm(() => {
7004
7316
  init_diff_model();
7005
7317
  init_look();
7006
7318
  init_rgba();
7319
+ init_icons();
7320
+ init_selection_pill();
7007
7321
  init_tree();
7008
7322
  SYNTAX = { dark: "pierre-dark", light: "pierre-light" };
7009
7323
  IN = springMotion(SPRINGS.default);
@@ -7011,13 +7325,14 @@ var init_diff = __esm(() => {
7011
7325
  FADE4 = springMotion(FADE);
7012
7326
  HEIGHT = springMotion(SPRINGS.calm);
7013
7327
  DRAWER = springMotion(SPRINGS.calm);
7014
- 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"/>');
7015
- PREV = svg('<path d="M10 3.5 5.5 8l4.5 4.5"/>');
7016
- UNVIEWED = svg('<circle cx="8" cy="8" r="5.5"/>');
7017
- VIEWED = svg('<circle cx="8" cy="8" r="5.5"/><path d="m5.6 8.1 1.7 1.7 3.2-3.5"/>');
7018
- SEARCH = svg('<circle cx="7" cy="7" r="4.5"/><path d="m10.5 10.5 3 3"/>');
7019
- CLEAR2 = svg('<path d="m5 5 6 6M11 5l-6 6"/>');
7020
- SIDEBAR = svg('<rect x="2" y="3" width="12" height="10" rx="2"/><path d="M6.5 3v10"/>');
7328
+ WRAP = icon("arrowUDownLeft");
7329
+ PREV = icon("caretLeft");
7330
+ NEXT = icon("caretRight");
7331
+ UNVIEWED = icon("circle");
7332
+ VIEWED = icon("checkCircle");
7333
+ SEARCH = icon("magnifyingGlass", { small: true });
7334
+ CLEAR2 = icon("x", { small: true });
7335
+ SIDEBAR = icon("sidebar");
7021
7336
  });
7022
7337
 
7023
7338
  // src/ui/layer.ts
@@ -8000,10 +8315,25 @@ function advance(t, dt, o) {
8000
8315
  }
8001
8316
  return { t: hi, ended: true, wrapped: false };
8002
8317
  }
8003
- 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;
8318
+ function moveHead(head, t, motion, catching, minGap = 0) {
8319
+ if (motion === "jump" && Math.abs(t - head.x) > minGap)
8320
+ return { x: head.x, target: t, catching: true };
8321
+ if (catching)
8322
+ return { x: head.x + (t - head.target), target: t, catching: true };
8323
+ return { x: t, target: t, catching: false };
8324
+ }
8325
+ function currentMarker(markers, t, fps) {
8326
+ let best = null;
8327
+ for (const m of markers)
8328
+ if (m.time <= t + 0.5 / fps && (!best || m.time >= best.time))
8329
+ best = m;
8330
+ return best;
8331
+ }
8332
+ 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;
8004
8333
  var init_timeline_model = __esm(() => {
8005
8334
  SECOND_STEPS = [0.1, 0.2, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600];
8006
8335
  FRAME_STEPS = [1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 9000, 18000];
8336
+ CATCH_UP = { duration: 0.15, bounce: 0 };
8007
8337
  });
8008
8338
 
8009
8339
  // src/ui/timeline.ts
@@ -8016,6 +8346,7 @@ function timeline(container, options) {
8016
8346
  let speed = options.speed ?? 1;
8017
8347
  let repeat = options.repeat ?? true;
8018
8348
  const arrows = options.arrows ?? "frames";
8349
+ const lane = options.variant === "lane";
8019
8350
  let time = Math.min(duration, Math.max(0, options.time ?? 0));
8020
8351
  let playing = false;
8021
8352
  let markers = [...options.markers ?? []];
@@ -8049,27 +8380,27 @@ function timeline(container, options) {
8049
8380
  root.setAttribute("aria-label", options.label ?? "Timeline");
8050
8381
  root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight , . Home End [ ] I O M");
8051
8382
  applyLook2(root, options);
8383
+ if (lane)
8384
+ root.dataset.variant = "lane";
8052
8385
  const transport = el("div", "knobs-tl-transport");
8053
8386
  const playBtn = el("button", "knobs-tl-play");
8054
8387
  playBtn.type = "button";
8055
8388
  playBtn.tabIndex = -1;
8056
- const readout = el("div", "knobs-tl-time");
8057
- const now = el("span", "knobs-tl-now");
8058
- const sub = el("span", "knobs-tl-sub");
8059
- readout.append(now, sub);
8060
- transport.append(playBtn, readout);
8389
+ transport.append(playBtn);
8061
8390
  const track = el("div", "knobs-tl-track");
8062
8391
  const canvas = el("canvas", "knobs-tl-ruler");
8063
8392
  canvas.setAttribute("aria-hidden", "true");
8064
- const lane = el("div", "knobs-tl-lane");
8393
+ const markerLane = el("div", "knobs-tl-lane");
8065
8394
  const hover = el("div", "knobs-tl-hover");
8395
+ const tag = el("span", "knobs-tl-tag");
8396
+ tag.setAttribute("aria-hidden", "true");
8066
8397
  const head = el("div", "knobs-tl-head");
8067
8398
  head.id = `${id}-head`;
8068
8399
  head.setAttribute("role", "slider");
8069
8400
  head.setAttribute("aria-label", "Playhead");
8070
8401
  head.setAttribute("aria-valuemin", "0");
8071
8402
  head.setAttribute("aria-valuemax", String(duration));
8072
- track.append(canvas, lane, hover, head);
8403
+ track.append(canvas, markerLane, hover, head, tag);
8073
8404
  root.append(transport, track);
8074
8405
  container.append(root);
8075
8406
  let width = 1;
@@ -8116,13 +8447,14 @@ function timeline(container, options) {
8116
8447
  let drawn = "";
8117
8448
  const drawRuler = () => {
8118
8449
  const v = view();
8119
- const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${units}:${fps}`;
8450
+ const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${height}:${units}:${fps}`;
8120
8451
  if (key === drawn)
8121
8452
  return;
8122
8453
  drawn = key;
8123
8454
  const dpr = win.devicePixelRatio || 1;
8124
8455
  const w = Math.max(1, Math.round(width * dpr));
8125
- const h = Math.round(RULER * dpr);
8456
+ const rh = lane ? height : RULER;
8457
+ const h = Math.round(rh * dpr);
8126
8458
  if (canvas.width !== w || canvas.height !== h) {
8127
8459
  canvas.width = w;
8128
8460
  canvas.height = h;
@@ -8131,8 +8463,19 @@ function timeline(container, options) {
8131
8463
  if (!ctx)
8132
8464
  return;
8133
8465
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
8134
- ctx.clearRect(0, 0, width, RULER);
8466
+ ctx.clearRect(0, 0, width, rh);
8135
8467
  const ticks = rulerTicks(v, width, { fps, units });
8468
+ if (lane) {
8469
+ ctx.fillStyle = colors.tick;
8470
+ for (const t of ticks.minor)
8471
+ if (t >= -0.000000001 && t <= duration + 0.000000001)
8472
+ ctx.fillRect(xOf(t) - 0.5, rh - 4, 1, 4);
8473
+ ctx.fillStyle = colors.major;
8474
+ for (const m of ticks.major)
8475
+ if (m.t >= -0.000000001 && m.t <= duration + 0.000000001)
8476
+ ctx.fillRect(xOf(m.t) - 0.5, rh - 7, 1, 7);
8477
+ return;
8478
+ }
8136
8479
  const px = (t) => Math.round(xOf(t)) + 0.5;
8137
8480
  ctx.fillStyle = colors.line;
8138
8481
  ctx.fillRect(0, RULER - 1, width, 1);
@@ -8172,18 +8515,21 @@ function timeline(container, options) {
8172
8515
  n.dataset.id = m.id;
8173
8516
  const pin = el("span", "knobs-tl-pin");
8174
8517
  const label = el("span", "knobs-tl-mlabel");
8518
+ label.append(el("span", "knobs-tl-mtext knobs-scroll-fade-x"));
8175
8519
  n.append(pin, label);
8176
- lane.append(n);
8520
+ markerLane.append(n);
8177
8521
  markerEls.set(m.id, n);
8178
8522
  if (built)
8179
8523
  enter(n);
8180
8524
  }
8181
8525
  const label = n.querySelector(".knobs-tl-mlabel");
8182
- if (label.textContent !== (m.label ?? ""))
8183
- label.textContent = m.label ?? "";
8526
+ const text = label.firstElementChild;
8527
+ if (text.textContent !== (m.label ?? ""))
8528
+ text.textContent = m.label ?? "";
8184
8529
  label.hidden = !m.label;
8185
8530
  n.style.setProperty("--knobs-tl-color", m.color ?? "");
8186
8531
  n.toggleAttribute("data-selected", selected?.kind === "marker" && selected.id === m.id);
8532
+ n.toggleAttribute("data-current", m.id === currentId);
8187
8533
  n.title = `${m.label ? `${m.label} · ` : ""}${clock(m.time)}`;
8188
8534
  }
8189
8535
  placeMarkers();
@@ -8211,7 +8557,7 @@ function timeline(container, options) {
8211
8557
  b.dataset.edge = "end";
8212
8558
  const label = el("span", "knobs-tl-rlabel");
8213
8559
  n.append(label, a, b);
8214
- track.insertBefore(n, lane);
8560
+ track.insertBefore(n, markerLane);
8215
8561
  rangeEls.set(r.id, n);
8216
8562
  if (built)
8217
8563
  enter(n);
@@ -8260,16 +8606,17 @@ function timeline(container, options) {
8260
8606
  };
8261
8607
  const clock = (t) => units === "frames" ? `f${toFrame(t, fps)}` : `${timeLabel(t, "seconds", fps, 0.01)}`;
8262
8608
  let shownHead = time;
8609
+ let currentId = null;
8263
8610
  const renderHead = () => {
8264
8611
  head.style.transform = `translateX(${xOf(shownHead).toFixed(2)}px)`;
8265
- const main = units === "frames" ? `f${toFrame(time, fps)}` : `${time.toFixed(2)}s`;
8266
- const minor = units === "frames" ? `${time.toFixed(2)}s` : `f${toFrame(time, fps)}`;
8267
- if (now.textContent !== main)
8268
- now.textContent = main;
8269
- if (sub.textContent !== minor)
8270
- sub.textContent = minor;
8271
8612
  head.setAttribute("aria-valuenow", time.toFixed(3));
8272
8613
  head.setAttribute("aria-valuetext", `${time.toFixed(2)} seconds, frame ${toFrame(time, fps)}`);
8614
+ const cur = currentMarker(markers, time, fps)?.id ?? null;
8615
+ if (cur !== currentId) {
8616
+ markerEls.get(currentId ?? "")?.removeAttribute("data-current");
8617
+ currentId = cur;
8618
+ markerEls.get(cur ?? "")?.setAttribute("data-current", "");
8619
+ }
8273
8620
  };
8274
8621
  let viewSent = "";
8275
8622
  const render = () => {
@@ -8295,18 +8642,27 @@ function timeline(container, options) {
8295
8642
  };
8296
8643
  const caret = new Spring(time, GLIDE.omega, GLIDE.zeta, 0.00001);
8297
8644
  let gliding = false;
8298
- const setTime = (t, source, shown = t, glide = false) => {
8299
- const next = Math.min(duration, Math.max(0, t));
8300
- if (glide && !reducedMotion(win)) {
8645
+ const headTo = (shown, motion) => {
8646
+ const from = gliding ? { x: caret.x, target: caret.target } : { x: shownHead, target: shownHead };
8647
+ const jump = motion === "jump" && !reducedMotion(win);
8648
+ const next = moveHead(from, shown, jump ? "jump" : "follow", gliding, 2 / pxPerSec());
8649
+ if (next.catching) {
8301
8650
  if (!gliding)
8302
- caret.snap(shownHead);
8303
- caret.set(next);
8651
+ caret.snap(from.x);
8652
+ caret.x = next.x;
8653
+ caret.target = next.target;
8304
8654
  gliding = true;
8655
+ shownHead = caret.x;
8305
8656
  loop.wake();
8306
8657
  } else {
8307
8658
  gliding = false;
8308
- shownHead = shown;
8659
+ caret.snap(next.x);
8660
+ shownHead = next.x;
8309
8661
  }
8662
+ };
8663
+ const setTime = (t, source, shown = t, glide = false) => {
8664
+ const next = Math.min(duration, Math.max(0, t));
8665
+ headTo(Math.min(duration, Math.max(0, shown)), glide ? "jump" : "follow");
8310
8666
  const moved = next !== time;
8311
8667
  time = next;
8312
8668
  renderHead();
@@ -8347,8 +8703,7 @@ function timeline(container, options) {
8347
8703
  options.onCross?.(m);
8348
8704
  }
8349
8705
  time = step.t;
8350
- shownHead = time;
8351
- gliding = false;
8706
+ headTo(time, step.wrapped ? "jump" : "follow");
8352
8707
  emitSeek(time, "play");
8353
8708
  keepInView();
8354
8709
  if (step.ended) {
@@ -8361,7 +8716,7 @@ function timeline(container, options) {
8361
8716
  shownHead = caret.step(dt);
8362
8717
  if (caret.resting) {
8363
8718
  gliding = false;
8364
- shownHead = time;
8719
+ shownHead = caret.target;
8365
8720
  }
8366
8721
  }
8367
8722
  vStart.step(dt);
@@ -8374,9 +8729,9 @@ function timeline(container, options) {
8374
8729
  return;
8375
8730
  const r = loopRange();
8376
8731
  if (r && (time < r.start || time >= r.end - 0.5 / fps))
8377
- setTime(r.start, "code");
8732
+ setTime(r.start, "code", r.start, true);
8378
8733
  else if (!r && time >= duration - 0.5 / fps)
8379
- setTime(0, "code");
8734
+ setTime(0, "code", 0, true);
8380
8735
  playing = true;
8381
8736
  syncPlay();
8382
8737
  emitPlay(true);
@@ -8505,24 +8860,35 @@ function timeline(container, options) {
8505
8860
  };
8506
8861
  const localX = (e) => e.clientX - track.getBoundingClientRect().left;
8507
8862
  let drag = null;
8508
- const scrubTo = (x) => {
8863
+ const scrubTo = (x, press = false) => {
8509
8864
  const shownX = softClamp(x, xOf(0), xOf(duration), 10);
8510
8865
  const s = scrubTime(tOf(shownX), {
8511
8866
  duration,
8512
8867
  fps,
8513
8868
  pxPerSec: pxPerSec(),
8514
- stops: snapMarkers ? markers.map((m) => m.time) : [],
8869
+ stops: snapMarkers && !lane ? markers.map((m) => m.time) : [],
8515
8870
  frames: snapFrames
8516
8871
  });
8517
- setTime(s.t, "scrub", s.shown);
8872
+ setTime(s.t, "scrub", s.shown, press);
8518
8873
  root.toggleAttribute("data-stuck", s.stop !== null);
8519
8874
  };
8875
+ const showTag = (x) => {
8876
+ const at = Math.min(duration, Math.max(0, tOf(x)));
8877
+ const text = `${at.toFixed(2)} s`;
8878
+ if (tag.textContent !== text)
8879
+ tag.textContent = text;
8880
+ const cx = Math.min(width, Math.max(0, x));
8881
+ const flip = cx > width - tag.offsetWidth - 12;
8882
+ tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
8883
+ tag.dataset.on = "";
8884
+ };
8885
+ const hideTag = () => delete tag.dataset.on;
8520
8886
  track.addEventListener("pointerdown", (e) => {
8521
8887
  if (e.button !== 0 || editing)
8522
8888
  return;
8523
8889
  root.focus({ preventScroll: true });
8524
8890
  const t = e.target;
8525
- const markerEl = t.closest(".knobs-tl-marker");
8891
+ const markerEl = lane ? null : t.closest(".knobs-tl-marker");
8526
8892
  const edgeEl = t.closest(".knobs-tl-edge");
8527
8893
  const rangeEl = t.closest(".knobs-tl-range");
8528
8894
  track.setPointerCapture(e.pointerId);
@@ -8567,13 +8933,16 @@ function timeline(container, options) {
8567
8933
  root.dataset.scrubbing = "";
8568
8934
  for (const l of listeners)
8569
8935
  l.onScrub?.(true);
8570
- scrubTo(localX(e));
8936
+ scrubTo(localX(e), true);
8937
+ showTag(localX(e));
8571
8938
  }
8572
8939
  e.preventDefault();
8573
8940
  });
8574
8941
  track.addEventListener("pointermove", (e) => {
8575
8942
  const x = localX(e);
8576
8943
  hover.style.transform = `translateX(${x.toFixed(1)}px)`;
8944
+ if (!drag || drag.kind === "scrub")
8945
+ showTag(x);
8577
8946
  if (!drag)
8578
8947
  return;
8579
8948
  if (drag.kind === "scrub")
@@ -8619,8 +8988,10 @@ function timeline(container, options) {
8619
8988
  delete root.dataset.dragging;
8620
8989
  root.removeAttribute("data-stuck");
8621
8990
  if (d.kind === "scrub") {
8622
- shownHead = time;
8991
+ headTo(time, "follow");
8623
8992
  renderHead();
8993
+ if (!track.matches(":hover"))
8994
+ hideTag();
8624
8995
  for (const l of listeners)
8625
8996
  l.onScrub?.(false);
8626
8997
  if (d.wasPlaying)
@@ -8633,7 +9004,7 @@ function timeline(container, options) {
8633
9004
  } else {
8634
9005
  const m = markers.find((x) => x.id === d.id);
8635
9006
  if (m)
8636
- setTime(m.time, "marker");
9007
+ setTime(m.time, "marker", m.time, true);
8637
9008
  }
8638
9009
  } else if (d.kind === "edge" || d.moved)
8639
9010
  emitRanges();
@@ -8642,10 +9013,14 @@ function timeline(container, options) {
8642
9013
  track.addEventListener("pointercancel", endDrag);
8643
9014
  track.addEventListener("lostpointercapture", endDrag);
8644
9015
  track.addEventListener("pointerleave", () => {
8645
- if (!drag)
8646
- hover.style.transform = "translateX(-100px)";
9016
+ if (drag)
9017
+ return;
9018
+ hover.style.transform = "translateX(-100px)";
9019
+ hideTag();
8647
9020
  });
8648
9021
  track.addEventListener("dblclick", (e) => {
9022
+ if (lane)
9023
+ return;
8649
9024
  const markerEl = e.target.closest(".knobs-tl-marker");
8650
9025
  if (markerEl)
8651
9026
  return rename(markerEl.dataset.id);
@@ -8656,6 +9031,8 @@ function timeline(container, options) {
8656
9031
  rename(m.id);
8657
9032
  });
8658
9033
  track.addEventListener("wheel", (e) => {
9034
+ if (lane)
9035
+ return;
8659
9036
  e.preventDefault();
8660
9037
  const scale = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? height : 1;
8661
9038
  if (e.ctrlKey || e.metaKey) {
@@ -8719,7 +9096,7 @@ function timeline(container, options) {
8719
9096
  if (!m)
8720
9097
  return;
8721
9098
  select({ kind: "marker", id: m.id });
8722
- return setTime(m.time, "marker");
9099
+ return setTime(m.time, "marker", m.time, true);
8723
9100
  }
8724
9101
  case "i":
8725
9102
  case "I":
@@ -8731,11 +9108,13 @@ function timeline(container, options) {
8731
9108
  return setEdge("end", time);
8732
9109
  case "m":
8733
9110
  case "M":
9111
+ if (lane)
9112
+ return;
8734
9113
  claim();
8735
9114
  addMarker(time);
8736
9115
  return;
8737
9116
  case "Enter":
8738
- if (selected?.kind === "marker") {
9117
+ if (selected?.kind === "marker" && !lane) {
8739
9118
  claim();
8740
9119
  rename(selected.id);
8741
9120
  }
@@ -8753,19 +9132,25 @@ function timeline(container, options) {
8753
9132
  return;
8754
9133
  case "=":
8755
9134
  case "+":
9135
+ if (lane)
9136
+ return;
8756
9137
  claim();
8757
9138
  return zoom(1.5);
8758
9139
  case "-":
9140
+ if (lane)
9141
+ return;
8759
9142
  claim();
8760
9143
  return zoom(1 / 1.5);
8761
9144
  case "0":
9145
+ if (lane)
9146
+ return;
8762
9147
  claim();
8763
9148
  return fit();
8764
9149
  }
8765
9150
  });
8766
9151
  const unregisterHints = options.hints === false ? undefined : hints.register(root, () => [
8767
9152
  { el: playBtn, label: playing ? "Pause" : "Play", activate: toggle },
8768
- ...markers.flatMap((m) => {
9153
+ ...(lane ? [] : markers).flatMap((m) => {
8769
9154
  const n = markerEls.get(m.id);
8770
9155
  if (!n || n.dataset.leaving !== undefined)
8771
9156
  return [];
@@ -8777,7 +9162,7 @@ function timeline(container, options) {
8777
9162
  ...label && !label.hidden ? { labelEl: label } : {},
8778
9163
  activate: () => {
8779
9164
  select({ kind: "marker", id: m.id });
8780
- setTime(m.time, "marker");
9165
+ setTime(m.time, "marker", m.time, true);
8781
9166
  root.focus({ preventScroll: true });
8782
9167
  }
8783
9168
  }
@@ -8789,7 +9174,7 @@ function timeline(container, options) {
8789
9174
  activate: () => root.focus({ preventScroll: true })
8790
9175
  }
8791
9176
  ], { listen: false, rank: 1 });
8792
- const menu = contextMenu(track, (e) => {
9177
+ const menu = lane ? null : contextMenu(track, (e) => {
8793
9178
  const t = e?.target ?? null;
8794
9179
  const markerEl = t?.closest(".knobs-tl-marker");
8795
9180
  const rangeEl = t?.closest(".knobs-tl-range");
@@ -8804,7 +9189,7 @@ function timeline(container, options) {
8804
9189
  run: () => {
8805
9190
  const m = markers.find((x) => x.id === mid);
8806
9191
  if (m)
8807
- setTime(m.time, "marker");
9192
+ setTime(m.time, "marker", m.time, true);
8808
9193
  }
8809
9194
  },
8810
9195
  { label: "Rename…", shortcut: "Enter", run: () => rename(mid) },
@@ -8968,7 +9353,7 @@ function timeline(container, options) {
8968
9353
  listeners.clear();
8969
9354
  loop.stop();
8970
9355
  ro.disconnect();
8971
- menu.dispose();
9356
+ menu?.dispose();
8972
9357
  root.remove();
8973
9358
  }
8974
9359
  };
@@ -8985,10 +9370,21 @@ var init_timeline = __esm(() => {
8985
9370
  init_timeline_model();
8986
9371
  INSTANT2 = springMotion(SPRINGS.instant);
8987
9372
  CALM = springMotion(SPRINGS.calm);
8988
- GLIDE = springMotion(SPRINGS.snappy);
9373
+ GLIDE = springMotion(CATCH_UP);
8989
9374
  });
8990
9375
 
8991
9376
  // src/ui/compare-model.ts
9377
+ function compareModes(modes, mode) {
9378
+ const list = [...new Set((modes ?? ["side", "wipe"]).filter((m) => ALL_MODES.includes(m)))];
9379
+ const offered = list.length ? list : ["side", "wipe"];
9380
+ return { modes: offered, mode: mode && offered.includes(mode) ? mode : offered[0] };
9381
+ }
9382
+ function modeForKey(key, modes) {
9383
+ const k = key.toLowerCase();
9384
+ if (/^[1-9]$/.test(k))
9385
+ return modes[Number(k) - 1] ?? null;
9386
+ return modes.find((m) => MODE_KEYS[m] === k) ?? null;
9387
+ }
8992
9388
  function mediaKind(src, kind) {
8993
9389
  if (kind)
8994
9390
  return kind;
@@ -9067,6 +9463,18 @@ function wipeGrab(x, rect, wipe, slop = 12) {
9067
9463
  const onLine = Math.abs(x - line) <= slop;
9068
9464
  return { onLine, offset: onLine ? x - line : 0 };
9069
9465
  }
9466
+ function seamLabels(x, left, right, wb, wa, gap = 8) {
9467
+ const room = (space, w) => Math.min(1, Math.max(0, (space - w - 2 * gap) / gap));
9468
+ return {
9469
+ before: { x: x - gap - wb, opacity: room(x - left, wb) },
9470
+ after: { x: x + gap, opacity: room(right - x, wa) }
9471
+ };
9472
+ }
9473
+ function gripTop(y, rect, grip, margin = 8) {
9474
+ const lo = rect.y + margin;
9475
+ const hi = rect.y + rect.h - margin - grip;
9476
+ return hi < lo ? rect.y + (rect.h - grip) / 2 : Math.min(hi, Math.max(lo, y - grip / 2));
9477
+ }
9070
9478
  function stripColumns(view, width, o) {
9071
9479
  const { fps } = o;
9072
9480
  const total = Math.max(1, Math.round(o.duration * fps));
@@ -9117,7 +9525,7 @@ function stripPeaks(diffs, o = {}) {
9117
9525
  });
9118
9526
  return out;
9119
9527
  }
9120
- 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({
9528
+ 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({
9121
9529
  u: r.u - dx / Math.max(1, base.w) * r.w,
9122
9530
  v: r.v - dy / Math.max(1, base.h) * r.h,
9123
9531
  w: r.w
@@ -9136,6 +9544,8 @@ var VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), last
9136
9544
  h: a.h + (b.h - a.h) * k
9137
9545
  }), 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));
9138
9546
  var init_compare_model = __esm(() => {
9547
+ ALL_MODES = ["side", "wipe", "difference"];
9548
+ MODE_KEYS = { side: "s", wipe: "w", difference: "d" };
9139
9549
  VIDEO = /\.(mp4|m4v|mov|webm|ogv|mkv)$/i;
9140
9550
  SEEK_IDLE = { seeking: false, pending: null };
9141
9551
  ZOOM_FIT = { u: 0, v: 0, w: 1, h: 1 };
@@ -9935,8 +10345,8 @@ async function createCompareGpu(canvas, onLost) {
9935
10345
  r(0, f.before);
9936
10346
  r(4, f.after);
9937
10347
  data.set([f.width, f.height, f.dpr, 0], 8);
9938
- data.set([f.wipeX, f.soft, 0, f.phase ? 1 : 0], 12);
9939
- data.set([f.from, f.to, f.mix, f.flick ? 1 : 0], 16);
10348
+ data.set([f.wipeX, f.soft, 0, 0], 12);
10349
+ data.set([f.from, f.to, f.mix, 0], 16);
9940
10350
  data.set([f.threshold, f.gain, ready[0], ready[1]], 20);
9941
10351
  data.set([f.accent[0], f.accent[1], f.accent[2], 1], 24);
9942
10352
  const l = f.loupe;
@@ -9978,8 +10388,8 @@ var MODE_INDEX, WGSL = `
9978
10388
  struct U {
9979
10389
  ra: vec4f, rb: vec4f,
9980
10390
  view: vec4f, // w, h, dpr, _
9981
- wipe: vec4f, // x, soft, _, phase
9982
- modes: vec4f, // from, to, mix, flick
10391
+ wipe: vec4f, // x, soft, _, _
10392
+ modes: vec4f, // from, to, mix, _
9983
10393
  diff: vec4f, // threshold, gain, readyA, readyB
9984
10394
  accent: vec4f,
9985
10395
  loupe: vec4f, // x, y, r, zoom (0: none)
@@ -10030,21 +10440,14 @@ fn takeB(p: vec2f, r: vec4f, c0: vec4f, near: bool) -> vec4f {
10030
10440
 
10031
10441
  fn pm(c: vec4f) -> vec4f { return vec4f(c.rgb * c.a, c.a); }
10032
10442
 
10033
- fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
10034
- let flick = u.modes.w > 0.5;
10443
+ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
10035
10444
  switch mode {
10036
- case 0u: {
10037
- let bb = select(b, vec4f(a2.rgb, b.a), flick);
10038
- return vec4f(a.rgb * a.a + bb.rgb * bb.a * (1.0 - a.a), a.a + bb.a * (1.0 - a.a));
10039
- }
10040
10445
  case 1u: {
10041
- if (flick) { return pm(a); }
10042
10446
  let s = max(u.wipe.y, 0.5);
10043
10447
  let m = smoothstep(u.wipe.x - s, u.wipe.x + s, p.x);
10044
10448
  return pm(vec4f(mix(a.rgb, b.rgb, m), mix(a.a, b.a, m)));
10045
10449
  }
10046
10450
  case 2u: {
10047
- if (flick) { return pm(a); }
10048
10451
  let d = abs(a.rgb - b.rgb);
10049
10452
  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);
10050
10453
  let c = mix(vec3f(luma(b.rgb) * 0.35), u.accent.rgb, h);
@@ -10052,8 +10455,8 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
10052
10455
  }
10053
10456
  default: {}
10054
10457
  }
10055
- // Flicker: After, or Before while X is held or on the flicker's other beat.
10056
- return pm(select(b, a, flick || u.wipe.w > 0.5));
10458
+ // Side by side: each side in its own place, Before over After where they meet.
10459
+ return vec4f(a.rgb * a.a + b.rgb * b.a * (1.0 - a.a), a.a + b.a * (1.0 - a.a));
10057
10460
  }
10058
10461
 
10059
10462
  @fragment fn fs(@builtin(position) q: vec4f) -> @location(0) vec4f {
@@ -10071,16 +10474,14 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
10071
10474
  }
10072
10475
  let a = takeA(p, u.ra, u.ca, near);
10073
10476
  let b = takeB(p, u.rb, u.cb, near);
10074
- // Before in After's place (X held, side by side): After's rect, as if Before were zoomed there.
10075
- let a2 = takeA(p, u.rb, u.cb, near);
10076
- var o = mix(composite(u32(u.modes.x), p, a, b, a2), composite(u32(u.modes.y), p, a, b, a2), u.modes.z);
10477
+ var o = mix(composite(u32(u.modes.x), p, a, b), composite(u32(u.modes.y), p, a, b), u.modes.z);
10077
10478
  if (near) { o = o + vec4f(0.05, 0.05, 0.06, 1.0) * (1.0 - o.a); }
10078
10479
  o = mix(o, vec4f(0.92), ring * 0.9);
10079
10480
  return o;
10080
10481
  }
10081
10482
  `, TEXTURE_BINDING = 4, COPY_DST = 2, RENDER_ATTACHMENT = 16, shared = null, lostListeners;
10082
10483
  var init_compare_gpu = __esm(() => {
10083
- MODE_INDEX = { side: 0, wipe: 1, difference: 2, flicker: 3 };
10484
+ MODE_INDEX = { side: 0, wipe: 1, difference: 2 };
10084
10485
  lostListeners = new Set;
10085
10486
  });
10086
10487
 
@@ -10466,7 +10867,9 @@ function compare(container, options) {
10466
10867
  ...options.theme ? { theme: options.theme } : {},
10467
10868
  ...options.appearance ? { appearance: options.appearance } : {}
10468
10869
  };
10469
- let mode = options.mode ?? "side";
10870
+ const offered = compareModes(options.modes, options.mode);
10871
+ const modeList = offered.modes;
10872
+ let mode = offered.mode;
10470
10873
  let wipe = Math.min(1, Math.max(0, options.wipe ?? 0.5));
10471
10874
  let loupeOn = options.loupe ?? false;
10472
10875
  let threshold = options.threshold ?? 4 / 255;
@@ -10484,7 +10887,7 @@ function compare(container, options) {
10484
10887
  root.setAttribute("role", "group");
10485
10888
  root.setAttribute("aria-roledescription", "compare");
10486
10889
  root.setAttribute("aria-label", options.label ?? "Compare");
10487
- root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight , . 1 2 3 4 S W D X Z L 0 = -");
10890
+ root.setAttribute("aria-keyshortcuts", `Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight , . ${modeList.map((m, i) => `${i + 1} ${modeKey(m)}`).join(" ")} Z L 0 = -`);
10488
10891
  applyLook2(root, options);
10489
10892
  root.dataset.mode = mode;
10490
10893
  if (auto)
@@ -10493,35 +10896,37 @@ function compare(container, options) {
10493
10896
  const modes = el("div", "knobs-cmp-modes");
10494
10897
  modes.setAttribute("role", "radiogroup");
10495
10898
  modes.setAttribute("aria-label", "Mode");
10496
- const modeBtns = MODES2.map((m, i) => {
10899
+ modes.hidden = modeList.length < 2;
10900
+ const modeBtns = modeList.map((m, i) => {
10497
10901
  const b = el("button", "knobs-cmp-mode");
10498
10902
  b.type = "button";
10499
10903
  b.tabIndex = -1;
10500
- b.textContent = m.label;
10501
- b.title = `${m.label} · ${i + 1}${m.key ? ` or ${m.key}` : " (hold X to flick)"}`;
10904
+ b.textContent = MODE_LABEL[m].short;
10905
+ b.title = `${MODE_LABEL[m].long} · ${i + 1} or ${modeKey(m)}`;
10502
10906
  b.setAttribute("role", "radio");
10503
- b.setAttribute("aria-keyshortcuts", m.key ? `${i + 1} ${m.key}` : `${i + 1}`);
10907
+ b.setAttribute("aria-keyshortcuts", `${i + 1} ${modeKey(m)}`);
10504
10908
  b.addEventListener("click", () => {
10505
- setMode(m.mode);
10909
+ setMode(m);
10506
10910
  root.focus({ preventScroll: true });
10507
10911
  });
10508
10912
  modes.append(b);
10509
10913
  return b;
10510
10914
  });
10511
- const loupeBtn = el("button", "knobs-cmp-toggle");
10512
- loupeBtn.type = "button";
10513
- loupeBtn.tabIndex = -1;
10514
- loupeBtn.textContent = "Loupe";
10515
- loupeBtn.title = "Loupe · hold Z";
10915
+ const toggleBtn = (name, label, title) => {
10916
+ const b = el("button", "knobs-cmp-toggle");
10917
+ b.type = "button";
10918
+ b.tabIndex = -1;
10919
+ b.innerHTML = icon(name);
10920
+ b.setAttribute("aria-label", label);
10921
+ b.title = title;
10922
+ return b;
10923
+ };
10924
+ const loupeBtn = toggleBtn("magnifyingGlass", "Loupe", "Loupe · hold Z");
10516
10925
  loupeBtn.addEventListener("click", () => {
10517
10926
  setLoupe(!loupeOn);
10518
10927
  root.focus({ preventScroll: true });
10519
10928
  });
10520
- const loopBtn = el("button", "knobs-cmp-toggle");
10521
- loopBtn.type = "button";
10522
- loopBtn.tabIndex = -1;
10523
- loopBtn.textContent = "Loop";
10524
- loopBtn.title = "Loop · L (drag its ends on the timeline)";
10929
+ const loopBtn = toggleBtn("repeat", "Loop", "Loop · L (drag its ends on the timeline)");
10525
10930
  loopBtn.setAttribute("aria-keyshortcuts", "L");
10526
10931
  loopBtn.addEventListener("click", () => {
10527
10932
  api.setLoop(!loopNow());
@@ -10538,7 +10943,8 @@ function compare(container, options) {
10538
10943
  const zoomReset = el("button", "knobs-cmp-zoom-reset");
10539
10944
  zoomReset.type = "button";
10540
10945
  zoomReset.tabIndex = -1;
10541
- zoomReset.textContent = "Reset";
10946
+ zoomReset.innerHTML = icon("arrowCounterClockwise", { small: true });
10947
+ zoomReset.setAttribute("aria-label", "Reset zoom");
10542
10948
  zoomReset.title = "Fit · 0 or double-click";
10543
10949
  zoomReset.addEventListener("click", () => {
10544
10950
  api.setZoom(null);
@@ -10553,6 +10959,9 @@ function compare(container, options) {
10553
10959
  handle.setAttribute("aria-label", "Wipe");
10554
10960
  handle.setAttribute("aria-valuemin", "0");
10555
10961
  handle.setAttribute("aria-valuemax", "100");
10962
+ const grip = el("span", "knobs-cmp-grip");
10963
+ grip.setAttribute("aria-hidden", "true");
10964
+ handle.append(grip);
10556
10965
  const note = el("div", "knobs-cmp-note");
10557
10966
  stage.append(canvas, split, labels[0], labels[1], handle, zoomPill, note);
10558
10967
  root.append(bar, stage);
@@ -10569,6 +10978,7 @@ function compare(container, options) {
10569
10978
  root.append(host);
10570
10979
  tl = timeline(host, {
10571
10980
  ...look,
10981
+ variant: "lane",
10572
10982
  duration: 1,
10573
10983
  fps: fps(),
10574
10984
  arrows: { step: 0.5, shift: 2 },
@@ -10587,9 +10997,9 @@ function compare(container, options) {
10587
10997
  const fade = new Spring(1, FADE6.omega, FADE6.zeta, 0.001);
10588
10998
  let fromMode = mode;
10589
10999
  let toMode = mode;
10590
- let phase = false;
10591
- let flickerTimer = 0;
10592
- let flickHeld = false;
11000
+ const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
11001
+ let wipeCatching = false;
11002
+ let gripY = null;
10593
11003
  let loupeHeld = false;
10594
11004
  let pointer = null;
10595
11005
  let drag = null;
@@ -10907,25 +11317,16 @@ function compare(container, options) {
10907
11317
  gpu = g;
10908
11318
  setRenderer("gpu");
10909
11319
  };
10910
- const syncFlicker = () => {
10911
- win.clearInterval(flickerTimer);
10912
- flickerTimer = 0;
10913
- phase = false;
10914
- if (mode === "flicker")
10915
- flickerTimer = win.setInterval(() => {
10916
- phase = !phase;
10917
- requestDraw();
10918
- }, FLICKER_MS);
10919
- };
10920
11320
  const syncModeUi = () => {
10921
11321
  root.dataset.mode = mode;
10922
- MODES2.forEach((m, i) => {
10923
- modeBtns[i].setAttribute("aria-checked", String(m.mode === mode));
11322
+ modeList.forEach((m, i) => {
11323
+ modeBtns[i].setAttribute("aria-checked", String(m === mode));
10924
11324
  });
10925
11325
  handle.tabIndex = mode === "wipe" ? 0 : -1;
11326
+ modePill?.update();
10926
11327
  };
10927
11328
  const setMode = (next, emit = true) => {
10928
- if (next === mode)
11329
+ if (next === mode || !modeList.includes(next))
10929
11330
  return;
10930
11331
  mode = next;
10931
11332
  const reduced = reducedMotion(win);
@@ -10941,19 +11342,29 @@ function compare(container, options) {
10941
11342
  else
10942
11343
  lay.set(next === "side" ? 0 : 1);
10943
11344
  syncModeUi();
10944
- syncFlicker();
10945
11345
  if (emit)
10946
11346
  options.onMode?.(next);
10947
11347
  requestDraw();
10948
11348
  };
10949
- const setWipe = (next, emit = true) => {
11349
+ const setWipe = (next, emit = true, jump = false) => {
10950
11350
  const w = Math.min(1, Math.max(0, next));
10951
- if (w === wipe)
11351
+ if (w === wipe && !jump)
10952
11352
  return;
10953
11353
  wipe = w;
11354
+ const minGap = 2 / Math.max(1, wipeRect(layoutNow().L.overlay).w);
11355
+ const head = moveHead({ x: wipeLine.x, target: wipeLine.target }, w, jump && !reducedMotion(win) ? "jump" : "follow", wipeCatching, minGap);
11356
+ wipeCatching = head.catching;
11357
+ if (head.catching) {
11358
+ wipeLine.x = head.x;
11359
+ wipeLine.target = head.target;
11360
+ } else
11361
+ wipeLine.snap(head.x);
10954
11362
  if (emit)
10955
11363
  options.onWipe?.(w);
10956
- drawNow();
11364
+ if (head.catching)
11365
+ requestDraw();
11366
+ else
11367
+ drawNow();
10957
11368
  };
10958
11369
  const setLoupe = (on) => {
10959
11370
  loupeOn = on;
@@ -11106,28 +11517,44 @@ function compare(container, options) {
11106
11517
  viewSent = key;
11107
11518
  options.onView({ zoom: clampZoom(z), loop: l });
11108
11519
  };
11520
+ const labelsIn = (m, r, wr, wx) => {
11521
+ if (m === "wipe") {
11522
+ const at = seamLabels(wx, wr.x, wr.x + wr.w, labelW[0], labelW[1], INSET);
11523
+ return [at.before, at.after];
11524
+ }
11525
+ const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * lay.x;
11526
+ return [
11527
+ { x: r.before.x + INSET, opacity: 1 },
11528
+ { x: ax, opacity: 1 }
11529
+ ];
11530
+ };
11531
+ const placeGrip = (wr) => {
11532
+ const gy = gripTop(gripY ?? wr.y + wr.h / 2, wr, GRIP.h) - wr.y;
11533
+ grip.style.transform = `translate3d(0,${gy.toFixed(2)}px,0)`;
11534
+ };
11109
11535
  const draw = () => {
11110
11536
  dirty = false;
11111
11537
  const r = layoutNow();
11112
11538
  const wr = wipeRect(r.L.overlay);
11113
- const wx = wipeX(wr, wipe);
11539
+ const wx = wipeX(wr, wipeLine.x);
11114
11540
  const k = lay.x;
11115
- const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * k;
11116
- labels[0].style.transform = `translate(${(r.before.x + INSET).toFixed(2)}px,${(r.before.y + INSET).toFixed(2)}px)`;
11117
- labels[1].style.transform = `translate(${ax.toFixed(2)}px,${(r.after.y + INSET).toFixed(2)}px)`;
11118
- const showsBefore = flickHeld || mode === "flicker" && phase;
11119
- const flickering = flickHeld || mode === "flicker";
11120
- labels[0].toggleAttribute("data-lit", flickering && showsBefore);
11121
- labels[1].toggleAttribute("data-lit", flickering && !showsBefore);
11122
- labels[0].toggleAttribute("data-dim", flickering && !showsBefore || mode === "wipe" && wipe < 0.02);
11123
- labels[1].toggleAttribute("data-dim", flickering && showsBefore || mode === "wipe" && wipe > 0.98);
11541
+ const a = labelsIn(fromMode, r, wr, wx);
11542
+ const b = labelsIn(toMode, r, wr, wx);
11543
+ const f = Math.min(1, Math.max(0, fade.x));
11544
+ const ys = [r.before.y + INSET, r.after.y + INSET];
11545
+ for (const i of [0, 1]) {
11546
+ const x = a[i].x + (b[i].x - a[i].x) * f;
11547
+ labels[i].style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
11548
+ labels[i].style.opacity = (a[i].opacity + (b[i].opacity - a[i].opacity) * f).toFixed(3);
11549
+ }
11124
11550
  const top = Math.min(r.L.side.before.y, r.L.side.after.y);
11125
11551
  split.style.transform = `translate(${r.L.side.split.toFixed(2)}px,${top.toFixed(2)}px)`;
11126
11552
  split.style.height = `${Math.max(0, r.L.side.after.h).toFixed(2)}px`;
11127
11553
  split.style.opacity = String(Math.max(0, 1 - k * 2));
11128
- handle.toggleAttribute("data-on", mode === "wipe" && !flickHeld);
11554
+ handle.toggleAttribute("data-on", mode === "wipe");
11129
11555
  handle.style.transform = `translate(${wx.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
11130
11556
  handle.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
11557
+ placeGrip(wr);
11131
11558
  handle.setAttribute("aria-valuenow", String(Math.round(wipe * 100)));
11132
11559
  handle.setAttribute("aria-valuetext", `${Math.round(wipe * 100)}% Before`);
11133
11560
  const pct = `${Math.round(100 / zw.x)}%`;
@@ -11150,7 +11577,7 @@ function compare(container, options) {
11150
11577
  const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, cutAt));
11151
11578
  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)` : "";
11152
11579
  };
11153
- const cut = mode === "wipe" && !flickHeld ? wx : Number.POSITIVE_INFINITY;
11580
+ const cut = mode === "wipe" ? wx : Number.POSITIVE_INFINITY;
11154
11581
  for (const s of sides) {
11155
11582
  const rect = s.index === 0 ? r.zBefore : r.zAfter;
11156
11583
  const clip = s.index === 0 ? r.before : r.after;
@@ -11159,10 +11586,8 @@ function compare(container, options) {
11159
11586
  place(s.preview, rect, clip, at);
11160
11587
  if (s.index !== 0)
11161
11588
  continue;
11162
- for (const n of [s.el, s.preview]) {
11163
- n.style.mixBlendMode = mode === "difference" && !flickHeld ? "difference" : "";
11164
- n.style.opacity = mode === "flicker" && !showsBefore ? "0" : "";
11165
- }
11589
+ for (const n of [s.el, s.preview])
11590
+ n.style.mixBlendMode = mode === "difference" ? "difference" : "";
11166
11591
  }
11167
11592
  return;
11168
11593
  }
@@ -11183,8 +11608,6 @@ function compare(container, options) {
11183
11608
  from: MODE_INDEX[fromMode],
11184
11609
  to: MODE_INDEX[toMode],
11185
11610
  mix: Math.min(1, Math.max(0, fade.x)),
11186
- flick: flickHeld,
11187
- phase,
11188
11611
  threshold,
11189
11612
  gain: 1,
11190
11613
  accent,
@@ -11200,6 +11623,11 @@ function compare(container, options) {
11200
11623
  const loop = frameLoop(win, (dt) => {
11201
11624
  lay.step(dt);
11202
11625
  fade.step(dt);
11626
+ if (wipeCatching) {
11627
+ wipeLine.step(dt);
11628
+ if (wipeLine.resting)
11629
+ wipeCatching = false;
11630
+ }
11203
11631
  zu.step(dt);
11204
11632
  zv.step(dt);
11205
11633
  zw.step(dt);
@@ -11233,7 +11661,7 @@ function compare(container, options) {
11233
11661
  pushFrame(s);
11234
11662
  }
11235
11663
  }
11236
- const moving = !lay.resting || !fade.resting || !zoomResting();
11664
+ const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching;
11237
11665
  if (moving)
11238
11666
  dirty = true;
11239
11667
  if (auto && !lay.resting)
@@ -11376,16 +11804,19 @@ function compare(container, options) {
11376
11804
  e.preventDefault();
11377
11805
  return startPinch();
11378
11806
  }
11379
- if (mode === "wipe" && !flickHeld) {
11807
+ if (mode === "wipe") {
11380
11808
  const wr = wipeRect(layoutNow().L.overlay);
11381
11809
  const inside = p.x >= wr.x - SLOP && p.x <= wr.x + wr.w + SLOP && p.y >= wr.y && p.y <= wr.y + wr.h;
11382
- const grab = wipeGrab(p.x, wr, wipe, SLOP);
11810
+ const grab = wipeGrab(p.x, wr, wipeLine.x, SLOP);
11383
11811
  if (inside && (grab.onLine || !zoomed())) {
11384
11812
  drag = { kind: "wipe", offset: grab.offset };
11385
11813
  root.dataset.dragging = "";
11814
+ gripY = p.y;
11386
11815
  e.preventDefault();
11387
11816
  if (!grab.onLine)
11388
- setWipe(wipeAt(p.x, wr));
11817
+ setWipe(wipeAt(p.x, wr), true, true);
11818
+ else
11819
+ drawNow();
11389
11820
  return;
11390
11821
  }
11391
11822
  }
@@ -11411,6 +11842,11 @@ function compare(container, options) {
11411
11842
  pinch = { d, x: mid.x, y: mid.y };
11412
11843
  return handZoom(zoomAbout(panned, k, q.pu, q.pv));
11413
11844
  }
11845
+ if (mode === "wipe" && drag?.kind !== "pan") {
11846
+ gripY = p.y;
11847
+ if (!drag)
11848
+ placeGrip(wipeRect(layoutNow().L.overlay));
11849
+ }
11414
11850
  if (drag?.kind === "wipe")
11415
11851
  setWipe(wipeAt(p.x, wipeRect(layoutNow().L.overlay), drag.offset));
11416
11852
  else if (drag?.kind === "pan") {
@@ -11483,12 +11919,6 @@ function compare(container, options) {
11483
11919
  const z = { u: zu.target, v: zv.target, w: zw.target, h: zw.target };
11484
11920
  glideZoom(zoomAbout(z, factor, 0.5, 0.5));
11485
11921
  };
11486
- const holdFlick = (on) => {
11487
- if (flickHeld === on)
11488
- return;
11489
- flickHeld = on;
11490
- drawNow();
11491
- };
11492
11922
  const holdLoupe = (on) => {
11493
11923
  if (loupeHeld === on)
11494
11924
  return;
@@ -11534,25 +11964,17 @@ function compare(container, options) {
11534
11964
  case "s":
11535
11965
  case "w":
11536
11966
  case "d":
11537
- if (e.altKey || e.shiftKey || key === "s" && claimsHintKey(doc, key))
11538
- return;
11539
- claim();
11540
- return setMode(KEY_MODE[key]);
11541
11967
  case "1":
11542
11968
  case "2":
11543
- case "3":
11544
- case "4":
11545
- if (e.altKey || e.shiftKey)
11969
+ case "3": {
11970
+ if (e.altKey || e.shiftKey || key === "s" && claimsHintKey(doc, key))
11546
11971
  return;
11547
- claim();
11548
- return setMode(MODES2[Number(key) - 1].mode);
11549
- case "x":
11550
- if (e.altKey)
11972
+ const m = modeForKey(key, modeList);
11973
+ if (!m)
11551
11974
  return;
11552
11975
  claim();
11553
- if (!e.repeat)
11554
- holdFlick(true);
11555
- return;
11976
+ return setMode(m);
11977
+ }
11556
11978
  case "z":
11557
11979
  if (e.altKey)
11558
11980
  return;
@@ -11608,8 +12030,11 @@ function compare(container, options) {
11608
12030
  labels[1].textContent = labelOf(1);
11609
12031
  loupeBtn.setAttribute("aria-pressed", String(loupeOn));
11610
12032
  loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
12033
+ const modePill = modeList.length > 1 ? selectionPill(modes, {
12034
+ rows: () => modeBtns,
12035
+ selected: () => modeBtns[modeList.indexOf(mode)] ?? null
12036
+ }) : null;
11611
12037
  syncModeUi();
11612
- syncFlicker();
11613
12038
  const unlisten = tl?.listen({
11614
12039
  onSeek: (t, source) => {
11615
12040
  if (source === "play") {
@@ -11667,24 +12092,20 @@ function compare(container, options) {
11667
12092
  });
11668
12093
  root.addEventListener("keydown", onKey);
11669
12094
  root.addEventListener("keyup", (e) => {
11670
- const key = e.key.toLowerCase();
11671
- if (key === "x")
11672
- holdFlick(false);
11673
- else if (key === "z")
12095
+ if (e.key.toLowerCase() === "z")
11674
12096
  holdLoupe(false);
11675
12097
  });
11676
12098
  root.addEventListener("focusout", (e) => {
11677
12099
  if (root.contains(e.relatedTarget))
11678
12100
  return;
11679
- holdFlick(false);
11680
12101
  holdLoupe(false);
11681
12102
  });
11682
12103
  const unregisterHints = options.hints === false ? undefined : hints.register(root, () => [
11683
- ...MODES2.map((m, i) => ({
12104
+ ...modeList.length > 1 ? modeList.map((m, i) => ({
11684
12105
  el: modeBtns[i],
11685
- label: m.label,
11686
- activate: () => setMode(m.mode)
11687
- })),
12106
+ label: MODE_LABEL[m].long,
12107
+ activate: () => setMode(m)
12108
+ })) : [],
11688
12109
  { el: loopBtn, label: "Loop", activate: () => api.setLoop(!loopNow()) },
11689
12110
  ...loupeBtn.hidden ? [] : [{ el: loupeBtn, label: "Loupe", activate: () => setLoupe(!loupeOn) }],
11690
12111
  ...zoomed() ? [{ el: zoomReset, label: "Reset zoom", activate: () => api.setZoom(null) }] : [],
@@ -11709,6 +12130,7 @@ function compare(container, options) {
11709
12130
  get mode() {
11710
12131
  return mode;
11711
12132
  },
12133
+ modes: modeList,
11712
12134
  get wipe() {
11713
12135
  return wipe;
11714
12136
  },
@@ -11764,7 +12186,7 @@ function compare(container, options) {
11764
12186
  tl?.update({ fps: next.fps });
11765
12187
  requestDraw();
11766
12188
  },
11767
- seek: (t) => tl ? tl.seek(t) : ownSeek(t),
12189
+ seek: (t, o) => tl ? tl.seek(t, o?.glide ? { glide: true } : undefined) : ownSeek(t),
11768
12190
  seekBy(d) {
11769
12191
  const total = duration || tl?.duration || 0;
11770
12192
  const t = Math.min(total, Math.max(0, now() + d));
@@ -11805,7 +12227,7 @@ function compare(container, options) {
11805
12227
  dispose() {
11806
12228
  disposed = true;
11807
12229
  loop.stop();
11808
- win.clearInterval(flickerTimer);
12230
+ modePill?.dispose();
11809
12231
  win.clearTimeout(releaseTimer);
11810
12232
  ro.disconnect();
11811
12233
  nearIo?.disconnect();
@@ -11835,7 +12257,7 @@ function compare(container, options) {
11835
12257
  wake();
11836
12258
  return api;
11837
12259
  }
11838
- var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, FLICKER_MS = 400, SLOP = 12, INSET = 8, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODES2, KEY_MODE;
12260
+ var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
11839
12261
  var init_compare = __esm(() => {
11840
12262
  init_motion();
11841
12263
  init_page_hints();
@@ -11843,21 +12265,24 @@ var init_compare = __esm(() => {
11843
12265
  init_compare_gpu();
11844
12266
  init_compare_model();
11845
12267
  init_compare_strip();
12268
+ init_icons();
11846
12269
  init_look();
11847
12270
  init_rgba();
12271
+ init_selection_pill();
11848
12272
  init_timeline();
12273
+ init_timeline_model();
11849
12274
  LAYOUT2 = springMotion({ duration: 0.35, bounce: 0 });
11850
12275
  FADE6 = springMotion(SPRINGS.snappy);
11851
12276
  FADE_REDUCED = springMotion(FADE);
11852
12277
  ZOOM = springMotion({ duration: 0.35, bounce: 0 });
12278
+ WIPE_CATCH = springMotion(CATCH_UP);
12279
+ GRIP = { w: 16, h: 28 };
11853
12280
  LOUPE = { r: 88, zoom: 4 };
11854
- MODES2 = [
11855
- { mode: "side", label: "Side by side", key: "S" },
11856
- { mode: "wipe", label: "Wipe", key: "W" },
11857
- { mode: "difference", label: "Difference", key: "D" },
11858
- { mode: "flicker", label: "Flicker", key: "" }
11859
- ];
11860
- KEY_MODE = { s: "side", w: "wipe", d: "difference" };
12281
+ MODE_LABEL = {
12282
+ side: { short: "Side", long: "Side by side" },
12283
+ wipe: { short: "Wipe", long: "Wipe" },
12284
+ difference: { short: "Difference", long: "Difference" }
12285
+ };
11861
12286
  });
11862
12287
 
11863
12288
  // src/ui/react.tsx
@@ -14757,6 +15182,7 @@ function timelineChanges(now, changed) {
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`;