@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.js CHANGED
@@ -570,12 +570,20 @@ var compareCss = `
570
570
  display:flex;flex-direction:column;gap:6px;width:100%;height:100%;min-height:160px;outline:none;user-select:none;-webkit-user-select:none;
571
571
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
572
572
  :where(.knobs-ui) :is(.knobs-cmp-mode,.knobs-cmp-toggle){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;margin:0;padding:0;outline:none;box-shadow:none}
573
- .knobs-cmp-bar{flex:none;display:flex;align-items:center;gap:2px;height:24px;padding:0 2px}
574
- .knobs-cmp-modes{display:flex;align-items:center;gap:2px}
575
- .knobs-ui .knobs-cmp-mode,.knobs-ui .knobs-cmp-toggle{height:22px;padding:0 7px;border-radius:max(0px,calc(var(--_k-radius) - 2px));color:var(--_k-fg-3);cursor:pointer;white-space:nowrap;
576
- transition:color .15s,background .15s}
577
- .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible),.knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash)}
578
- .knobs-ui .knobs-cmp-mode[aria-checked="true"],.knobs-ui .knobs-cmp-toggle[aria-pressed="true"]{color:var(--_k-fg);font-weight:var(--_k-weight-label)}
573
+ .knobs-cmp-bar{flex:none;display:flex;align-items:center;gap:var(--_k-gap);height:var(--_k-control-h)}
574
+ /* The modes: one fill, the selection pill on the one picked; a mode isn't brighter for being
575
+ picked: the pill's bright copy is. */
576
+ .knobs-cmp-modes{position:relative;display:inline-flex;height:var(--_k-control-h);border-radius:var(--_k-radius);background:var(--_k-fill)}
577
+ .knobs-cmp-modes[hidden]{display:none}
578
+ .knobs-ui .knobs-cmp-mode{display:inline-flex;align-items:center;height:100%;padding:0 8px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:pointer;white-space:nowrap;
579
+ transition:color calc(var(--_k-hover-out) * 1ms)}
580
+ .knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible){color:var(--_k-fg)}
581
+ /* Toggles: icon buttons, the accent's wash when on. */
582
+ .knobs-ui .knobs-cmp-toggle{display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:var(--_k-radius);color:var(--_k-fg-3);cursor:pointer;
583
+ transition:color calc(var(--_k-hover-out) * 1ms),background calc(var(--_k-hover-out) * 1ms)}
584
+ .knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash)}
585
+ .knobs-ui .knobs-cmp-toggle[aria-pressed="true"]{color:var(--_k-accent-fg);background:var(--_k-accent-soft)}
586
+ .knobs-cmp-toggle svg{display:block}
579
587
  .knobs-cmp-fill{flex:1}
580
588
  .knobs-cmp-toggle[hidden]{display:none}
581
589
  /* One surface: on a card (a walkthrough's step, a split's pane, anything .knobs-surface) or marked
@@ -590,7 +598,8 @@ var compareCss = `
590
598
  /* Unzoomed, a finger still scrolls the page up and down; zoomed, the stage takes every gesture. */
591
599
  .knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:grab;touch-action:none}
592
600
  .knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:grabbing!important}
593
- .knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:ew-resize!important}
601
+ /* Wiping, the grip stands in for the cursor. */
602
+ .knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:none!important}
594
603
  .knobs-cmp-sizer{position:absolute;left:0;right:0;top:0;height:0;visibility:hidden;pointer-events:none}
595
604
  .knobs-cmp-canvas{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
596
605
  .knobs-cmp-media{position:absolute;left:0;top:0;display:block;max-width:none;margin:0;padding:0;border:0;object-fit:fill;transform-origin:0 0;pointer-events:none;will-change:transform}
@@ -598,18 +607,21 @@ var compareCss = `
598
607
  /* Glass: what's behind shows through, blurred, on the knobs tokens. */
599
608
  .knobs-cmp-label,.knobs-cmp-zoom{background:color-mix(in srgb,var(--_k-bg) 52%,transparent);-webkit-backdrop-filter:blur(14px) saturate(1.6);backdrop-filter:blur(14px) saturate(1.6);
600
609
  box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim),0 2px 10px rgba(0,0,0,calc(.18 * var(--_k-shadow-k,1)))}
601
- .knobs-cmp-label{position:absolute;left:0;top:0;padding:1px 7px;border-radius:999px;font-size:var(--_k-text-meta);line-height:16px;color:var(--_k-fg-2);white-space:nowrap;pointer-events:none;will-change:transform;transition:color .15s}
602
- .knobs-cmp-label[data-lit]{color:var(--_k-fg)}
603
- .knobs-cmp-label[data-dim]{color:var(--_k-fg-3)}
604
- /* The wipe: a hairline down the line with a grip at its middle; a wider strip takes the hand and
605
- the keys (a slider). It fades with the mode. */
610
+ .knobs-cmp-label{position:absolute;left:0;top:0;padding:1px 7px;border-radius:999px;font-size:var(--_k-text-meta);line-height:16px;color:var(--_k-fg-2);white-space:nowrap;pointer-events:none;will-change:transform;transition:color calc(var(--_k-hover-out) * 1ms)}
611
+ /* The wipe: a hairline down the line with a raised grip at the pointer's height; a wider strip
612
+ takes the hand and the keys (a slider). It fades with the mode; wiping, line and grip brighten. */
606
613
  .knobs-cmp-handle{cursor:ew-resize;position:absolute;left:-12px;top:0;width:24px;height:0;opacity:0;pointer-events:none;outline:none;will-change:transform;transition:opacity .18s cubic-bezier(.4,0,1,1)}
607
614
  .knobs-cmp-handle[data-on]{opacity:1;pointer-events:auto;transition:opacity .22s cubic-bezier(.2,.8,.2,1)}
608
- .knobs-cmp-handle::before{content:"";position:absolute;top:0;bottom:0;left:11.5px;width:1px;background:var(--_ku-cmp-line);box-shadow:0 0 0 .5px rgba(0,0,0,.28),0 0 6px rgba(0,0,0,.25)}
609
- .knobs-cmp-handle::after{content:"";position:absolute;top:50%;left:9px;width:6px;height:28px;margin-top:-14px;border-radius:3px;background:var(--_ku-cmp-line);
610
- box-shadow:0 1px 3px rgba(0,0,0,.35),0 0 0 .5px rgba(0,0,0,.2);transition:scale .18s var(--_ku-ease),box-shadow .15s}
611
- .knobs-compare[data-dragging] .knobs-cmp-handle::after{scale:1.12}
612
- .knobs-cmp-handle:focus-visible::after,.knobs-compare[data-keyboard] .knobs-cmp-handle:focus::after{box-shadow:0 1px 3px rgba(0,0,0,.35),0 0 0 3px color-mix(in srgb,var(--_k-accent) 40%,transparent)}
615
+ .knobs-cmp-handle::before{content:"";position:absolute;top:0;bottom:0;left:11.5px;width:1px;background:color-mix(in srgb,var(--_ku-cmp-line) 72%,transparent);box-shadow:0 0 0 .5px rgba(0,0,0,.28),0 0 6px rgba(0,0,0,.25)}
616
+ .knobs-compare[data-dragging] .knobs-cmp-handle::before{background:var(--_ku-cmp-line)}
617
+ .knobs-cmp-grip{position:absolute;top:0;left:4px;width:16px;height:28px;border-radius:min(8px,calc(var(--_k-radius) * 999));background:var(--_k-bg-overlay);
618
+ box-shadow:var(--_k-shadow-overlay),inset 0 0 0 1px var(--_k-border),inset 0 1px 0 var(--_k-rim);will-change:transform;transition:box-shadow calc(var(--_k-hover-out) * 1ms)}
619
+ .knobs-cmp-grip::before,.knobs-cmp-grip::after{content:"";position:absolute;top:10px;width:1px;height:8px;background:var(--_k-fg-3);transition:background calc(var(--_k-hover-out) * 1ms)}
620
+ .knobs-cmp-grip::before{left:6px}
621
+ .knobs-cmp-grip::after{left:9px}
622
+ .knobs-compare[data-dragging] .knobs-cmp-grip{box-shadow:var(--_k-shadow-overlay),inset 0 0 0 1px var(--_k-border-strong),inset 0 1px 0 var(--_k-rim)}
623
+ .knobs-compare[data-dragging] .knobs-cmp-grip::before,.knobs-compare[data-dragging] .knobs-cmp-grip::after{background:var(--_k-fg)}
624
+ .knobs-cmp-handle:focus-visible .knobs-cmp-grip,.knobs-compare[data-keyboard] .knobs-cmp-handle:focus .knobs-cmp-grip{box-shadow:var(--_k-shadow-overlay),0 0 0 2px var(--_k-accent)}
613
625
  /* The split's hairline, down the middle side by side. */
614
626
  .knobs-cmp-split{position:absolute;left:0;top:0;width:1px;height:0;margin-left:-.5px;background:color-mix(in srgb,var(--_k-bg) 72%,transparent);pointer-events:none;will-change:transform,opacity}
615
627
  /* Zoomed: how far, and a way back, in the stage's corner; it fades in and out, nothing shifts. */
@@ -618,12 +630,12 @@ var compareCss = `
618
630
  opacity:0;scale:.96;pointer-events:none;transition:opacity .16s cubic-bezier(.4,0,1,1),scale .16s cubic-bezier(.4,0,1,1)}
619
631
  .knobs-cmp-zoom[data-on]{opacity:1;scale:1;pointer-events:auto;transition:opacity .2s cubic-bezier(.2,.8,.2,1),scale .2s cubic-bezier(.2,.8,.2,1)}
620
632
  :where(.knobs-ui) .knobs-cmp-zoom-reset{all:revert;box-sizing:border-box;font:inherit;letter-spacing:inherit;margin:0;border:0;outline:none;box-shadow:none;
621
- height:20px;padding:0 8px;border-radius:999px;background:transparent;color:var(--_k-fg-2);cursor:pointer;transition:color .15s,background .15s}
633
+ display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border-radius:999px;background:transparent;color:var(--_k-fg-2);cursor:pointer;transition:color .15s,background .15s}
622
634
  :where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){color:var(--_k-fg);background:color-mix(in srgb,var(--_k-fg) 12%,transparent)}
623
635
  .knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);font-size:var(--_k-text-meta);pointer-events:none}
624
636
  .knobs-cmp-note:empty{display:none}
625
637
  .knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
626
- .knobs-cmp-timeline-host{flex:none;height:56px;min-height:0}
638
+ .knobs-cmp-timeline-host{flex:none;min-height:0}
627
639
  /* The frame strip: Before over After per column, a difference bar under them, a dot where it peaks. */
628
640
  .knobs-ui.knobs-cmp-strip{--_ku-cmp-diff:var(--k-compare-diff,var(--_k-ok));position:relative;width:100%;height:100%;min-height:40px;overflow:hidden;cursor:col-resize;touch-action:none;
629
641
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg)}
@@ -766,7 +778,7 @@ var init_picker_css = __esm(() => {
766
778
  .knobs-picker-suggested{flex:none;color:var(--_k-fg-3);font-size:var(--_k-text-meta)}
767
779
  .knobs-picker-check{flex:none;display:flex;width:14px;height:14px;color:var(--_k-accent);opacity:0;scale:.6;filter:blur(2px);
768
780
  transition:opacity .11s ease-in,scale .11s ease-in,filter .11s ease-in}
769
- .knobs-picker-check svg{display:block;width:14px;height:14px}
781
+ .knobs-picker-check svg{display:block;width:12px;height:12px}
770
782
  .knobs-picker-option[data-chosen] .knobs-picker-check{opacity:1;scale:1;filter:none;transition:opacity .15s ease-out,scale var(--kp-snappy-ms) var(--kp-snappy),filter .15s ease-out}
771
783
  .knobs-picker-body{position:relative;overflow:visible}
772
784
  .knobs-picker-body[data-moving]{overflow:hidden}
@@ -870,6 +882,15 @@ var readoutCss = `
870
882
  @media(prefers-reduced-motion:reduce){.knobs-rd :where(*){transition:none!important}}
871
883
  `;
872
884
 
885
+ // src/ui/selection-pill-css.ts
886
+ var selectionPillCss = `
887
+ .knobs-pick-pill,.knobs-pick-copy{position:absolute;left:0;top:0;width:0;height:0;pointer-events:none;transition:opacity .1s linear}
888
+ .knobs-pick-pill{background:var(--_k-fill-selected);will-change:transform}
889
+ .knobs-pick-copy{overflow:hidden;will-change:transform}
890
+ .knobs-pick-copy-rows{position:absolute;left:0;top:0;width:0;height:0}
891
+ :is(.knobs-pick-pill,.knobs-pick-copy)[data-off]{opacity:0}
892
+ `;
893
+
873
894
  // src/tokens/color.ts
874
895
  function linearToOklch([r, g, b, a]) {
875
896
  const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
@@ -2797,11 +2818,9 @@ var css2 = `
2797
2818
  .knobs-tree:focus .knobs-tree-row[data-active][aria-selected="true"]::before{background:var(--_ku-select-focus)}
2798
2819
  .knobs-tree-row[data-drop="into"]::before{opacity:1;background:color-mix(in srgb,var(--_k-accent) 30%,transparent)}
2799
2820
  .knobs-tree-row[data-dragged]{opacity:.38}
2800
- /* glide: one highlight under the selected row that glides between rows (the row's own stays off). */
2801
- .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);
2802
- opacity:0;pointer-events:none;will-change:transform;transition:opacity .15s ease-out,background .12s}
2803
- .knobs-tree-cursor[data-on]{opacity:1}
2804
- .knobs-tree:focus .knobs-tree-cursor{background:var(--_ku-select-focus)}
2821
+ /* glide: the selection pill on the selected row, in the selection's colour (the row's own stays off). */
2822
+ .knobs-tree>.knobs-pick-pill{background:var(--_ku-select)}
2823
+ .knobs-tree:focus>.knobs-pick-pill{background:var(--_ku-select-focus)}
2805
2824
  .knobs-tree[data-glide] .knobs-tree-row[aria-selected="true"]::before{opacity:0!important}
2806
2825
  .knobs-tree-row>*{position:relative}
2807
2826
  .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)}
@@ -2833,22 +2852,20 @@ var css2 = `
2833
2852
  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)}
2834
2853
  .knobs-tree-empty{padding:10px var(--_k-inset);color:var(--_k-fg-3)}
2835
2854
 
2836
- /* Timeline: a transport (play, the time) beside a track: the ruler's canvas over a lane of
2837
- markers, ranges as soft bands, and the playhead, a hairline in the accent with a cap. */
2855
+ /* Timeline: play beside a track: the ruler's canvas over a lane of markers, ranges as soft bands,
2856
+ and the playhead, a hairline in the accent with a cap. No clock: the time shows under the
2857
+ pointer on the track (the tag), in seconds, while hovering or scrubbing. */
2838
2858
  :where(.knobs-timeline,.knobs-timeline *){box-sizing:border-box}
2839
2859
  .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;
2840
2860
  font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
2841
2861
  :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}
2842
- .knobs-tl-transport{flex:none;display:flex;align-items:center;gap:8px;min-width:96px}
2862
+ .knobs-tl-transport{flex:none;display:flex;align-items:center}
2843
2863
  .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);
2844
2864
  background:var(--_k-fill-selected);color:var(--_k-fg);cursor:pointer;transition:background .15s,transform .15s var(--_ku-ease)}
2845
2865
  .knobs-ui .knobs-tl-play:hover{background:color-mix(in srgb,var(--_k-fg) 16%,transparent)}
2846
2866
  .knobs-ui .knobs-tl-play:active{transform:scale(.94)}
2847
2867
  .knobs-tl-play svg{width:12px;height:12px}
2848
2868
  .knobs-timeline[data-playing] .knobs-tl-play{background:var(--_k-accent);color:var(--_k-bg)}
2849
- .knobs-tl-time{display:flex;flex-direction:column;line-height:1.2;font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}
2850
- .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}
2851
- .knobs-tl-sub{font-size:var(--_k-text-meta);color:var(--_k-fg-3);white-space:nowrap}
2852
2869
  .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;
2853
2870
  transition:background calc(var(--_k-hover-out) * 1ms)}
2854
2871
  .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)}
@@ -2872,7 +2889,8 @@ var css2 = `
2872
2889
  .knobs-tl-pin{position:absolute;top:13px;left:-5px;width:10px;height:10px;pointer-events:auto;cursor:grab}
2873
2890
  .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)}
2874
2891
  .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);
2875
- 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}
2892
+ font-size:var(--_k-text-meta);line-height:17px;white-space:nowrap;pointer-events:auto;cursor:grab;transition:background .15s,color .15s}
2893
+ .knobs-tl-mtext{display:block;overflow:hidden;white-space:nowrap}
2876
2894
  .knobs-tl-marker[data-selected]{--knobs-tl-c:var(--_k-accent)}
2877
2895
  .knobs-tl-marker[data-selected] .knobs-tl-mlabel{background:color-mix(in srgb,var(--_k-accent) 24%,var(--_k-fill-selected))}
2878
2896
  .knobs-tl-marker[data-selected] .knobs-tl-pin::after{scale:1.2}
@@ -2893,7 +2911,22 @@ var css2 = `
2893
2911
  .knobs-timeline[data-scrubbing] .knobs-tl-hover{opacity:0}
2894
2912
  @media(prefers-reduced-motion:reduce){.knobs-timeline :where(*,::before,::after){transition:none!important}}
2895
2913
  @media(prefers-reduced-motion:reduce){:is(.knobs-tree,.knobs-split>.knobs-divider,.knobs-pane-head,.knobs-pane-rail) :where(*,::before,::after){transition:none!important}}
2896
- `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss) + rolling_css_default, RESTYLED = ".knobs-ui,.knobs-panel,.knobs-chip,.knobs-notice", lookDefaults, tune = (s, m) => {
2914
+ .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}
2915
+ .knobs-tl-tag[data-on]{opacity:1}
2916
+ /* The lane (variant: "lane"): one slim track of ticks under a compare or a video; markers are
2917
+ faint chapter ticks, the current one in text colour; nothing to edit. */
2918
+ .knobs-ui.knobs-timeline[data-variant="lane"]{height:auto;min-height:0;padding:0;gap:var(--_k-gap)}
2919
+ .knobs-timeline[data-variant="lane"] .knobs-tl-track{height:var(--_k-control-h)}
2920
+ .knobs-timeline[data-variant="lane"] .knobs-tl-ruler{height:100%}
2921
+ .knobs-timeline[data-variant="lane"] .knobs-tl-lane{top:0}
2922
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker{top:0}
2923
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker::before{top:0;bottom:50%;left:-.75px;width:1.5px;background:var(--_k-fg-3)}
2924
+ .knobs-timeline[data-variant="lane"] .knobs-tl-marker[data-current]::before{background:var(--_k-fg)}
2925
+ .knobs-timeline[data-variant="lane"] :is(.knobs-tl-pin,.knobs-tl-mlabel,.knobs-tl-rlabel){display:none}
2926
+ .knobs-timeline[data-variant="lane"] .knobs-tl-head::before{top:0}
2927
+ .knobs-timeline[data-variant="lane"] .knobs-tl-head::after{content:none}
2928
+ .knobs-timeline[data-variant="lane"] .knobs-tl-tag{top:2px;bottom:auto}
2929
+ `, uiCss = () => aliased(css2 + readoutCss + overlayCss + bentoCss + activityCss + pickerCss + compareCss + selectionPillCss) + rolling_css_default, RESTYLED = ".knobs-ui,.knobs-panel,.knobs-chip,.knobs-notice", lookDefaults, tune = (s, m) => {
2897
2930
  s.omega = m.omega;
2898
2931
  s.zeta = m.zeta;
2899
2932
  return s;
@@ -4659,6 +4692,283 @@ var init_tree_model = __esm(() => {
4659
4692
  };
4660
4693
  });
4661
4694
 
4695
+ // src/ui/icons.ts
4696
+ function icon(name, options = {}) {
4697
+ const size = options.small ? 12 : 16;
4698
+ 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>`;
4699
+ }
4700
+ var PATHS, ICON_NAMES;
4701
+ var init_icons = __esm(() => {
4702
+ PATHS = {
4703
+ 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",
4704
+ 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",
4705
+ 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",
4706
+ 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",
4707
+ 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",
4708
+ 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",
4709
+ 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",
4710
+ 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",
4711
+ 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",
4712
+ 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",
4713
+ 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",
4714
+ 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",
4715
+ 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",
4716
+ 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",
4717
+ 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",
4718
+ 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",
4719
+ 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",
4720
+ 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",
4721
+ 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",
4722
+ 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",
4723
+ 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",
4724
+ 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",
4725
+ 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",
4726
+ 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",
4727
+ 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",
4728
+ 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",
4729
+ 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"
4730
+ };
4731
+ ICON_NAMES = Object.keys(PATHS);
4732
+ });
4733
+
4734
+ // src/ui/selection-pill.ts
4735
+ function pillSpring(distance, base = SPRINGS.default) {
4736
+ return distance > 400 ? { duration: base.duration * 1.4, bounce: base.bounce } : base;
4737
+ }
4738
+ function pillBoxOf(row, container) {
4739
+ return {
4740
+ x: row.left - container.left - container.clientLeft + container.scrollLeft,
4741
+ y: row.top - container.top - container.clientTop + container.scrollTop,
4742
+ w: row.width,
4743
+ h: row.height
4744
+ };
4745
+ }
4746
+ function pillRadius(from, to, h, k) {
4747
+ const t = Math.abs(to.h - from.h) > 0.5 ? (h - from.h) / (to.h - from.h) : Number.isFinite(k) ? k : 1;
4748
+ return from.r + (to.r - from.r) * Math.min(1, Math.max(0, t));
4749
+ }
4750
+ function pillProgress(from, to, at) {
4751
+ const d = Math.hypot(to.x - from.x, to.y - from.y);
4752
+ if (d < 0.5)
4753
+ return 1;
4754
+ const p = ((at.x - from.x) * (to.x - from.x) + (at.y - from.y) * (to.y - from.y)) / (d * d);
4755
+ return Math.min(1, Math.max(0, p));
4756
+ }
4757
+ function planPill(state, next, flags = {}) {
4758
+ if (!next)
4759
+ return { kind: "hide" };
4760
+ if (!state.shown || !state.at || flags.first || flags.reduced || flags.snap)
4761
+ return { kind: "land", box: next };
4762
+ const t = state.target;
4763
+ if (t && t.x === next.x && t.y === next.y && t.w === next.w && t.h === next.h)
4764
+ return { kind: "stay" };
4765
+ const d = Math.hypot(next.x - state.at.x, next.y - state.at.y);
4766
+ return { kind: "travel", box: next, spring: pillSpring(d) };
4767
+ }
4768
+ function selectionPill(container, options) {
4769
+ const doc = container.ownerDocument;
4770
+ const win = doc.defaultView;
4771
+ injectUiStyles(doc);
4772
+ if (win.getComputedStyle(container).position === "static")
4773
+ container.style.position = "relative";
4774
+ const pill = doc.createElement("i");
4775
+ pill.className = "knobs-pick-pill";
4776
+ pill.setAttribute("aria-hidden", "true");
4777
+ pill.dataset.off = "";
4778
+ const copyOn = options.copy !== false;
4779
+ const copy = doc.createElement("div");
4780
+ copy.className = "knobs-pick-copy";
4781
+ copy.setAttribute("aria-hidden", "true");
4782
+ copy.inert = true;
4783
+ copy.dataset.off = "";
4784
+ const copyRows = doc.createElement("div");
4785
+ copyRows.className = "knobs-pick-copy-rows";
4786
+ copy.append(copyRows);
4787
+ container.prepend(pill);
4788
+ if (copyOn)
4789
+ container.append(copy);
4790
+ const { omega, zeta } = springMotion(SPRINGS.default);
4791
+ const springs = [0, 0, 0, 0].map(() => new Spring(0, omega, zeta, 0.01));
4792
+ const [sx, sy, sw, sh] = springs;
4793
+ let shown = false;
4794
+ let first = true;
4795
+ let target = null;
4796
+ let from = { x: 0, y: 0, w: 0, h: 0 };
4797
+ let fromR = 0;
4798
+ let toR = 0;
4799
+ let rowBoxes = [];
4800
+ const clones = new Map;
4801
+ let disposed = false;
4802
+ const radiusOf = (row) => options.radius?.(row) ?? (Number.parseFloat(win.getComputedStyle(row).borderTopLeftRadius) || 0);
4803
+ const rowsNow = () => options.rows ? [...options.rows()] : [...container.children].filter((n) => n !== pill && n !== copy);
4804
+ const measure = (row) => {
4805
+ const c = container.getBoundingClientRect();
4806
+ const r = row.getBoundingClientRect();
4807
+ return pillBoxOf(r, {
4808
+ left: c.left,
4809
+ top: c.top,
4810
+ scrollLeft: container.scrollLeft,
4811
+ scrollTop: container.scrollTop,
4812
+ clientLeft: container.clientLeft,
4813
+ clientTop: container.clientTop
4814
+ });
4815
+ };
4816
+ const at = () => ({ x: sx.x, y: sy.x, w: sw.x, h: sh.x });
4817
+ const syncCopy = (span) => {
4818
+ if (!copyOn)
4819
+ return;
4820
+ const keep = new Set;
4821
+ for (const { row, box } of rowBoxes) {
4822
+ if (!boxesMeet(box, span))
4823
+ continue;
4824
+ keep.add(row);
4825
+ let n = clones.get(row);
4826
+ if (!n) {
4827
+ n = row.cloneNode(true);
4828
+ n.removeAttribute("id");
4829
+ for (const d of n.querySelectorAll("[id]"))
4830
+ d.removeAttribute("id");
4831
+ clones.set(row, n);
4832
+ copyRows.append(n);
4833
+ }
4834
+ const s = n.style;
4835
+ s.position = "absolute";
4836
+ s.inset = "auto";
4837
+ s.left = `${box.x}px`;
4838
+ s.top = `${box.y}px`;
4839
+ s.width = `${box.w}px`;
4840
+ s.height = `${box.h}px`;
4841
+ s.margin = "0";
4842
+ s.transform = "none";
4843
+ s.clipPath = "";
4844
+ s.opacity = "";
4845
+ s.boxSizing = "border-box";
4846
+ s.color = options.color ?? "var(--_k-fg)";
4847
+ }
4848
+ for (const [row, n] of clones)
4849
+ if (!keep.has(row)) {
4850
+ n.remove();
4851
+ clones.delete(row);
4852
+ }
4853
+ };
4854
+ const paint = () => {
4855
+ const b = at();
4856
+ const x0 = grid(b.x);
4857
+ const y0 = grid(b.y);
4858
+ const w = grid(b.x + b.w) - x0;
4859
+ const h = grid(b.y + b.h) - y0;
4860
+ const r = target ? pillRadius({ h: from.h, r: fromR }, { h: target.h, r: toR }, b.h, pillProgress(from, target, b)) : toR;
4861
+ const radius = `${Math.max(0, Math.min(r, h / 2, w / 2)).toFixed(2)}px`;
4862
+ for (const n of copyOn ? [pill, copy] : [pill]) {
4863
+ n.style.transform = `translate3d(${x0}px,${y0}px,0)`;
4864
+ n.style.width = `${w}px`;
4865
+ n.style.height = `${h}px`;
4866
+ n.style.borderRadius = radius;
4867
+ }
4868
+ if (copyOn)
4869
+ copyRows.style.transform = `translate3d(${-x0}px,${-y0}px,0)`;
4870
+ };
4871
+ const loop = frameLoop(win, (dt) => {
4872
+ for (const s of springs)
4873
+ s.step(dt);
4874
+ paint();
4875
+ return springs.some((s) => !s.resting);
4876
+ });
4877
+ const show = (on) => {
4878
+ shown = on;
4879
+ pill.toggleAttribute("data-off", !on);
4880
+ copy.toggleAttribute("data-off", !on);
4881
+ container.toggleAttribute("data-pick-empty", !on);
4882
+ };
4883
+ const update = (o) => {
4884
+ if (disposed)
4885
+ return;
4886
+ const row = options.selected();
4887
+ const next = row?.isConnected ? measure(row) : null;
4888
+ const plan = planPill({ shown, at: shown ? at() : null, target }, next, {
4889
+ first,
4890
+ reduced: reducedMotion(win),
4891
+ ...o?.snap ? { snap: true } : {}
4892
+ });
4893
+ first = false;
4894
+ for (const n of clones.values())
4895
+ n.remove();
4896
+ clones.clear();
4897
+ rowBoxes = copyOn ? rowsNow().filter((r) => r.isConnected).map((r) => ({ row: r, box: measure(r) })) : [];
4898
+ if (plan.kind === "hide") {
4899
+ if (shown)
4900
+ show(false);
4901
+ return;
4902
+ }
4903
+ if (plan.kind === "stay") {
4904
+ syncCopy(boxUnion(at(), target));
4905
+ return;
4906
+ }
4907
+ const r = radiusOf(row);
4908
+ if (plan.kind === "land") {
4909
+ sx.snap(plan.box.x);
4910
+ sy.snap(plan.box.y);
4911
+ sw.snap(plan.box.w);
4912
+ sh.snap(plan.box.h);
4913
+ from = plan.box;
4914
+ fromR = r;
4915
+ toR = r;
4916
+ target = plan.box;
4917
+ syncCopy(plan.box);
4918
+ paint();
4919
+ loop.stop();
4920
+ if (!shown)
4921
+ show(true);
4922
+ return;
4923
+ }
4924
+ const m = springMotion(plan.spring);
4925
+ for (const s of springs) {
4926
+ s.omega = m.omega;
4927
+ s.zeta = m.zeta;
4928
+ }
4929
+ const now = at();
4930
+ fromR = target ? pillRadius({ h: from.h, r: fromR }, { h: target.h, r: toR }, now.h, pillProgress(from, target, now)) : r;
4931
+ from = now;
4932
+ toR = r;
4933
+ target = plan.box;
4934
+ sx.set(plan.box.x);
4935
+ sy.set(plan.box.y);
4936
+ sw.set(plan.box.w);
4937
+ sh.set(plan.box.h);
4938
+ syncCopy(boxUnion(now, plan.box));
4939
+ loop.wake();
4940
+ };
4941
+ const ro = "ResizeObserver" in win ? new win.ResizeObserver(() => update({ snap: true })) : null;
4942
+ ro?.observe(container);
4943
+ update();
4944
+ return {
4945
+ element: pill,
4946
+ update,
4947
+ dispose() {
4948
+ disposed = true;
4949
+ loop.stop();
4950
+ ro?.disconnect();
4951
+ pill.remove();
4952
+ copy.remove();
4953
+ container.removeAttribute("data-pick-empty");
4954
+ }
4955
+ };
4956
+ }
4957
+ 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) => {
4958
+ const x = Math.min(a.x, b.x);
4959
+ const y = Math.min(a.y, b.y);
4960
+ return {
4961
+ x,
4962
+ y,
4963
+ w: Math.max(a.x + a.w, b.x + b.w) - x,
4964
+ h: Math.max(a.y + a.h, b.y + b.h) - y
4965
+ };
4966
+ }, grid = (v) => Math.round(v * 64) / 64;
4967
+ var init_selection_pill = __esm(() => {
4968
+ init_motion();
4969
+ init_look();
4970
+ });
4971
+
4662
4972
  // src/core/storage.ts
4663
4973
  function browserStorage() {
4664
4974
  return typeof window === "undefined" ? null : window.localStorage;
@@ -4981,10 +5291,25 @@ function advance(t, dt, o) {
4981
5291
  }
4982
5292
  return { t: hi, ended: true, wrapped: false };
4983
5293
  }
4984
- 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;
5294
+ function moveHead(head, t, motion, catching, minGap = 0) {
5295
+ if (motion === "jump" && Math.abs(t - head.x) > minGap)
5296
+ return { x: head.x, target: t, catching: true };
5297
+ if (catching)
5298
+ return { x: head.x + (t - head.target), target: t, catching: true };
5299
+ return { x: t, target: t, catching: false };
5300
+ }
5301
+ function currentMarker(markers, t, fps) {
5302
+ let best = null;
5303
+ for (const m of markers)
5304
+ if (m.time <= t + 0.5 / fps && (!best || m.time >= best.time))
5305
+ best = m;
5306
+ return best;
5307
+ }
5308
+ 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;
4985
5309
  var init_timeline_model = __esm(() => {
4986
5310
  SECOND_STEPS = [0.1, 0.2, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600];
4987
5311
  FRAME_STEPS = [1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 9000, 18000];
5312
+ CATCH_UP = { duration: 0.15, bounce: 0 };
4988
5313
  });
4989
5314
 
4990
5315
  // src/ui/timeline.ts
@@ -4997,6 +5322,7 @@ function timeline(container, options) {
4997
5322
  let speed = options.speed ?? 1;
4998
5323
  let repeat = options.repeat ?? true;
4999
5324
  const arrows = options.arrows ?? "frames";
5325
+ const lane = options.variant === "lane";
5000
5326
  let time = Math.min(duration, Math.max(0, options.time ?? 0));
5001
5327
  let playing = false;
5002
5328
  let markers = [...options.markers ?? []];
@@ -5030,27 +5356,27 @@ function timeline(container, options) {
5030
5356
  root.setAttribute("aria-label", options.label ?? "Timeline");
5031
5357
  root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight , . Home End [ ] I O M");
5032
5358
  applyLook2(root, options);
5359
+ if (lane)
5360
+ root.dataset.variant = "lane";
5033
5361
  const transport = el("div", "knobs-tl-transport");
5034
5362
  const playBtn = el("button", "knobs-tl-play");
5035
5363
  playBtn.type = "button";
5036
5364
  playBtn.tabIndex = -1;
5037
- const readout = el("div", "knobs-tl-time");
5038
- const now = el("span", "knobs-tl-now");
5039
- const sub = el("span", "knobs-tl-sub");
5040
- readout.append(now, sub);
5041
- transport.append(playBtn, readout);
5365
+ transport.append(playBtn);
5042
5366
  const track = el("div", "knobs-tl-track");
5043
5367
  const canvas = el("canvas", "knobs-tl-ruler");
5044
5368
  canvas.setAttribute("aria-hidden", "true");
5045
- const lane = el("div", "knobs-tl-lane");
5369
+ const markerLane = el("div", "knobs-tl-lane");
5046
5370
  const hover = el("div", "knobs-tl-hover");
5371
+ const tag = el("span", "knobs-tl-tag");
5372
+ tag.setAttribute("aria-hidden", "true");
5047
5373
  const head = el("div", "knobs-tl-head");
5048
5374
  head.id = `${id}-head`;
5049
5375
  head.setAttribute("role", "slider");
5050
5376
  head.setAttribute("aria-label", "Playhead");
5051
5377
  head.setAttribute("aria-valuemin", "0");
5052
5378
  head.setAttribute("aria-valuemax", String(duration));
5053
- track.append(canvas, lane, hover, head);
5379
+ track.append(canvas, markerLane, hover, head, tag);
5054
5380
  root.append(transport, track);
5055
5381
  container.append(root);
5056
5382
  let width = 1;
@@ -5097,13 +5423,14 @@ function timeline(container, options) {
5097
5423
  let drawn = "";
5098
5424
  const drawRuler = () => {
5099
5425
  const v = view();
5100
- const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${units}:${fps}`;
5426
+ const key = `${v.start.toFixed(6)}:${v.span.toFixed(6)}:${width}:${height}:${units}:${fps}`;
5101
5427
  if (key === drawn)
5102
5428
  return;
5103
5429
  drawn = key;
5104
5430
  const dpr = win.devicePixelRatio || 1;
5105
5431
  const w = Math.max(1, Math.round(width * dpr));
5106
- const h = Math.round(RULER * dpr);
5432
+ const rh = lane ? height : RULER;
5433
+ const h = Math.round(rh * dpr);
5107
5434
  if (canvas.width !== w || canvas.height !== h) {
5108
5435
  canvas.width = w;
5109
5436
  canvas.height = h;
@@ -5112,8 +5439,19 @@ function timeline(container, options) {
5112
5439
  if (!ctx)
5113
5440
  return;
5114
5441
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
5115
- ctx.clearRect(0, 0, width, RULER);
5442
+ ctx.clearRect(0, 0, width, rh);
5116
5443
  const ticks = rulerTicks(v, width, { fps, units });
5444
+ if (lane) {
5445
+ ctx.fillStyle = colors.tick;
5446
+ for (const t of ticks.minor)
5447
+ if (t >= -0.000000001 && t <= duration + 0.000000001)
5448
+ ctx.fillRect(xOf(t) - 0.5, rh - 4, 1, 4);
5449
+ ctx.fillStyle = colors.major;
5450
+ for (const m of ticks.major)
5451
+ if (m.t >= -0.000000001 && m.t <= duration + 0.000000001)
5452
+ ctx.fillRect(xOf(m.t) - 0.5, rh - 7, 1, 7);
5453
+ return;
5454
+ }
5117
5455
  const px = (t) => Math.round(xOf(t)) + 0.5;
5118
5456
  ctx.fillStyle = colors.line;
5119
5457
  ctx.fillRect(0, RULER - 1, width, 1);
@@ -5153,18 +5491,21 @@ function timeline(container, options) {
5153
5491
  n.dataset.id = m.id;
5154
5492
  const pin = el("span", "knobs-tl-pin");
5155
5493
  const label = el("span", "knobs-tl-mlabel");
5494
+ label.append(el("span", "knobs-tl-mtext knobs-scroll-fade-x"));
5156
5495
  n.append(pin, label);
5157
- lane.append(n);
5496
+ markerLane.append(n);
5158
5497
  markerEls.set(m.id, n);
5159
5498
  if (built)
5160
5499
  enter(n);
5161
5500
  }
5162
5501
  const label = n.querySelector(".knobs-tl-mlabel");
5163
- if (label.textContent !== (m.label ?? ""))
5164
- label.textContent = m.label ?? "";
5502
+ const text = label.firstElementChild;
5503
+ if (text.textContent !== (m.label ?? ""))
5504
+ text.textContent = m.label ?? "";
5165
5505
  label.hidden = !m.label;
5166
5506
  n.style.setProperty("--knobs-tl-color", m.color ?? "");
5167
5507
  n.toggleAttribute("data-selected", selected?.kind === "marker" && selected.id === m.id);
5508
+ n.toggleAttribute("data-current", m.id === currentId);
5168
5509
  n.title = `${m.label ? `${m.label} · ` : ""}${clock(m.time)}`;
5169
5510
  }
5170
5511
  placeMarkers();
@@ -5192,7 +5533,7 @@ function timeline(container, options) {
5192
5533
  b.dataset.edge = "end";
5193
5534
  const label = el("span", "knobs-tl-rlabel");
5194
5535
  n.append(label, a, b);
5195
- track.insertBefore(n, lane);
5536
+ track.insertBefore(n, markerLane);
5196
5537
  rangeEls.set(r.id, n);
5197
5538
  if (built)
5198
5539
  enter(n);
@@ -5241,16 +5582,17 @@ function timeline(container, options) {
5241
5582
  };
5242
5583
  const clock = (t) => units === "frames" ? `f${toFrame(t, fps)}` : `${timeLabel(t, "seconds", fps, 0.01)}`;
5243
5584
  let shownHead = time;
5585
+ let currentId = null;
5244
5586
  const renderHead = () => {
5245
5587
  head.style.transform = `translateX(${xOf(shownHead).toFixed(2)}px)`;
5246
- const main = units === "frames" ? `f${toFrame(time, fps)}` : `${time.toFixed(2)}s`;
5247
- const minor = units === "frames" ? `${time.toFixed(2)}s` : `f${toFrame(time, fps)}`;
5248
- if (now.textContent !== main)
5249
- now.textContent = main;
5250
- if (sub.textContent !== minor)
5251
- sub.textContent = minor;
5252
5588
  head.setAttribute("aria-valuenow", time.toFixed(3));
5253
5589
  head.setAttribute("aria-valuetext", `${time.toFixed(2)} seconds, frame ${toFrame(time, fps)}`);
5590
+ const cur = currentMarker(markers, time, fps)?.id ?? null;
5591
+ if (cur !== currentId) {
5592
+ markerEls.get(currentId ?? "")?.removeAttribute("data-current");
5593
+ currentId = cur;
5594
+ markerEls.get(cur ?? "")?.setAttribute("data-current", "");
5595
+ }
5254
5596
  };
5255
5597
  let viewSent = "";
5256
5598
  const render = () => {
@@ -5276,18 +5618,27 @@ function timeline(container, options) {
5276
5618
  };
5277
5619
  const caret = new Spring(time, GLIDE.omega, GLIDE.zeta, 0.00001);
5278
5620
  let gliding = false;
5279
- const setTime = (t, source, shown = t, glide = false) => {
5280
- const next = Math.min(duration, Math.max(0, t));
5281
- if (glide && !reducedMotion(win)) {
5621
+ const headTo = (shown, motion) => {
5622
+ const from = gliding ? { x: caret.x, target: caret.target } : { x: shownHead, target: shownHead };
5623
+ const jump = motion === "jump" && !reducedMotion(win);
5624
+ const next = moveHead(from, shown, jump ? "jump" : "follow", gliding, 2 / pxPerSec());
5625
+ if (next.catching) {
5282
5626
  if (!gliding)
5283
- caret.snap(shownHead);
5284
- caret.set(next);
5627
+ caret.snap(from.x);
5628
+ caret.x = next.x;
5629
+ caret.target = next.target;
5285
5630
  gliding = true;
5631
+ shownHead = caret.x;
5286
5632
  loop.wake();
5287
5633
  } else {
5288
5634
  gliding = false;
5289
- shownHead = shown;
5635
+ caret.snap(next.x);
5636
+ shownHead = next.x;
5290
5637
  }
5638
+ };
5639
+ const setTime = (t, source, shown = t, glide = false) => {
5640
+ const next = Math.min(duration, Math.max(0, t));
5641
+ headTo(Math.min(duration, Math.max(0, shown)), glide ? "jump" : "follow");
5291
5642
  const moved = next !== time;
5292
5643
  time = next;
5293
5644
  renderHead();
@@ -5328,8 +5679,7 @@ function timeline(container, options) {
5328
5679
  options.onCross?.(m);
5329
5680
  }
5330
5681
  time = step.t;
5331
- shownHead = time;
5332
- gliding = false;
5682
+ headTo(time, step.wrapped ? "jump" : "follow");
5333
5683
  emitSeek(time, "play");
5334
5684
  keepInView();
5335
5685
  if (step.ended) {
@@ -5342,7 +5692,7 @@ function timeline(container, options) {
5342
5692
  shownHead = caret.step(dt);
5343
5693
  if (caret.resting) {
5344
5694
  gliding = false;
5345
- shownHead = time;
5695
+ shownHead = caret.target;
5346
5696
  }
5347
5697
  }
5348
5698
  vStart.step(dt);
@@ -5355,9 +5705,9 @@ function timeline(container, options) {
5355
5705
  return;
5356
5706
  const r = loopRange();
5357
5707
  if (r && (time < r.start || time >= r.end - 0.5 / fps))
5358
- setTime(r.start, "code");
5708
+ setTime(r.start, "code", r.start, true);
5359
5709
  else if (!r && time >= duration - 0.5 / fps)
5360
- setTime(0, "code");
5710
+ setTime(0, "code", 0, true);
5361
5711
  playing = true;
5362
5712
  syncPlay();
5363
5713
  emitPlay(true);
@@ -5486,24 +5836,35 @@ function timeline(container, options) {
5486
5836
  };
5487
5837
  const localX = (e) => e.clientX - track.getBoundingClientRect().left;
5488
5838
  let drag = null;
5489
- const scrubTo = (x) => {
5839
+ const scrubTo = (x, press = false) => {
5490
5840
  const shownX = softClamp(x, xOf(0), xOf(duration), 10);
5491
5841
  const s = scrubTime(tOf(shownX), {
5492
5842
  duration,
5493
5843
  fps,
5494
5844
  pxPerSec: pxPerSec(),
5495
- stops: snapMarkers ? markers.map((m) => m.time) : [],
5845
+ stops: snapMarkers && !lane ? markers.map((m) => m.time) : [],
5496
5846
  frames: snapFrames
5497
5847
  });
5498
- setTime(s.t, "scrub", s.shown);
5848
+ setTime(s.t, "scrub", s.shown, press);
5499
5849
  root.toggleAttribute("data-stuck", s.stop !== null);
5500
5850
  };
5851
+ const showTag = (x) => {
5852
+ const at = Math.min(duration, Math.max(0, tOf(x)));
5853
+ const text = `${at.toFixed(2)} s`;
5854
+ if (tag.textContent !== text)
5855
+ tag.textContent = text;
5856
+ const cx = Math.min(width, Math.max(0, x));
5857
+ const flip = cx > width - tag.offsetWidth - 12;
5858
+ tag.style.transform = `translateX(${cx.toFixed(1)}px) translateX(${flip ? "calc(-100% - 4px)" : "4px"})`;
5859
+ tag.dataset.on = "";
5860
+ };
5861
+ const hideTag = () => delete tag.dataset.on;
5501
5862
  track.addEventListener("pointerdown", (e) => {
5502
5863
  if (e.button !== 0 || editing)
5503
5864
  return;
5504
5865
  root.focus({ preventScroll: true });
5505
5866
  const t = e.target;
5506
- const markerEl = t.closest(".knobs-tl-marker");
5867
+ const markerEl = lane ? null : t.closest(".knobs-tl-marker");
5507
5868
  const edgeEl = t.closest(".knobs-tl-edge");
5508
5869
  const rangeEl = t.closest(".knobs-tl-range");
5509
5870
  track.setPointerCapture(e.pointerId);
@@ -5548,13 +5909,16 @@ function timeline(container, options) {
5548
5909
  root.dataset.scrubbing = "";
5549
5910
  for (const l of listeners)
5550
5911
  l.onScrub?.(true);
5551
- scrubTo(localX(e));
5912
+ scrubTo(localX(e), true);
5913
+ showTag(localX(e));
5552
5914
  }
5553
5915
  e.preventDefault();
5554
5916
  });
5555
5917
  track.addEventListener("pointermove", (e) => {
5556
5918
  const x = localX(e);
5557
5919
  hover.style.transform = `translateX(${x.toFixed(1)}px)`;
5920
+ if (!drag || drag.kind === "scrub")
5921
+ showTag(x);
5558
5922
  if (!drag)
5559
5923
  return;
5560
5924
  if (drag.kind === "scrub")
@@ -5600,8 +5964,10 @@ function timeline(container, options) {
5600
5964
  delete root.dataset.dragging;
5601
5965
  root.removeAttribute("data-stuck");
5602
5966
  if (d.kind === "scrub") {
5603
- shownHead = time;
5967
+ headTo(time, "follow");
5604
5968
  renderHead();
5969
+ if (!track.matches(":hover"))
5970
+ hideTag();
5605
5971
  for (const l of listeners)
5606
5972
  l.onScrub?.(false);
5607
5973
  if (d.wasPlaying)
@@ -5614,7 +5980,7 @@ function timeline(container, options) {
5614
5980
  } else {
5615
5981
  const m = markers.find((x) => x.id === d.id);
5616
5982
  if (m)
5617
- setTime(m.time, "marker");
5983
+ setTime(m.time, "marker", m.time, true);
5618
5984
  }
5619
5985
  } else if (d.kind === "edge" || d.moved)
5620
5986
  emitRanges();
@@ -5623,10 +5989,14 @@ function timeline(container, options) {
5623
5989
  track.addEventListener("pointercancel", endDrag);
5624
5990
  track.addEventListener("lostpointercapture", endDrag);
5625
5991
  track.addEventListener("pointerleave", () => {
5626
- if (!drag)
5627
- hover.style.transform = "translateX(-100px)";
5992
+ if (drag)
5993
+ return;
5994
+ hover.style.transform = "translateX(-100px)";
5995
+ hideTag();
5628
5996
  });
5629
5997
  track.addEventListener("dblclick", (e) => {
5998
+ if (lane)
5999
+ return;
5630
6000
  const markerEl = e.target.closest(".knobs-tl-marker");
5631
6001
  if (markerEl)
5632
6002
  return rename(markerEl.dataset.id);
@@ -5637,6 +6007,8 @@ function timeline(container, options) {
5637
6007
  rename(m.id);
5638
6008
  });
5639
6009
  track.addEventListener("wheel", (e) => {
6010
+ if (lane)
6011
+ return;
5640
6012
  e.preventDefault();
5641
6013
  const scale = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? height : 1;
5642
6014
  if (e.ctrlKey || e.metaKey) {
@@ -5700,7 +6072,7 @@ function timeline(container, options) {
5700
6072
  if (!m)
5701
6073
  return;
5702
6074
  select({ kind: "marker", id: m.id });
5703
- return setTime(m.time, "marker");
6075
+ return setTime(m.time, "marker", m.time, true);
5704
6076
  }
5705
6077
  case "i":
5706
6078
  case "I":
@@ -5712,11 +6084,13 @@ function timeline(container, options) {
5712
6084
  return setEdge("end", time);
5713
6085
  case "m":
5714
6086
  case "M":
6087
+ if (lane)
6088
+ return;
5715
6089
  claim();
5716
6090
  addMarker(time);
5717
6091
  return;
5718
6092
  case "Enter":
5719
- if (selected?.kind === "marker") {
6093
+ if (selected?.kind === "marker" && !lane) {
5720
6094
  claim();
5721
6095
  rename(selected.id);
5722
6096
  }
@@ -5734,19 +6108,25 @@ function timeline(container, options) {
5734
6108
  return;
5735
6109
  case "=":
5736
6110
  case "+":
6111
+ if (lane)
6112
+ return;
5737
6113
  claim();
5738
6114
  return zoom(1.5);
5739
6115
  case "-":
6116
+ if (lane)
6117
+ return;
5740
6118
  claim();
5741
6119
  return zoom(1 / 1.5);
5742
6120
  case "0":
6121
+ if (lane)
6122
+ return;
5743
6123
  claim();
5744
6124
  return fit();
5745
6125
  }
5746
6126
  });
5747
6127
  const unregisterHints = options.hints === false ? undefined : hints.register(root, () => [
5748
6128
  { el: playBtn, label: playing ? "Pause" : "Play", activate: toggle },
5749
- ...markers.flatMap((m) => {
6129
+ ...(lane ? [] : markers).flatMap((m) => {
5750
6130
  const n = markerEls.get(m.id);
5751
6131
  if (!n || n.dataset.leaving !== undefined)
5752
6132
  return [];
@@ -5758,7 +6138,7 @@ function timeline(container, options) {
5758
6138
  ...label && !label.hidden ? { labelEl: label } : {},
5759
6139
  activate: () => {
5760
6140
  select({ kind: "marker", id: m.id });
5761
- setTime(m.time, "marker");
6141
+ setTime(m.time, "marker", m.time, true);
5762
6142
  root.focus({ preventScroll: true });
5763
6143
  }
5764
6144
  }
@@ -5770,7 +6150,7 @@ function timeline(container, options) {
5770
6150
  activate: () => root.focus({ preventScroll: true })
5771
6151
  }
5772
6152
  ], { listen: false, rank: 1 });
5773
- const menu = contextMenu(track, (e) => {
6153
+ const menu = lane ? null : contextMenu(track, (e) => {
5774
6154
  const t = e?.target ?? null;
5775
6155
  const markerEl = t?.closest(".knobs-tl-marker");
5776
6156
  const rangeEl = t?.closest(".knobs-tl-range");
@@ -5785,7 +6165,7 @@ function timeline(container, options) {
5785
6165
  run: () => {
5786
6166
  const m = markers.find((x) => x.id === mid);
5787
6167
  if (m)
5788
- setTime(m.time, "marker");
6168
+ setTime(m.time, "marker", m.time, true);
5789
6169
  }
5790
6170
  },
5791
6171
  { label: "Rename…", shortcut: "Enter", run: () => rename(mid) },
@@ -5949,7 +6329,7 @@ function timeline(container, options) {
5949
6329
  listeners.clear();
5950
6330
  loop.stop();
5951
6331
  ro.disconnect();
5952
- menu.dispose();
6332
+ menu?.dispose();
5953
6333
  root.remove();
5954
6334
  }
5955
6335
  };
@@ -5966,10 +6346,21 @@ var init_timeline = __esm(() => {
5966
6346
  init_timeline_model();
5967
6347
  INSTANT = springMotion(SPRINGS.instant);
5968
6348
  CALM = springMotion(SPRINGS.calm);
5969
- GLIDE = springMotion(SPRINGS.snappy);
6349
+ GLIDE = springMotion(CATCH_UP);
5970
6350
  });
5971
6351
 
5972
6352
  // src/ui/compare-model.ts
6353
+ function compareModes(modes, mode) {
6354
+ const list = [...new Set((modes ?? ["side", "wipe"]).filter((m) => ALL_MODES.includes(m)))];
6355
+ const offered = list.length ? list : ["side", "wipe"];
6356
+ return { modes: offered, mode: mode && offered.includes(mode) ? mode : offered[0] };
6357
+ }
6358
+ function modeForKey(key, modes) {
6359
+ const k = key.toLowerCase();
6360
+ if (/^[1-9]$/.test(k))
6361
+ return modes[Number(k) - 1] ?? null;
6362
+ return modes.find((m) => MODE_KEYS[m] === k) ?? null;
6363
+ }
5973
6364
  function mediaKind(src, kind) {
5974
6365
  if (kind)
5975
6366
  return kind;
@@ -6048,6 +6439,18 @@ function wipeGrab(x, rect, wipe, slop = 12) {
6048
6439
  const onLine = Math.abs(x - line) <= slop;
6049
6440
  return { onLine, offset: onLine ? x - line : 0 };
6050
6441
  }
6442
+ function seamLabels(x, left, right, wb, wa, gap = 8) {
6443
+ const room = (space, w) => Math.min(1, Math.max(0, (space - w - 2 * gap) / gap));
6444
+ return {
6445
+ before: { x: x - gap - wb, opacity: room(x - left, wb) },
6446
+ after: { x: x + gap, opacity: room(right - x, wa) }
6447
+ };
6448
+ }
6449
+ function gripTop(y, rect, grip, margin = 8) {
6450
+ const lo = rect.y + margin;
6451
+ const hi = rect.y + rect.h - margin - grip;
6452
+ return hi < lo ? rect.y + (rect.h - grip) / 2 : Math.min(hi, Math.max(lo, y - grip / 2));
6453
+ }
6051
6454
  function stripColumns(view, width, o) {
6052
6455
  const { fps } = o;
6053
6456
  const total = Math.max(1, Math.round(o.duration * fps));
@@ -6098,7 +6501,7 @@ function stripPeaks(diffs, o = {}) {
6098
6501
  });
6099
6502
  return out;
6100
6503
  }
6101
- 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), seekTime = (t, fps, duration) => frameMidpoint(frameOf(t, fps), fps, duration), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, splitHeight = (w, media) => media.w > 0 && media.h > 0 ? w / 2 * (media.h / media.w) : w / 2 * (9 / 16), ZOOM_FIT, ZOOM_MAX = 16, panBy = (r, dx, dy, base) => clampZoom({
6504
+ var ALL_MODES, MODE_KEYS, modeKey = (mode) => MODE_KEYS[mode].toUpperCase(), VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), lastFrame = (duration, fps) => Math.max(0, Math.ceil(duration * fps - 0.0001) - 1), seekTime = (t, fps, duration) => frameMidpoint(frameOf(t, fps), fps, duration), sameFrame = (a, b, fps) => frameOf(a, fps) === frameOf(b, fps), SEEK_IDLE, splitHeight = (w, media) => media.w > 0 && media.h > 0 ? w / 2 * (media.h / media.w) : w / 2 * (9 / 16), ZOOM_FIT, ZOOM_MAX = 16, panBy = (r, dx, dy, base) => clampZoom({
6102
6505
  u: r.u - dx / Math.max(1, base.w) * r.w,
6103
6506
  v: r.v - dy / Math.max(1, base.h) * r.h,
6104
6507
  w: r.w
@@ -6117,6 +6520,8 @@ var VIDEO, frameOf = (t, fps) => Math.max(0, Math.floor(t * fps + 0.0001)), last
6117
6520
  h: a.h + (b.h - a.h) * k
6118
6521
  }), 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));
6119
6522
  var init_compare_model = __esm(() => {
6523
+ ALL_MODES = ["side", "wipe", "difference"];
6524
+ MODE_KEYS = { side: "s", wipe: "w", difference: "d" };
6120
6525
  VIDEO = /\.(mp4|m4v|mov|webm|ogv|mkv)$/i;
6121
6526
  SEEK_IDLE = { seeking: false, pending: null };
6122
6527
  ZOOM_FIT = { u: 0, v: 0, w: 1, h: 1 };
@@ -6916,8 +7321,8 @@ async function createCompareGpu(canvas, onLost) {
6916
7321
  r(0, f.before);
6917
7322
  r(4, f.after);
6918
7323
  data.set([f.width, f.height, f.dpr, 0], 8);
6919
- data.set([f.wipeX, f.soft, 0, f.phase ? 1 : 0], 12);
6920
- data.set([f.from, f.to, f.mix, f.flick ? 1 : 0], 16);
7324
+ data.set([f.wipeX, f.soft, 0, 0], 12);
7325
+ data.set([f.from, f.to, f.mix, 0], 16);
6921
7326
  data.set([f.threshold, f.gain, ready[0], ready[1]], 20);
6922
7327
  data.set([f.accent[0], f.accent[1], f.accent[2], 1], 24);
6923
7328
  const l = f.loupe;
@@ -6959,8 +7364,8 @@ var MODE_INDEX, WGSL = `
6959
7364
  struct U {
6960
7365
  ra: vec4f, rb: vec4f,
6961
7366
  view: vec4f, // w, h, dpr, _
6962
- wipe: vec4f, // x, soft, _, phase
6963
- modes: vec4f, // from, to, mix, flick
7367
+ wipe: vec4f, // x, soft, _, _
7368
+ modes: vec4f, // from, to, mix, _
6964
7369
  diff: vec4f, // threshold, gain, readyA, readyB
6965
7370
  accent: vec4f,
6966
7371
  loupe: vec4f, // x, y, r, zoom (0: none)
@@ -7011,21 +7416,14 @@ fn takeB(p: vec2f, r: vec4f, c0: vec4f, near: bool) -> vec4f {
7011
7416
 
7012
7417
  fn pm(c: vec4f) -> vec4f { return vec4f(c.rgb * c.a, c.a); }
7013
7418
 
7014
- fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
7015
- let flick = u.modes.w > 0.5;
7419
+ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f) -> vec4f {
7016
7420
  switch mode {
7017
- case 0u: {
7018
- let bb = select(b, vec4f(a2.rgb, b.a), flick);
7019
- return vec4f(a.rgb * a.a + bb.rgb * bb.a * (1.0 - a.a), a.a + bb.a * (1.0 - a.a));
7020
- }
7021
7421
  case 1u: {
7022
- if (flick) { return pm(a); }
7023
7422
  let s = max(u.wipe.y, 0.5);
7024
7423
  let m = smoothstep(u.wipe.x - s, u.wipe.x + s, p.x);
7025
7424
  return pm(vec4f(mix(a.rgb, b.rgb, m), mix(a.a, b.a, m)));
7026
7425
  }
7027
7426
  case 2u: {
7028
- if (flick) { return pm(a); }
7029
7427
  let d = abs(a.rgb - b.rgb);
7030
7428
  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);
7031
7429
  let c = mix(vec3f(luma(b.rgb) * 0.35), u.accent.rgb, h);
@@ -7033,8 +7431,8 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
7033
7431
  }
7034
7432
  default: {}
7035
7433
  }
7036
- // Flicker: After, or Before while X is held or on the flicker's other beat.
7037
- return pm(select(b, a, flick || u.wipe.w > 0.5));
7434
+ // Side by side: each side in its own place, Before over After where they meet.
7435
+ return vec4f(a.rgb * a.a + b.rgb * b.a * (1.0 - a.a), a.a + b.a * (1.0 - a.a));
7038
7436
  }
7039
7437
 
7040
7438
  @fragment fn fs(@builtin(position) q: vec4f) -> @location(0) vec4f {
@@ -7052,16 +7450,14 @@ fn composite(mode: u32, p: vec2f, a: vec4f, b: vec4f, a2: vec4f) -> vec4f {
7052
7450
  }
7053
7451
  let a = takeA(p, u.ra, u.ca, near);
7054
7452
  let b = takeB(p, u.rb, u.cb, near);
7055
- // Before in After's place (X held, side by side): After's rect, as if Before were zoomed there.
7056
- let a2 = takeA(p, u.rb, u.cb, near);
7057
- var o = mix(composite(u32(u.modes.x), p, a, b, a2), composite(u32(u.modes.y), p, a, b, a2), u.modes.z);
7453
+ var o = mix(composite(u32(u.modes.x), p, a, b), composite(u32(u.modes.y), p, a, b), u.modes.z);
7058
7454
  if (near) { o = o + vec4f(0.05, 0.05, 0.06, 1.0) * (1.0 - o.a); }
7059
7455
  o = mix(o, vec4f(0.92), ring * 0.9);
7060
7456
  return o;
7061
7457
  }
7062
7458
  `, TEXTURE_BINDING = 4, COPY_DST = 2, RENDER_ATTACHMENT = 16, shared = null, lostListeners;
7063
7459
  var init_compare_gpu = __esm(() => {
7064
- MODE_INDEX = { side: 0, wipe: 1, difference: 2, flicker: 3 };
7460
+ MODE_INDEX = { side: 0, wipe: 1, difference: 2 };
7065
7461
  lostListeners = new Set;
7066
7462
  });
7067
7463
 
@@ -7447,7 +7843,9 @@ function compare(container, options) {
7447
7843
  ...options.theme ? { theme: options.theme } : {},
7448
7844
  ...options.appearance ? { appearance: options.appearance } : {}
7449
7845
  };
7450
- let mode = options.mode ?? "side";
7846
+ const offered = compareModes(options.modes, options.mode);
7847
+ const modeList = offered.modes;
7848
+ let mode = offered.mode;
7451
7849
  let wipe = Math.min(1, Math.max(0, options.wipe ?? 0.5));
7452
7850
  let loupeOn = options.loupe ?? false;
7453
7851
  let threshold = options.threshold ?? 4 / 255;
@@ -7465,7 +7863,7 @@ function compare(container, options) {
7465
7863
  root.setAttribute("role", "group");
7466
7864
  root.setAttribute("aria-roledescription", "compare");
7467
7865
  root.setAttribute("aria-label", options.label ?? "Compare");
7468
- root.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight , . 1 2 3 4 S W D X Z L 0 = -");
7866
+ root.setAttribute("aria-keyshortcuts", `Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight , . ${modeList.map((m, i) => `${i + 1} ${modeKey(m)}`).join(" ")} Z L 0 = -`);
7469
7867
  applyLook2(root, options);
7470
7868
  root.dataset.mode = mode;
7471
7869
  if (auto)
@@ -7474,35 +7872,37 @@ function compare(container, options) {
7474
7872
  const modes = el("div", "knobs-cmp-modes");
7475
7873
  modes.setAttribute("role", "radiogroup");
7476
7874
  modes.setAttribute("aria-label", "Mode");
7477
- const modeBtns = MODES2.map((m, i) => {
7875
+ modes.hidden = modeList.length < 2;
7876
+ const modeBtns = modeList.map((m, i) => {
7478
7877
  const b = el("button", "knobs-cmp-mode");
7479
7878
  b.type = "button";
7480
7879
  b.tabIndex = -1;
7481
- b.textContent = m.label;
7482
- b.title = `${m.label} · ${i + 1}${m.key ? ` or ${m.key}` : " (hold X to flick)"}`;
7880
+ b.textContent = MODE_LABEL[m].short;
7881
+ b.title = `${MODE_LABEL[m].long} · ${i + 1} or ${modeKey(m)}`;
7483
7882
  b.setAttribute("role", "radio");
7484
- b.setAttribute("aria-keyshortcuts", m.key ? `${i + 1} ${m.key}` : `${i + 1}`);
7883
+ b.setAttribute("aria-keyshortcuts", `${i + 1} ${modeKey(m)}`);
7485
7884
  b.addEventListener("click", () => {
7486
- setMode(m.mode);
7885
+ setMode(m);
7487
7886
  root.focus({ preventScroll: true });
7488
7887
  });
7489
7888
  modes.append(b);
7490
7889
  return b;
7491
7890
  });
7492
- const loupeBtn = el("button", "knobs-cmp-toggle");
7493
- loupeBtn.type = "button";
7494
- loupeBtn.tabIndex = -1;
7495
- loupeBtn.textContent = "Loupe";
7496
- loupeBtn.title = "Loupe · hold Z";
7891
+ const toggleBtn = (name, label, title) => {
7892
+ const b = el("button", "knobs-cmp-toggle");
7893
+ b.type = "button";
7894
+ b.tabIndex = -1;
7895
+ b.innerHTML = icon(name);
7896
+ b.setAttribute("aria-label", label);
7897
+ b.title = title;
7898
+ return b;
7899
+ };
7900
+ const loupeBtn = toggleBtn("magnifyingGlass", "Loupe", "Loupe · hold Z");
7497
7901
  loupeBtn.addEventListener("click", () => {
7498
7902
  setLoupe(!loupeOn);
7499
7903
  root.focus({ preventScroll: true });
7500
7904
  });
7501
- const loopBtn = el("button", "knobs-cmp-toggle");
7502
- loopBtn.type = "button";
7503
- loopBtn.tabIndex = -1;
7504
- loopBtn.textContent = "Loop";
7505
- loopBtn.title = "Loop · L (drag its ends on the timeline)";
7905
+ const loopBtn = toggleBtn("repeat", "Loop", "Loop · L (drag its ends on the timeline)");
7506
7906
  loopBtn.setAttribute("aria-keyshortcuts", "L");
7507
7907
  loopBtn.addEventListener("click", () => {
7508
7908
  api.setLoop(!loopNow());
@@ -7519,7 +7919,8 @@ function compare(container, options) {
7519
7919
  const zoomReset = el("button", "knobs-cmp-zoom-reset");
7520
7920
  zoomReset.type = "button";
7521
7921
  zoomReset.tabIndex = -1;
7522
- zoomReset.textContent = "Reset";
7922
+ zoomReset.innerHTML = icon("arrowCounterClockwise", { small: true });
7923
+ zoomReset.setAttribute("aria-label", "Reset zoom");
7523
7924
  zoomReset.title = "Fit · 0 or double-click";
7524
7925
  zoomReset.addEventListener("click", () => {
7525
7926
  api.setZoom(null);
@@ -7534,6 +7935,9 @@ function compare(container, options) {
7534
7935
  handle.setAttribute("aria-label", "Wipe");
7535
7936
  handle.setAttribute("aria-valuemin", "0");
7536
7937
  handle.setAttribute("aria-valuemax", "100");
7938
+ const grip = el("span", "knobs-cmp-grip");
7939
+ grip.setAttribute("aria-hidden", "true");
7940
+ handle.append(grip);
7537
7941
  const note = el("div", "knobs-cmp-note");
7538
7942
  stage.append(canvas, split, labels[0], labels[1], handle, zoomPill, note);
7539
7943
  root.append(bar, stage);
@@ -7550,6 +7954,7 @@ function compare(container, options) {
7550
7954
  root.append(host);
7551
7955
  tl = timeline(host, {
7552
7956
  ...look,
7957
+ variant: "lane",
7553
7958
  duration: 1,
7554
7959
  fps: fps(),
7555
7960
  arrows: { step: 0.5, shift: 2 },
@@ -7568,9 +7973,9 @@ function compare(container, options) {
7568
7973
  const fade = new Spring(1, FADE2.omega, FADE2.zeta, 0.001);
7569
7974
  let fromMode = mode;
7570
7975
  let toMode = mode;
7571
- let phase = false;
7572
- let flickerTimer = 0;
7573
- let flickHeld = false;
7976
+ const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
7977
+ let wipeCatching = false;
7978
+ let gripY = null;
7574
7979
  let loupeHeld = false;
7575
7980
  let pointer = null;
7576
7981
  let drag = null;
@@ -7888,25 +8293,16 @@ function compare(container, options) {
7888
8293
  gpu = g;
7889
8294
  setRenderer("gpu");
7890
8295
  };
7891
- const syncFlicker = () => {
7892
- win.clearInterval(flickerTimer);
7893
- flickerTimer = 0;
7894
- phase = false;
7895
- if (mode === "flicker")
7896
- flickerTimer = win.setInterval(() => {
7897
- phase = !phase;
7898
- requestDraw();
7899
- }, FLICKER_MS);
7900
- };
7901
8296
  const syncModeUi = () => {
7902
8297
  root.dataset.mode = mode;
7903
- MODES2.forEach((m, i) => {
7904
- modeBtns[i].setAttribute("aria-checked", String(m.mode === mode));
8298
+ modeList.forEach((m, i) => {
8299
+ modeBtns[i].setAttribute("aria-checked", String(m === mode));
7905
8300
  });
7906
8301
  handle.tabIndex = mode === "wipe" ? 0 : -1;
8302
+ modePill?.update();
7907
8303
  };
7908
8304
  const setMode = (next, emit = true) => {
7909
- if (next === mode)
8305
+ if (next === mode || !modeList.includes(next))
7910
8306
  return;
7911
8307
  mode = next;
7912
8308
  const reduced = reducedMotion(win);
@@ -7922,19 +8318,29 @@ function compare(container, options) {
7922
8318
  else
7923
8319
  lay.set(next === "side" ? 0 : 1);
7924
8320
  syncModeUi();
7925
- syncFlicker();
7926
8321
  if (emit)
7927
8322
  options.onMode?.(next);
7928
8323
  requestDraw();
7929
8324
  };
7930
- const setWipe = (next, emit = true) => {
8325
+ const setWipe = (next, emit = true, jump = false) => {
7931
8326
  const w = Math.min(1, Math.max(0, next));
7932
- if (w === wipe)
8327
+ if (w === wipe && !jump)
7933
8328
  return;
7934
8329
  wipe = w;
8330
+ const minGap = 2 / Math.max(1, wipeRect(layoutNow().L.overlay).w);
8331
+ const head = moveHead({ x: wipeLine.x, target: wipeLine.target }, w, jump && !reducedMotion(win) ? "jump" : "follow", wipeCatching, minGap);
8332
+ wipeCatching = head.catching;
8333
+ if (head.catching) {
8334
+ wipeLine.x = head.x;
8335
+ wipeLine.target = head.target;
8336
+ } else
8337
+ wipeLine.snap(head.x);
7935
8338
  if (emit)
7936
8339
  options.onWipe?.(w);
7937
- drawNow();
8340
+ if (head.catching)
8341
+ requestDraw();
8342
+ else
8343
+ drawNow();
7938
8344
  };
7939
8345
  const setLoupe = (on) => {
7940
8346
  loupeOn = on;
@@ -8087,28 +8493,44 @@ function compare(container, options) {
8087
8493
  viewSent = key;
8088
8494
  options.onView({ zoom: clampZoom(z), loop: l });
8089
8495
  };
8496
+ const labelsIn = (m, r, wr, wx) => {
8497
+ if (m === "wipe") {
8498
+ const at = seamLabels(wx, wr.x, wr.x + wr.w, labelW[0], labelW[1], INSET);
8499
+ return [at.before, at.after];
8500
+ }
8501
+ const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * lay.x;
8502
+ return [
8503
+ { x: r.before.x + INSET, opacity: 1 },
8504
+ { x: ax, opacity: 1 }
8505
+ ];
8506
+ };
8507
+ const placeGrip = (wr) => {
8508
+ const gy = gripTop(gripY ?? wr.y + wr.h / 2, wr, GRIP.h) - wr.y;
8509
+ grip.style.transform = `translate3d(0,${gy.toFixed(2)}px,0)`;
8510
+ };
8090
8511
  const draw = () => {
8091
8512
  dirty = false;
8092
8513
  const r = layoutNow();
8093
8514
  const wr = wipeRect(r.L.overlay);
8094
- const wx = wipeX(wr, wipe);
8515
+ const wx = wipeX(wr, wipeLine.x);
8095
8516
  const k = lay.x;
8096
- const ax = r.after.x + INSET + (r.after.w - labelW[1] - INSET * 2) * k;
8097
- labels[0].style.transform = `translate(${(r.before.x + INSET).toFixed(2)}px,${(r.before.y + INSET).toFixed(2)}px)`;
8098
- labels[1].style.transform = `translate(${ax.toFixed(2)}px,${(r.after.y + INSET).toFixed(2)}px)`;
8099
- const showsBefore = flickHeld || mode === "flicker" && phase;
8100
- const flickering = flickHeld || mode === "flicker";
8101
- labels[0].toggleAttribute("data-lit", flickering && showsBefore);
8102
- labels[1].toggleAttribute("data-lit", flickering && !showsBefore);
8103
- labels[0].toggleAttribute("data-dim", flickering && !showsBefore || mode === "wipe" && wipe < 0.02);
8104
- labels[1].toggleAttribute("data-dim", flickering && showsBefore || mode === "wipe" && wipe > 0.98);
8517
+ const a = labelsIn(fromMode, r, wr, wx);
8518
+ const b = labelsIn(toMode, r, wr, wx);
8519
+ const f = Math.min(1, Math.max(0, fade.x));
8520
+ const ys = [r.before.y + INSET, r.after.y + INSET];
8521
+ for (const i of [0, 1]) {
8522
+ const x = a[i].x + (b[i].x - a[i].x) * f;
8523
+ labels[i].style.transform = `translate(${x.toFixed(2)}px,${ys[i].toFixed(2)}px)`;
8524
+ labels[i].style.opacity = (a[i].opacity + (b[i].opacity - a[i].opacity) * f).toFixed(3);
8525
+ }
8105
8526
  const top = Math.min(r.L.side.before.y, r.L.side.after.y);
8106
8527
  split.style.transform = `translate(${r.L.side.split.toFixed(2)}px,${top.toFixed(2)}px)`;
8107
8528
  split.style.height = `${Math.max(0, r.L.side.after.h).toFixed(2)}px`;
8108
8529
  split.style.opacity = String(Math.max(0, 1 - k * 2));
8109
- handle.toggleAttribute("data-on", mode === "wipe" && !flickHeld);
8530
+ handle.toggleAttribute("data-on", mode === "wipe");
8110
8531
  handle.style.transform = `translate(${wx.toFixed(2)}px,${wr.y.toFixed(2)}px)`;
8111
8532
  handle.style.height = `${Math.max(0, wr.h).toFixed(2)}px`;
8533
+ placeGrip(wr);
8112
8534
  handle.setAttribute("aria-valuenow", String(Math.round(wipe * 100)));
8113
8535
  handle.setAttribute("aria-valuetext", `${Math.round(wipe * 100)}% Before`);
8114
8536
  const pct = `${Math.round(100 / zw.x)}%`;
@@ -8131,7 +8553,7 @@ function compare(container, options) {
8131
8553
  const right = Math.max(0, rect.x + rect.w - Math.min(clip.x + clip.w, cutAt));
8132
8554
  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)` : "";
8133
8555
  };
8134
- const cut = mode === "wipe" && !flickHeld ? wx : Number.POSITIVE_INFINITY;
8556
+ const cut = mode === "wipe" ? wx : Number.POSITIVE_INFINITY;
8135
8557
  for (const s of sides) {
8136
8558
  const rect = s.index === 0 ? r.zBefore : r.zAfter;
8137
8559
  const clip = s.index === 0 ? r.before : r.after;
@@ -8140,10 +8562,8 @@ function compare(container, options) {
8140
8562
  place(s.preview, rect, clip, at);
8141
8563
  if (s.index !== 0)
8142
8564
  continue;
8143
- for (const n of [s.el, s.preview]) {
8144
- n.style.mixBlendMode = mode === "difference" && !flickHeld ? "difference" : "";
8145
- n.style.opacity = mode === "flicker" && !showsBefore ? "0" : "";
8146
- }
8565
+ for (const n of [s.el, s.preview])
8566
+ n.style.mixBlendMode = mode === "difference" ? "difference" : "";
8147
8567
  }
8148
8568
  return;
8149
8569
  }
@@ -8164,8 +8584,6 @@ function compare(container, options) {
8164
8584
  from: MODE_INDEX[fromMode],
8165
8585
  to: MODE_INDEX[toMode],
8166
8586
  mix: Math.min(1, Math.max(0, fade.x)),
8167
- flick: flickHeld,
8168
- phase,
8169
8587
  threshold,
8170
8588
  gain: 1,
8171
8589
  accent,
@@ -8181,6 +8599,11 @@ function compare(container, options) {
8181
8599
  const loop = frameLoop(win, (dt) => {
8182
8600
  lay.step(dt);
8183
8601
  fade.step(dt);
8602
+ if (wipeCatching) {
8603
+ wipeLine.step(dt);
8604
+ if (wipeLine.resting)
8605
+ wipeCatching = false;
8606
+ }
8184
8607
  zu.step(dt);
8185
8608
  zv.step(dt);
8186
8609
  zw.step(dt);
@@ -8214,7 +8637,7 @@ function compare(container, options) {
8214
8637
  pushFrame(s);
8215
8638
  }
8216
8639
  }
8217
- const moving = !lay.resting || !fade.resting || !zoomResting();
8640
+ const moving = !lay.resting || !fade.resting || !zoomResting() || wipeCatching;
8218
8641
  if (moving)
8219
8642
  dirty = true;
8220
8643
  if (auto && !lay.resting)
@@ -8357,16 +8780,19 @@ function compare(container, options) {
8357
8780
  e.preventDefault();
8358
8781
  return startPinch();
8359
8782
  }
8360
- if (mode === "wipe" && !flickHeld) {
8783
+ if (mode === "wipe") {
8361
8784
  const wr = wipeRect(layoutNow().L.overlay);
8362
8785
  const inside = p.x >= wr.x - SLOP && p.x <= wr.x + wr.w + SLOP && p.y >= wr.y && p.y <= wr.y + wr.h;
8363
- const grab = wipeGrab(p.x, wr, wipe, SLOP);
8786
+ const grab = wipeGrab(p.x, wr, wipeLine.x, SLOP);
8364
8787
  if (inside && (grab.onLine || !zoomed())) {
8365
8788
  drag = { kind: "wipe", offset: grab.offset };
8366
8789
  root.dataset.dragging = "";
8790
+ gripY = p.y;
8367
8791
  e.preventDefault();
8368
8792
  if (!grab.onLine)
8369
- setWipe(wipeAt(p.x, wr));
8793
+ setWipe(wipeAt(p.x, wr), true, true);
8794
+ else
8795
+ drawNow();
8370
8796
  return;
8371
8797
  }
8372
8798
  }
@@ -8392,6 +8818,11 @@ function compare(container, options) {
8392
8818
  pinch = { d, x: mid.x, y: mid.y };
8393
8819
  return handZoom(zoomAbout(panned, k, q.pu, q.pv));
8394
8820
  }
8821
+ if (mode === "wipe" && drag?.kind !== "pan") {
8822
+ gripY = p.y;
8823
+ if (!drag)
8824
+ placeGrip(wipeRect(layoutNow().L.overlay));
8825
+ }
8395
8826
  if (drag?.kind === "wipe")
8396
8827
  setWipe(wipeAt(p.x, wipeRect(layoutNow().L.overlay), drag.offset));
8397
8828
  else if (drag?.kind === "pan") {
@@ -8464,12 +8895,6 @@ function compare(container, options) {
8464
8895
  const z = { u: zu.target, v: zv.target, w: zw.target, h: zw.target };
8465
8896
  glideZoom(zoomAbout(z, factor, 0.5, 0.5));
8466
8897
  };
8467
- const holdFlick = (on) => {
8468
- if (flickHeld === on)
8469
- return;
8470
- flickHeld = on;
8471
- drawNow();
8472
- };
8473
8898
  const holdLoupe = (on) => {
8474
8899
  if (loupeHeld === on)
8475
8900
  return;
@@ -8515,25 +8940,17 @@ function compare(container, options) {
8515
8940
  case "s":
8516
8941
  case "w":
8517
8942
  case "d":
8518
- if (e.altKey || e.shiftKey || key === "s" && claimsHintKey(doc, key))
8519
- return;
8520
- claim();
8521
- return setMode(KEY_MODE[key]);
8522
8943
  case "1":
8523
8944
  case "2":
8524
- case "3":
8525
- case "4":
8526
- if (e.altKey || e.shiftKey)
8945
+ case "3": {
8946
+ if (e.altKey || e.shiftKey || key === "s" && claimsHintKey(doc, key))
8527
8947
  return;
8528
- claim();
8529
- return setMode(MODES2[Number(key) - 1].mode);
8530
- case "x":
8531
- if (e.altKey)
8948
+ const m = modeForKey(key, modeList);
8949
+ if (!m)
8532
8950
  return;
8533
8951
  claim();
8534
- if (!e.repeat)
8535
- holdFlick(true);
8536
- return;
8952
+ return setMode(m);
8953
+ }
8537
8954
  case "z":
8538
8955
  if (e.altKey)
8539
8956
  return;
@@ -8589,8 +9006,11 @@ function compare(container, options) {
8589
9006
  labels[1].textContent = labelOf(1);
8590
9007
  loupeBtn.setAttribute("aria-pressed", String(loupeOn));
8591
9008
  loopBtn.setAttribute("aria-pressed", String(loopNow() !== null));
9009
+ const modePill = modeList.length > 1 ? selectionPill(modes, {
9010
+ rows: () => modeBtns,
9011
+ selected: () => modeBtns[modeList.indexOf(mode)] ?? null
9012
+ }) : null;
8592
9013
  syncModeUi();
8593
- syncFlicker();
8594
9014
  const unlisten = tl?.listen({
8595
9015
  onSeek: (t, source) => {
8596
9016
  if (source === "play") {
@@ -8648,24 +9068,20 @@ function compare(container, options) {
8648
9068
  });
8649
9069
  root.addEventListener("keydown", onKey);
8650
9070
  root.addEventListener("keyup", (e) => {
8651
- const key = e.key.toLowerCase();
8652
- if (key === "x")
8653
- holdFlick(false);
8654
- else if (key === "z")
9071
+ if (e.key.toLowerCase() === "z")
8655
9072
  holdLoupe(false);
8656
9073
  });
8657
9074
  root.addEventListener("focusout", (e) => {
8658
9075
  if (root.contains(e.relatedTarget))
8659
9076
  return;
8660
- holdFlick(false);
8661
9077
  holdLoupe(false);
8662
9078
  });
8663
9079
  const unregisterHints = options.hints === false ? undefined : hints.register(root, () => [
8664
- ...MODES2.map((m, i) => ({
9080
+ ...modeList.length > 1 ? modeList.map((m, i) => ({
8665
9081
  el: modeBtns[i],
8666
- label: m.label,
8667
- activate: () => setMode(m.mode)
8668
- })),
9082
+ label: MODE_LABEL[m].long,
9083
+ activate: () => setMode(m)
9084
+ })) : [],
8669
9085
  { el: loopBtn, label: "Loop", activate: () => api.setLoop(!loopNow()) },
8670
9086
  ...loupeBtn.hidden ? [] : [{ el: loupeBtn, label: "Loupe", activate: () => setLoupe(!loupeOn) }],
8671
9087
  ...zoomed() ? [{ el: zoomReset, label: "Reset zoom", activate: () => api.setZoom(null) }] : [],
@@ -8690,6 +9106,7 @@ function compare(container, options) {
8690
9106
  get mode() {
8691
9107
  return mode;
8692
9108
  },
9109
+ modes: modeList,
8693
9110
  get wipe() {
8694
9111
  return wipe;
8695
9112
  },
@@ -8745,7 +9162,7 @@ function compare(container, options) {
8745
9162
  tl?.update({ fps: next.fps });
8746
9163
  requestDraw();
8747
9164
  },
8748
- seek: (t) => tl ? tl.seek(t) : ownSeek(t),
9165
+ seek: (t, o) => tl ? tl.seek(t, o?.glide ? { glide: true } : undefined) : ownSeek(t),
8749
9166
  seekBy(d) {
8750
9167
  const total = duration || tl?.duration || 0;
8751
9168
  const t = Math.min(total, Math.max(0, now() + d));
@@ -8786,7 +9203,7 @@ function compare(container, options) {
8786
9203
  dispose() {
8787
9204
  disposed = true;
8788
9205
  loop.stop();
8789
- win.clearInterval(flickerTimer);
9206
+ modePill?.dispose();
8790
9207
  win.clearTimeout(releaseTimer);
8791
9208
  ro.disconnect();
8792
9209
  nearIo?.disconnect();
@@ -8816,7 +9233,7 @@ function compare(container, options) {
8816
9233
  wake();
8817
9234
  return api;
8818
9235
  }
8819
- var COMPARE_FFMPEG = "ffmpeg -i in.mov -vf fps=30 -fps_mode cfr -c:v libx264 -preset slow -crf 18 -g 15 -keyint_min 15 -pix_fmt yuv420p -movflags +faststart -an out.mp4", LAYOUT, FADE2, FADE_REDUCED, ZOOM, FLICKER_MS = 400, SLOP = 12, INSET = 8, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODES2, KEY_MODE;
9236
+ var COMPARE_FFMPEG = "ffmpeg -i in.mov -vf fps=30 -fps_mode cfr -c:v libx264 -preset slow -crf 18 -g 15 -keyint_min 15 -pix_fmt yuv420p -movflags +faststart -an out.mp4", LAYOUT, FADE2, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, INSET = 8, GRIP, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
8820
9237
  var init_compare = __esm(() => {
8821
9238
  init_motion();
8822
9239
  init_page_hints();
@@ -8824,21 +9241,24 @@ var init_compare = __esm(() => {
8824
9241
  init_compare_gpu();
8825
9242
  init_compare_model();
8826
9243
  init_compare_strip();
9244
+ init_icons();
8827
9245
  init_look();
8828
9246
  init_rgba();
9247
+ init_selection_pill();
8829
9248
  init_timeline();
9249
+ init_timeline_model();
8830
9250
  LAYOUT = springMotion({ duration: 0.35, bounce: 0 });
8831
9251
  FADE2 = springMotion(SPRINGS.snappy);
8832
9252
  FADE_REDUCED = springMotion(FADE);
8833
9253
  ZOOM = springMotion({ duration: 0.35, bounce: 0 });
9254
+ WIPE_CATCH = springMotion(CATCH_UP);
9255
+ GRIP = { w: 16, h: 28 };
8834
9256
  LOUPE = { r: 88, zoom: 4 };
8835
- MODES2 = [
8836
- { mode: "side", label: "Side by side", key: "S" },
8837
- { mode: "wipe", label: "Wipe", key: "W" },
8838
- { mode: "difference", label: "Difference", key: "D" },
8839
- { mode: "flicker", label: "Flicker", key: "" }
8840
- ];
8841
- KEY_MODE = { s: "side", w: "wipe", d: "difference" };
9257
+ MODE_LABEL = {
9258
+ side: { short: "Side", long: "Side by side" },
9259
+ wipe: { short: "Wipe", long: "Wipe" },
9260
+ difference: { short: "Difference", long: "Difference" }
9261
+ };
8842
9262
  });
8843
9263
 
8844
9264
  // src/ui/scrollbar.ts
@@ -8960,34 +9380,27 @@ function tree(container, options) {
8960
9380
  const empty = doc.createElement("div");
8961
9381
  empty.className = "knobs-tree-empty";
8962
9382
  empty.hidden = true;
8963
- const cursor = doc.createElement("div");
8964
- cursor.className = "knobs-tree-cursor";
8965
- root.append(space, cursor, line, empty);
8966
- const cursorY = new Spring(0, MOVE.omega, MOVE.zeta, 0.1);
8967
- let cursorOn = false;
8968
- const drawCursor = () => {
8969
- cursor.style.transform = `translate3d(0,${Math.round(cursorY.x * 2) / 2}px,0)`;
9383
+ root.append(space, line, empty);
9384
+ let pill = null;
9385
+ let picked = null;
9386
+ let travelUntil = 0;
9387
+ const pickedRow = () => {
9388
+ const id = sel.ids.length === 1 ? sel.ids[0] : null;
9389
+ const v = id === null ? undefined : views.get(id);
9390
+ return v && !v.leaving && rowIndex.has(v.id) ? v : null;
8970
9391
  };
8971
9392
  const placeCursor = (animate) => {
8972
- if (!options.glide)
9393
+ if (!pill)
8973
9394
  return;
8974
9395
  const id = sel.ids.length === 1 ? sel.ids[0] : null;
8975
- const i = id === null ? undefined : rowIndex.get(id);
8976
- root.toggleAttribute("data-glide", i !== undefined);
8977
- if (i === undefined) {
8978
- cursor.removeAttribute("data-on");
8979
- cursorOn = false;
8980
- return;
8981
- }
8982
- if (cursorOn && animate && !reducedMotion(win))
8983
- cursorY.set(i * h);
8984
- else
8985
- cursorY.snap(i * h);
8986
- cursorOn = true;
8987
- cursor.setAttribute("data-on", "");
8988
- drawCursor();
8989
- if (!cursorY.resting)
8990
- loop.wake();
9396
+ root.toggleAttribute("data-glide", id !== null && rowIndex.has(id));
9397
+ const fresh = id !== picked;
9398
+ picked = id;
9399
+ if (animate && fresh) {
9400
+ travelUntil = performance.now() + 450;
9401
+ pill.update();
9402
+ } else if (performance.now() >= travelUntil)
9403
+ pill.update({ snap: true });
8991
9404
  };
8992
9405
  container.append(root);
8993
9406
  const scrollbar = overlayScrollbar(root, space);
@@ -9016,6 +9429,12 @@ function tree(container, options) {
9016
9429
  };
9017
9430
  const views = new Map;
9018
9431
  const pool = [];
9432
+ if (options.glide)
9433
+ pill = selectionPill(root, {
9434
+ rows: () => [...views.values()].filter((v) => !v.leaving).map((v) => v.el),
9435
+ selected: () => pickedRow()?.el ?? null,
9436
+ radius: (row) => Number.parseFloat(win.getComputedStyle(row, "::before").borderTopLeftRadius) || 0
9437
+ });
9019
9438
  const makeView = (id) => {
9020
9439
  const v = pool.pop() ?? createView();
9021
9440
  v.id = id;
@@ -9196,12 +9615,6 @@ function tree(container, options) {
9196
9615
  function frame(dt) {
9197
9616
  let moving = false;
9198
9617
  let bottom = rows.length * h;
9199
- if (options.glide && !cursorY.resting) {
9200
- cursorY.step(dt);
9201
- drawCursor();
9202
- if (!cursorY.resting)
9203
- moving = true;
9204
- }
9205
9618
  for (const v of views.values()) {
9206
9619
  v.y.step(dt);
9207
9620
  if (!v.y.resting)
@@ -9221,6 +9634,8 @@ function tree(container, options) {
9221
9634
  }
9222
9635
  setSpace(moving ? bottom : rows.length * h);
9223
9636
  trim();
9637
+ if (pill && moving && performance.now() >= travelUntil)
9638
+ pill.update({ snap: true });
9224
9639
  return moving;
9225
9640
  }
9226
9641
  const trim = () => {
@@ -9849,6 +10264,7 @@ function tree(container, options) {
9849
10264
  autoScroll.stop();
9850
10265
  ro.disconnect();
9851
10266
  scrollbar.dispose();
10267
+ pill?.dispose();
9852
10268
  dragging?.ghost.remove();
9853
10269
  root.remove();
9854
10270
  }
@@ -9867,6 +10283,7 @@ var init_tree = __esm(() => {
9867
10283
  init_motion();
9868
10284
  init_page_hints();
9869
10285
  init_look();
10286
+ init_selection_pill();
9870
10287
  init_tree_model();
9871
10288
  MOVE = springMotion(SPRINGS.default);
9872
10289
  });
@@ -9916,33 +10333,29 @@ var init_diff_css = __esm(() => {
9916
10333
  .knobs-diff-fill{flex:1}
9917
10334
  .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}
9918
10335
  .knobs-diff-nav>span{min-width:3.6em;text-align:center}
9919
- :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;
10336
+ :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;
9920
10337
  margin:0;padding:0;min-width:0;box-shadow:none;outline:none}
9921
- .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;
10338
+ .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;
9922
10339
  transition:background .15s,color .15s,opacity .15s}
9923
10340
  .knobs-ui .knobs-diff-btn:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
9924
10341
  .knobs-ui .knobs-diff-btn[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
9925
10342
  .knobs-ui .knobs-diff-btn:disabled{opacity:.35;cursor:default;background:transparent}
9926
- .knobs-diff-btn svg{width:13px;height:13px}
10343
+ .knobs-diff-btn svg{width:16px;height:16px}
9927
10344
  /* Viewed: a check with its word, as on GitHub; the path dims once it's ticked. */
9928
10345
  .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);
9929
10346
  font-size:var(--_k-text-meta);font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:background .15s,color .15s}
9930
10347
  .knobs-ui .knobs-diff-viewed:is(:hover,:focus-visible){background:var(--_ku-wash-hi);color:var(--_k-fg)}
9931
10348
  .knobs-ui .knobs-diff-viewed[aria-pressed="true"]{color:var(--_k-fg);background:var(--_ku-wash)}
9932
- .knobs-diff-viewed svg{width:13px;height:13px}
10349
+ .knobs-diff-viewed svg{width:16px;height:16px}
9933
10350
  .knobs-diff-viewed[aria-pressed="true"] svg{color:var(--kd-add)}
9934
10351
  .knobs-diff-path{transition:opacity .2s}
9935
10352
  .knobs-diff[data-viewed]>.knobs-diff-head .knobs-diff-path,.knobs-diff-part[data-viewed] .knobs-diff-path{opacity:.6}
9936
- /* Split | Unified: one pill that glides between the two, raised as a panel's segments are (a
9937
- lighter bg in dark, near white in light). */
9938
- .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)}
9939
- .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);
9940
- font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:color .15s}
9941
- .knobs-ui .knobs-diff-seg>button:is(:hover,:focus-visible),.knobs-ui .knobs-diff-seg>button[aria-checked="true"]{color:var(--_k-fg)}
9942
- .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));
9943
- box-shadow:0 1px 2px rgba(0,0,0,calc(.16 * var(--k-lift,1)));transition:transform var(--_ku-ease-ms) var(--_ku-ease)}
9944
- .knobs-diff-seg[data-value="unified"]>.knobs-diff-pill{transform:translateX(100%)}
9945
- .knobs-diff[data-frame="flat"] .knobs-diff-pill{box-shadow:none}
10353
+ /* Split | Unified: the selection pill on the one picked, its bright copy the picked one's text; a
10354
+ choice isn't brighter for being picked. Choices are styled by class, as the copy holds them too. */
10355
+ .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)}
10356
+ .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);
10357
+ font-weight:var(--_k-weight-label);white-space:nowrap;cursor:pointer;transition:color calc(var(--_k-hover-out) * 1ms)}
10358
+ .knobs-ui .knobs-diff-choice:is(:hover,:focus-visible){color:var(--_k-fg)}
9946
10359
  /* The diff: one layer per file or layout on show, the old fading under the new. */
9947
10360
  .knobs-diff-body{position:relative;flex:1;min-height:0}
9948
10361
  .knobs-diff-probe{position:absolute;left:0;top:0;width:0;height:var(--kd-line);visibility:hidden;pointer-events:none}
@@ -10037,7 +10450,7 @@ var init_diff_css = __esm(() => {
10037
10450
  .knobs-ui .knobs-diff-filter>input::placeholder{color:var(--_k-fg-3)}
10038
10451
  .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}
10039
10452
  .knobs-ui .knobs-diff-filter>button:hover{background:var(--_ku-wash-hi);color:var(--_k-fg)}
10040
- .knobs-diff-filter>button svg{width:10px;height:10px}
10453
+ .knobs-diff-filter>button svg{width:12px;height:12px}
10041
10454
  .knobs-diff-list{position:relative;flex:1;min-height:0}
10042
10455
  .knobs-diff-files .knobs-diff-counts{font-size:var(--_k-text-meta);align-items:center}
10043
10456
  .knobs-diff-files .knobs-diff-counts{gap:5px}
@@ -10843,7 +11256,7 @@ function codeDiff(container, options) {
10843
11256
  };
10844
11257
  const nav = make("span", "knobs-diff-nav");
10845
11258
  const prev = button("Previous file", PREV);
10846
- const next = button("Next file", ICONS.caret);
11259
+ const next = button("Next file", NEXT);
10847
11260
  const place = make("span", "");
10848
11261
  nav.append(prev, place, next);
10849
11262
  const wrapButton = button("Wrap lines", WRAP);
@@ -10864,9 +11277,8 @@ function codeDiff(container, options) {
10864
11277
  const seg = make("div", "knobs-diff-seg");
10865
11278
  seg.setAttribute("role", "radiogroup");
10866
11279
  seg.setAttribute("aria-label", "Layout");
10867
- const pill = make("i", "knobs-diff-pill");
10868
11280
  const choice = (value, label) => {
10869
- const b = make("button", "");
11281
+ const b = make("button", "knobs-diff-choice");
10870
11282
  b.type = "button";
10871
11283
  b.setAttribute("role", "radio");
10872
11284
  b.dataset.value = value;
@@ -10875,7 +11287,7 @@ function codeDiff(container, options) {
10875
11287
  };
10876
11288
  const splitButton = choice("split", "Split");
10877
11289
  const unifiedButton = choice("unified", "Unified");
10878
- seg.append(pill, splitButton, unifiedButton);
11290
+ seg.append(splitButton, unifiedButton);
10879
11291
  if (options.lead)
10880
11292
  head.append(options.lead);
10881
11293
  head.append(path, stat, make("span", "knobs-diff-fill"), nav, wrapButton, seg, viewedButton);
@@ -10886,6 +11298,11 @@ function codeDiff(container, options) {
10886
11298
  body.append(note, probe);
10887
11299
  root.append(head, body);
10888
11300
  container.append(root);
11301
+ const segPill = selectionPill(seg, {
11302
+ rows: () => [splitButton, unifiedButton],
11303
+ selected: () => layout === "split" ? splitButton : unifiedButton
11304
+ });
11305
+ let pilled = layout;
10889
11306
  const fileNow = () => files.find((f) => f.name === current) ?? null;
10890
11307
  const paintHead = () => {
10891
11308
  const file = fileNow();
@@ -10918,6 +11335,10 @@ function codeDiff(container, options) {
10918
11335
  next.disabled = at >= files.length - 1;
10919
11336
  wrapButton.setAttribute("aria-pressed", String(wrap));
10920
11337
  seg.dataset.value = layout;
11338
+ if (pilled !== layout) {
11339
+ pilled = layout;
11340
+ segPill.update();
11341
+ }
10921
11342
  splitButton.setAttribute("aria-checked", String(layout === "split"));
10922
11343
  unifiedButton.setAttribute("aria-checked", String(layout === "unified"));
10923
11344
  splitButton.tabIndex = layout === "split" ? 0 : -1;
@@ -11752,6 +12173,7 @@ function codeDiff(container, options) {
11752
12173
  dispose() {
11753
12174
  disposed = true;
11754
12175
  loop.stop();
12176
+ segPill.dispose();
11755
12177
  sizes.disconnect();
11756
12178
  attaching.disconnect();
11757
12179
  resized.disconnect();
@@ -12107,7 +12529,7 @@ function diffReview(container, options) {
12107
12529
  }
12108
12530
  };
12109
12531
  }
12110
- var lib, load = () => lib ??= import("@pierre/diffs"), SYNTAX, IN, OUT, FADE3, HEIGHT, DRAWER, BLUR = 3, WRAP, PREV, UNVIEWED, VIEWED, SEARCH, CLEAR, SIDEBAR, SEPARATOR = 32;
12532
+ var lib, load = () => lib ??= import("@pierre/diffs"), SYNTAX, IN, OUT, FADE3, HEIGHT, DRAWER, BLUR = 3, WRAP, PREV, NEXT, UNVIEWED, VIEWED, SEARCH, CLEAR, SIDEBAR, SEPARATOR = 32;
12111
12533
  var init_diff = __esm(() => {
12112
12534
  init_glide();
12113
12535
  init_motion();
@@ -12115,6 +12537,8 @@ var init_diff = __esm(() => {
12115
12537
  init_diff_model();
12116
12538
  init_look();
12117
12539
  init_rgba();
12540
+ init_icons();
12541
+ init_selection_pill();
12118
12542
  init_tree();
12119
12543
  SYNTAX = { dark: "pierre-dark", light: "pierre-light" };
12120
12544
  IN = springMotion(SPRINGS.default);
@@ -12122,13 +12546,14 @@ var init_diff = __esm(() => {
12122
12546
  FADE3 = springMotion(FADE);
12123
12547
  HEIGHT = springMotion(SPRINGS.calm);
12124
12548
  DRAWER = springMotion(SPRINGS.calm);
12125
- 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"/>');
12126
- PREV = svg('<path d="M10 3.5 5.5 8l4.5 4.5"/>');
12127
- UNVIEWED = svg('<circle cx="8" cy="8" r="5.5"/>');
12128
- VIEWED = svg('<circle cx="8" cy="8" r="5.5"/><path d="m5.6 8.1 1.7 1.7 3.2-3.5"/>');
12129
- SEARCH = svg('<circle cx="7" cy="7" r="4.5"/><path d="m10.5 10.5 3 3"/>');
12130
- CLEAR = svg('<path d="m5 5 6 6M11 5l-6 6"/>');
12131
- SIDEBAR = svg('<rect x="2" y="3" width="12" height="10" rx="2"/><path d="M6.5 3v10"/>');
12549
+ WRAP = icon("arrowUDownLeft");
12550
+ PREV = icon("caretLeft");
12551
+ NEXT = icon("caretRight");
12552
+ UNVIEWED = icon("circle");
12553
+ VIEWED = icon("checkCircle");
12554
+ SEARCH = icon("magnifyingGlass", { small: true });
12555
+ CLEAR = icon("x", { small: true });
12556
+ SIDEBAR = icon("sidebar");
12132
12557
  });
12133
12558
 
12134
12559
  // src/ui/index.ts
@@ -14629,6 +15054,11 @@ function readout(field, options = {}) {
14629
15054
  dispose: () => view.dispose()
14630
15055
  };
14631
15056
  }
15057
+
15058
+ // src/ui/index.ts
15059
+ init_icons();
15060
+ init_selection_pill();
15061
+
14632
15062
  // src/ui/tooltip.ts
14633
15063
  init_keys();
14634
15064
  init_layer();
@@ -18390,6 +18820,7 @@ function activity(container, options) {
18390
18820
  // src/ui/picker.ts
18391
18821
  init_motion();
18392
18822
  init_look();
18823
+ init_icons();
18393
18824
 
18394
18825
  // src/ui/picker-model.ts
18395
18826
  function digitPick(ids, key) {
@@ -18444,7 +18875,7 @@ var CLOSE3 = springMotion(CLOSE);
18444
18875
  var CARD = springMotion(SPRINGS.default);
18445
18876
  var FADE6 = springMotion(FADE);
18446
18877
  var DRIFT = 6;
18447
- var CHECK2 = svg('<path d="m3.5 8.4 3 3 6-6.6"/>');
18878
+ var CHECK2 = icon("check", { small: true });
18448
18879
  function optionPicker(container, options) {
18449
18880
  const doc = container.ownerDocument;
18450
18881
  const win = doc.defaultView;
@@ -20262,7 +20693,7 @@ function mountTable(host, m, steps) {
20262
20693
  context: () => ({ kind: "table" })
20263
20694
  };
20264
20695
  }
20265
- var grid = (v) => Math.round(v * 64) / 64;
20696
+ var grid2 = (v) => Math.round(v * 64) / 64;
20266
20697
  function chapterPills(host, chapters, io) {
20267
20698
  const doc = host.ownerDocument;
20268
20699
  const win = doc.defaultView;
@@ -20290,9 +20721,9 @@ function chapterPills(host, chapters, io) {
20290
20721
  const y = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
20291
20722
  const w = new Spring(0, MOTION.calm.omega, MOTION.calm.zeta, 0.25);
20292
20723
  const draw = () => {
20293
- const left = grid(x.x);
20294
- mark.style.transform = `translate(${left}px,${grid(y.x)}px)`;
20295
- mark.style.width = `${grid(x.x + w.x) - left}px`;
20724
+ const left = grid2(x.x);
20725
+ mark.style.transform = `translate(${left}px,${grid2(y.x)}px)`;
20726
+ mark.style.width = `${grid2(x.x + w.x) - left}px`;
20296
20727
  };
20297
20728
  const loop = frameLoop(win, (dt) => {
20298
20729
  x.step(dt);
@@ -20896,7 +21327,7 @@ var lookOf2 = (o) => ({
20896
21327
  ...o.preset ? { preset: o.preset } : {},
20897
21328
  ...o.theme ? { theme: o.theme } : {}
20898
21329
  });
20899
- var grid2 = (v) => Math.round(v * 64) / 64;
21330
+ var grid3 = (v) => Math.round(v * 64) / 64;
20900
21331
  var ICON = {
20901
21332
  full: svg('<path d="M3 6.5V3h3.5M13 6.5V3H9.5M3 9.5V13h3.5M13 9.5V13H9.5"/>'),
20902
21333
  exit: svg('<path d="M6.5 3v3.5H3M9.5 3v3.5H13M6.5 13V9.5H3M9.5 13V9.5H13"/>'),
@@ -21076,8 +21507,8 @@ function walkthrough(container, options) {
21076
21507
  if (mark) {
21077
21508
  pillY.step(dt);
21078
21509
  pillH.step(dt);
21079
- mark.style.transform = `translateY(${grid2(pillY.x)}px)`;
21080
- mark.style.height = `${grid2(pillY.x + pillH.x) - grid2(pillY.x)}px`;
21510
+ mark.style.transform = `translateY(${grid3(pillY.x)}px)`;
21511
+ mark.style.height = `${grid3(pillY.x + pillH.x) - grid3(pillY.x)}px`;
21081
21512
  more ||= !pillY.resting || !pillH.resting;
21082
21513
  }
21083
21514
  if (scroll.to && scroll.el) {
@@ -21237,10 +21668,10 @@ function walkthrough(container, options) {
21237
21668
  const a = f.hold.getBoundingClientRect();
21238
21669
  const b = fullRect();
21239
21670
  const k = reduced() ? Math.round(f.p.target) : f.p.x;
21240
- const left = grid2(a.left + (b.x - a.left) * k);
21241
- const top = grid2(a.top + (b.y - a.top) * k);
21242
- const right = grid2(a.right + (b.x + b.w - a.right) * k);
21243
- const bottom = grid2(a.bottom + (b.y + b.h - a.bottom) * k);
21671
+ const left = grid3(a.left + (b.x - a.left) * k);
21672
+ const top = grid3(a.top + (b.y - a.top) * k);
21673
+ const right = grid3(a.right + (b.x + b.w - a.right) * k);
21674
+ const bottom = grid3(a.bottom + (b.y + b.h - a.bottom) * k);
21244
21675
  const s = f.v.card.style;
21245
21676
  s.left = `${left}px`;
21246
21677
  s.top = `${top}px`;
@@ -22104,9 +22535,11 @@ function walkthrough(container, options) {
22104
22535
  return api;
22105
22536
  }
22106
22537
  export {
22538
+ CATCH_UP,
22107
22539
  COLLAPSE_PAST,
22108
22540
  COMPARE_FFMPEG,
22109
22541
  FIT_MAX,
22542
+ ICON_NAMES,
22110
22543
  activity,
22111
22544
  adjacentMarker,
22112
22545
  advance,
@@ -22123,12 +22556,14 @@ export {
22123
22556
  commandsFrom,
22124
22557
  compare,
22125
22558
  compareLayout,
22559
+ compareModes,
22126
22560
  comparePaths,
22127
22561
  compareStrip,
22128
22562
  compareText,
22129
22563
  confirm,
22130
22564
  contextMenu,
22131
22565
  countChanges,
22566
+ currentMarker,
22132
22567
  diffActivity,
22133
22568
  diffBar,
22134
22569
  diffCounts,
@@ -22153,7 +22588,9 @@ export {
22153
22588
  frameOf,
22154
22589
  frameTime,
22155
22590
  fuzzy,
22591
+ gripTop,
22156
22592
  hints,
22593
+ icon,
22157
22594
  inTriangle,
22158
22595
  indexActivity,
22159
22596
  indexTree,
@@ -22172,7 +22609,9 @@ export {
22172
22609
  mediaKind,
22173
22610
  mediaSlot,
22174
22611
  menu,
22612
+ modeForKey,
22175
22613
  motionBlur,
22614
+ moveHead,
22176
22615
  navigate,
22177
22616
  navigateActivity,
22178
22617
  normalizeWalkthrough,
@@ -22181,9 +22620,14 @@ export {
22181
22620
  pageHints,
22182
22621
  parseLayout,
22183
22622
  parsePatch,
22623
+ pillBoxOf,
22624
+ pillProgress,
22625
+ pillRadius,
22626
+ pillSpring,
22184
22627
  place,
22185
22628
  plainAccess,
22186
22629
  planBento,
22630
+ planPill,
22187
22631
  popover,
22188
22632
  railSegments,
22189
22633
  rank,
@@ -22195,9 +22639,11 @@ export {
22195
22639
  rulerTicks,
22196
22640
  safeTriangle,
22197
22641
  scrubTime,
22642
+ seamLabels,
22198
22643
  seekTime,
22199
22644
  seeked,
22200
22645
  select,
22646
+ selectionPill,
22201
22647
  showsSelection,
22202
22648
  softClamp,
22203
22649
  solve2 as solveSplit,