@kitlangton/knobs 0.4.0-next.10 → 0.4.0-next.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui-react.js CHANGED
@@ -2965,13 +2965,17 @@ var css2 = `
2965
2965
  .knobs-tree-row[aria-expanded="true"]>.knobs-tree-caret{transform:rotate(90deg)}
2966
2966
  .knobs-tree-row:not([aria-expanded])>.knobs-tree-caret{visibility:hidden}
2967
2967
  .knobs-tree-icon{flex:none;display:flex;align-items:center;justify-content:center;width:14px;margin-right:6px;color:var(--_k-fg-3);font-size:calc(var(--_k-font-size) - 1px)}
2968
- .knobs-tree-label{position:relative;flex:1;min-width:0;overflow:hidden;font-weight:var(--_k-weight)}
2968
+ /* The label wins the room: it keeps its width while the detail (which only takes what's left)
2969
+ fades out at its end, and gives way only once the detail is gone. Each fades where it's cut. */
2970
+ .knobs-tree-label{position:relative;flex:0 1 auto;min-width:0;overflow:hidden;font-weight:var(--_k-weight)}
2971
+ .knobs-tree-row[data-editing]>.knobs-tree-label{flex-grow:1}
2969
2972
  .knobs-tree-row[data-ancestor] .knobs-tree-label{color:var(--_k-fg-2)}
2970
2973
  .knobs-tree-label .knobs-tree-mark{color:var(--_k-accent)}
2971
2974
  .knobs-tree-row[data-off~="visible"] :is(.knobs-tree-label,.knobs-tree-icon){opacity:.45}
2972
2975
  .knobs-tree-row[data-off~="locked"] .knobs-tree-label{color:var(--_k-fg-2)}
2973
- .knobs-tree-detail{flex:none;margin-left:6px;color:var(--_k-fg-3);font-variant-numeric:tabular-nums}
2974
- .knobs-tree-tools{flex:none;display:flex;align-items:center;gap:1px;margin-left:4px}
2976
+ .knobs-tree-detail{flex:1 1 0;min-width:0;overflow:hidden;padding-left:6px;text-align:right;color:var(--_k-fg-3);font-variant-numeric:tabular-nums}
2977
+ .knobs-tree-row[data-editing]>.knobs-tree-detail{flex-grow:0;visibility:hidden}
2978
+ .knobs-tree-tools{flex:none;display:flex;align-items:center;gap:1px;margin-left:auto;padding-left:4px}
2975
2979
  .knobs-ui .knobs-tree-toggle{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:calc(var(--_k-radius) * 999);color:var(--_k-fg-3);cursor:pointer;opacity:0;transition:opacity .12s,color .12s,background .12s}
2976
2980
  .knobs-ui .knobs-tree-toggle svg{width:12px;height:12px}
2977
2981
  .knobs-tree-row:is(:hover,[data-active],[aria-selected="true"]) .knobs-tree-toggle,.knobs-ui .knobs-tree-toggle[aria-pressed="false"]{opacity:1}
@@ -4576,13 +4580,13 @@ function orderHintGroups(groups) {
4576
4580
  }
4577
4581
  function pageHints(doc = document) {
4578
4582
  const holder = doc;
4579
- const registry = holder[KEY] ?? createRegistry(doc);
4580
- holder[KEY] = registry;
4583
+ const registry = holder[KEY2] ?? createRegistry(doc);
4584
+ holder[KEY2] = registry;
4581
4585
  return registry;
4582
4586
  }
4583
4587
  function claimsHintKey(doc, key) {
4584
4588
  const holder = doc;
4585
- return key === PAGE_HINT_KEY && Boolean(holder[KEY]?.listening);
4589
+ return key === PAGE_HINT_KEY && Boolean(holder[KEY2]?.listening);
4586
4590
  }
4587
4591
  function activateElement(el) {
4588
4592
  const pressable = el.matches(PRESSABLE) || el.tabIndex < 0 && !el.matches("input,select,textarea");
@@ -4948,7 +4952,7 @@ function layerPainter(doc, groups) {
4948
4952
  }
4949
4953
  };
4950
4954
  }
4951
- var HINT_LETTERS = "asdfghjkleruiowtyqpcvnmxzb", HOME_ROW = 9, COLUMN = 200, PAGE_HINT_KEY = "s", KEY, hints, isTyping, PRESSABLE = "button,a[href],summary,[role=button],[role=tab],[role=radio],[role=switch],[role=checkbox],[role=menuitem],[role=option]", labelOf = (el) => (el.dataset.hint || el.getAttribute("aria-label") || el.textContent || "").trim(), STYLE_ID = "knobs-page-hints-css", CSS = `
4955
+ var HINT_LETTERS = "asdfghjkleruiowtyqpcvnmxzb", HOME_ROW = 9, COLUMN = 200, PAGE_HINT_KEY = "s", KEY2, hints, isTyping, PRESSABLE = "button,a[href],summary,[role=button],[role=tab],[role=radio],[role=switch],[role=checkbox],[role=menuitem],[role=option]", labelOf = (el) => (el.dataset.hint || el.getAttribute("aria-label") || el.textContent || "").trim(), STYLE_ID = "knobs-page-hints-css", CSS = `
4952
4956
  .knobs-page-hints{position:fixed;left:0;top:0;width:0;height:0;pointer-events:none;z-index:2147483000}
4953
4957
  .knobs-page-hint{position:fixed;transform:translateY(-50%);display:inline-flex;align-items:center;height:15px;padding:0 4px;border-radius:var(--kh-radius);background:var(--kh-accent);color:var(--kh-bg);
4954
4958
  box-shadow:0 1px 3px rgba(0,0,0,calc(.35 * var(--kh-lift)));font:700 var(--kh-size)/1 var(--kh-mono);letter-spacing:.04em;text-transform:uppercase;white-space:nowrap;pointer-events:none;
@@ -4962,7 +4966,7 @@ var HINT_LETTERS = "asdfghjkleruiowtyqpcvnmxzb", HOME_ROW = 9, COLUMN = 200, PAG
4962
4966
  var init_page_hints = __esm(() => {
4963
4967
  init_keys();
4964
4968
  init_hints();
4965
- KEY = Symbol.for("knobs.pageHints.v1");
4969
+ KEY2 = Symbol.for("knobs.pageHints.v1");
4966
4970
  hints = {
4967
4971
  register(root, targets, options) {
4968
4972
  return pageHints(root.ownerDocument).register(root, targets, options);
@@ -5327,7 +5331,7 @@ function tree(container, options) {
5327
5331
  text.className = "knobs-tree-text";
5328
5332
  label.append(text);
5329
5333
  const detail = doc.createElement("span");
5330
- detail.className = "knobs-tree-detail";
5334
+ detail.className = "knobs-tree-detail knobs-scroll-fade-x";
5331
5335
  const tools = doc.createElement("span");
5332
5336
  tools.className = "knobs-tree-tools";
5333
5337
  el.append(caret, icon, label, detail, tools);
@@ -7790,13 +7794,13 @@ function place(o) {
7790
7794
  if (vertical) {
7791
7795
  y = side === "bottom" ? a.y + a.h + offset : a.y - offset - size.h;
7792
7796
  x = align === "start" ? a.x : align === "end" ? a.x + a.w - size.w : a.x + (a.w - size.w) / 2;
7793
- x = clamp2(x, v.x + margin, v.x + v.w - margin - size.w);
7797
+ x = clamp3(x, v.x + margin, v.x + v.w - margin - size.w);
7794
7798
  } else {
7795
7799
  x = side === "right" ? a.x + a.w + offset : a.x - offset - size.w;
7796
7800
  y = align === "start" ? a.y : align === "end" ? a.y + a.h - size.h : a.y + (a.h - size.h) / 2;
7797
- y = clamp2(y, v.y + margin, v.y + v.h - margin - size.h);
7801
+ y = clamp3(y, v.y + margin, v.y + v.h - margin - size.h);
7798
7802
  }
7799
- const origin = vertical ? { x: clamp2(a.x + a.w / 2 - x, 0, size.w), y: side === "bottom" ? 0 : size.h } : { x: side === "right" ? 0 : size.w, y: clamp2(a.y + a.h / 2 - y, 0, size.h) };
7803
+ const origin = vertical ? { x: clamp3(a.x + a.w / 2 - x, 0, size.w), y: side === "bottom" ? 0 : size.h } : { x: side === "right" ? 0 : size.w, y: clamp3(a.y + a.h / 2 - y, 0, size.h) };
7800
7804
  return {
7801
7805
  x: Math.round(x),
7802
7806
  y: Math.round(y),
@@ -7838,7 +7842,7 @@ function safeTriangle(from, sub) {
7838
7842
  const edge = right ? sub.x : sub.x + sub.w;
7839
7843
  return [from, { x: edge, y: sub.y - 4 }, { x: edge, y: sub.y + sub.h + 4 }];
7840
7844
  }
7841
- var opposite, clamp2 = (v, lo, hi) => hi < lo ? lo : Math.max(lo, Math.min(hi, v));
7845
+ var opposite, clamp3 = (v, lo, hi) => hi < lo ? lo : Math.max(lo, Math.min(hi, v));
7842
7846
  var init_overlay_model = __esm(() => {
7843
7847
  opposite = { top: "bottom", bottom: "top", left: "right", right: "left" };
7844
7848
  });
@@ -11373,13 +11377,13 @@ function compare(container, options) {
11373
11377
  let renderer = "dom";
11374
11378
  let pending = false;
11375
11379
  let gpu = null;
11376
- const lay = new Spring(mode === "side" ? 0 : 1, LAYOUT2.omega, LAYOUT2.zeta, 0.0001);
11380
+ const lay = new Spring(mode === "side" ? 0 : 1, LAYOUT3.omega, LAYOUT3.zeta, 0.0001);
11377
11381
  const fade = new Spring(1, FADE6.omega, FADE6.zeta, 0.001);
11378
11382
  let fromMode = mode;
11379
11383
  let toMode = mode;
11380
11384
  const wipeLine = new Spring(wipe, WIPE_CATCH.omega, WIPE_CATCH.zeta, 0.0001);
11381
11385
  let wipeCatching = false;
11382
- const gripAt = new Spring(0.5, LAYOUT2.omega, LAYOUT2.zeta, 0.0001);
11386
+ const gripAt = new Spring(0.5, LAYOUT3.omega, LAYOUT3.zeta, 0.0001);
11383
11387
  let loupeHeld = false;
11384
11388
  let pointer = null;
11385
11389
  let drag = null;
@@ -12686,7 +12690,7 @@ function compare(container, options) {
12686
12690
  wake();
12687
12691
  return api;
12688
12692
  }
12689
- var LAYOUT2, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, TAP_SLOP = 4, INSET = 8, GRIP, MARK, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
12693
+ var LAYOUT3, FADE6, FADE_REDUCED, ZOOM, WIPE_CATCH, SLOP = 12, TAP_SLOP = 4, INSET = 8, GRIP, MARK, LOUPE, FAST_SEEK_FRAMES = 15, RELEASE_MS = 3000, MODE_LABEL;
12690
12694
  var init_compare = __esm(() => {
12691
12695
  init_motion();
12692
12696
  init_page_hints();
@@ -12700,7 +12704,7 @@ var init_compare = __esm(() => {
12700
12704
  init_selection_pill();
12701
12705
  init_timeline();
12702
12706
  init_timeline_model();
12703
- LAYOUT2 = springMotion(SPRINGS.default);
12707
+ LAYOUT3 = springMotion(SPRINGS.default);
12704
12708
  FADE6 = springMotion(SPRINGS.snappy);
12705
12709
  FADE_REDUCED = springMotion(FADE);
12706
12710
  ZOOM = springMotion({ duration: 0.35, bounce: 0 });
@@ -15744,6 +15748,977 @@ function bento(container, options) {
15744
15748
  };
15745
15749
  }
15746
15750
 
15751
+ // src/ui/canvas.ts
15752
+ init_glide();
15753
+ init_motion();
15754
+
15755
+ // src/ui/canvas-css.ts
15756
+ var canvasCss = `
15757
+ .knobs-ui.knobs-canvas{--_kc-radius:var(--k-ui-canvas-radius,var(--_k-radius-panel));--_kc-fade:var(--k-ui-canvas-fade,16px);
15758
+ position:relative;width:100%;height:100%;overflow:hidden;outline:none;background:var(--k-ui-canvas-bg,var(--_k-bg-page));contain:strict;
15759
+ touch-action:none;overscroll-behavior:none;user-select:none;-webkit-user-select:none;cursor:default;
15760
+ font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}
15761
+ :where(.knobs-canvas-stage,.knobs-canvas-stage *,.knobs-canvas-zoom,.knobs-canvas-zoom *){box-sizing:border-box}
15762
+ /* No hard edges: what slides out of view fades over the last few px. */
15763
+ .knobs-canvas-stage{position:absolute;inset:0;overflow:hidden;
15764
+ -webkit-mask-image:linear-gradient(to right,transparent,#000 var(--_kc-fade),#000 calc(100% - var(--_kc-fade)),transparent),linear-gradient(to bottom,transparent,#000 var(--_kc-fade),#000 calc(100% - var(--_kc-fade)),transparent);
15765
+ -webkit-mask-composite:source-in;mask-image:linear-gradient(to right,transparent,#000 var(--_kc-fade),#000 calc(100% - var(--_kc-fade)),transparent),linear-gradient(to bottom,transparent,#000 var(--_kc-fade),#000 calc(100% - var(--_kc-fade)),transparent);mask-composite:intersect}
15766
+ .knobs-canvas-dots{position:absolute;left:-128px;top:-128px;right:-128px;bottom:-128px;pointer-events:none;will-change:transform;
15767
+ background-image:radial-gradient(circle,var(--k-ui-canvas-dot,color-mix(in srgb,var(--_k-fg) 22%,transparent)) 1px,transparent 1.25px)}
15768
+ .knobs-canvas-world{position:absolute;left:0;top:0;width:0;height:0;transform-origin:0 0}
15769
+ .knobs-canvas[data-moving] .knobs-canvas-world{will-change:transform}
15770
+ .knobs-canvas:is([data-panning],[data-space]) .knobs-canvas-world{pointer-events:none}
15771
+ .knobs-canvas[data-space]{cursor:grab}
15772
+ .knobs-canvas[data-grabbing]{cursor:grabbing}
15773
+ .knobs-canvas-tile{position:absolute;left:0;top:0;display:flex;flex-direction:column;width:max-content;transform-origin:50% 50%}
15774
+ .knobs-canvas-title{flex:none;contain:inline-size;height:24px;padding:0 2px 8px;overflow:hidden;white-space:nowrap;color:var(--_k-fg-2);
15775
+ font-size:var(--_k-type-small,12px);line-height:16px;-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 16px),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 16px),transparent)}
15776
+ .knobs-canvas-card{position:relative;flex:none;padding:var(--k-ui-canvas-pad,16px);border-radius:var(--_kc-radius);background:var(--k-ui-canvas-card,var(--_k-bg));
15777
+ overflow:hidden;user-select:text;-webkit-user-select:text}
15778
+ .knobs-canvas-card::after{content:"";position:absolute;inset:0;border-radius:inherit;box-shadow:inset 0 0 0 1px var(--_k-border);pointer-events:none}
15779
+ /* The readout: how far, and the way back to the whole set or 100%. */
15780
+ .knobs-canvas-zoom{position:absolute;right:10px;bottom:10px;z-index:1;display:flex;align-items:center;gap:2px;height:28px;padding:0 4px;border-radius:999px;
15781
+ background:var(--_k-bg);box-shadow:inset 0 0 0 1px var(--_k-border);color:var(--_k-fg-2);font-size:var(--_k-type-small,12px);
15782
+ font-variant-numeric:tabular-nums;font-feature-settings:"tnum";white-space:nowrap}
15783
+ :where(.knobs-ui) .knobs-canvas-btn{all:revert;box-sizing:border-box;font:inherit;letter-spacing:inherit;margin:0;border:0;outline:none;box-shadow:none;
15784
+ display:inline-flex;align-items:center;justify-content:center;height:20px;min-width:20px;padding:0 6px;border-radius:999px;background:transparent;color:inherit;cursor:default;
15785
+ transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}
15786
+ :where(.knobs-ui) .knobs-canvas-btn:is(:hover,:focus-visible){background:var(--_k-fill-hover);color:var(--_k-fg);transition-duration:0ms}
15787
+ :where(.knobs-ui) .knobs-canvas-btn:focus-visible{box-shadow:0 0 0 1.5px var(--_k-accent)}
15788
+ .knobs-canvas-pct{min-width:46px}
15789
+ .knobs-canvas-btn svg{width:13px;height:13px}
15790
+ `;
15791
+
15792
+ // src/ui/canvas-model.ts
15793
+ var clamp2 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
15794
+ function packRows(sizes, opts) {
15795
+ const { gap, width } = opts;
15796
+ const x0 = opts.x ?? 0;
15797
+ let x = 0;
15798
+ let y = opts.y ?? 0;
15799
+ let rowH = 0;
15800
+ return sizes.map((s) => {
15801
+ if (x > 0 && x + s.w > width) {
15802
+ x = 0;
15803
+ y += rowH + gap;
15804
+ rowH = 0;
15805
+ }
15806
+ const r = { x: Math.round(x0 + x), y: Math.round(y), w: s.w, h: s.h };
15807
+ x += s.w + gap;
15808
+ rowH = Math.max(rowH, s.h);
15809
+ return r;
15810
+ });
15811
+ }
15812
+ function rowWidth(sizes, gap, aspect) {
15813
+ let widest = 0;
15814
+ let total = 0;
15815
+ for (const s of sizes) {
15816
+ widest = Math.max(widest, s.w);
15817
+ total += s.w + gap;
15818
+ }
15819
+ if (!sizes.length)
15820
+ return 0;
15821
+ const a = Number.isFinite(aspect) && aspect > 0 ? aspect : 16 / 10;
15822
+ const shape = (width) => {
15823
+ const b = boundsOf(packRows(sizes, { gap, width }));
15824
+ return b.w / Math.max(1, b.h);
15825
+ };
15826
+ let lo = widest;
15827
+ let hi = Math.max(widest, total);
15828
+ for (let i = 0;i < 24 && hi - lo > 1; i++) {
15829
+ const mid = (lo + hi) / 2;
15830
+ if (shape(mid) < a)
15831
+ lo = mid;
15832
+ else
15833
+ hi = mid;
15834
+ }
15835
+ return Math.abs(Math.log(shape(lo) / a)) <= Math.abs(Math.log(shape(hi) / a)) ? lo : hi;
15836
+ }
15837
+ function layoutCanvas(items, opts) {
15838
+ const placed = items.flatMap((it) => it.x !== undefined && it.y !== undefined ? [{ x: it.x, y: it.y, w: it.w, h: it.h }] : []);
15839
+ const free = items.filter((it) => it.x === undefined || it.y === undefined);
15840
+ const width = opts.width ?? rowWidth(free, opts.gap, opts.aspect ?? 16 / 10);
15841
+ const under = boundsOf(placed);
15842
+ const packed = packRows(free, {
15843
+ gap: opts.gap,
15844
+ width,
15845
+ x: under ? under.x : 0,
15846
+ y: under ? under.y + under.h + opts.gap * 2 : 0
15847
+ });
15848
+ let i = 0;
15849
+ const rects = items.map((it) => it.x !== undefined && it.y !== undefined ? { x: it.x, y: it.y, w: it.w, h: it.h } : packed[i++]);
15850
+ return { rects, width };
15851
+ }
15852
+ function boundsOf(rects) {
15853
+ if (!rects.length)
15854
+ return null;
15855
+ let x0 = Number.POSITIVE_INFINITY;
15856
+ let y0 = Number.POSITIVE_INFINITY;
15857
+ let x1 = Number.NEGATIVE_INFINITY;
15858
+ let y1 = Number.NEGATIVE_INFINITY;
15859
+ for (const r of rects) {
15860
+ x0 = Math.min(x0, r.x);
15861
+ y0 = Math.min(y0, r.y);
15862
+ x1 = Math.max(x1, r.x + r.w);
15863
+ y1 = Math.max(y1, r.y + r.h);
15864
+ }
15865
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
15866
+ }
15867
+ function zoomAround(v, scale, at, min = 0, max = Number.POSITIVE_INFINITY) {
15868
+ const s = clamp2(scale, min, max);
15869
+ const k = s / v.scale;
15870
+ return { x: at.x - (at.x - v.x) * k, y: at.y - (at.y - v.y) * k, scale: s };
15871
+ }
15872
+ function pinchView(v, from, to, ratio, min = 0, max = Number.POSITIVE_INFINITY) {
15873
+ const z = zoomAround(v, v.scale * ratio, from, min, max);
15874
+ return { x: z.x + to.x - from.x, y: z.y + to.y - from.y, scale: z.scale };
15875
+ }
15876
+ var WHEEL_ZOOM_MAX = 1.25;
15877
+ function wheelZoom(deltaY, pinch) {
15878
+ const f = Math.exp(-deltaY * (pinch ? 0.01 : 0.0025));
15879
+ return clamp2(f, 1 / WHEEL_ZOOM_MAX, WHEEL_ZOOM_MAX);
15880
+ }
15881
+ function fitView(r, w, h, opts = {}) {
15882
+ const pad = opts.pad ?? 0;
15883
+ const room = (n) => Math.max(1, n - pad * 2);
15884
+ const raw = Math.min(room(w) / Math.max(1, r.w), room(h) / Math.max(1, r.h));
15885
+ const scale = clamp2(raw, opts.min ?? 0, opts.max ?? Number.POSITIVE_INFINITY);
15886
+ return {
15887
+ x: w / 2 - (r.x + r.w / 2) * scale,
15888
+ y: h / 2 - (r.y + r.h / 2) * scale,
15889
+ scale
15890
+ };
15891
+ }
15892
+ function viewPath(from, to) {
15893
+ const k = to.scale / from.scale;
15894
+ if (!Number.isFinite(k) || Math.abs(Math.log(k)) < 0.0001)
15895
+ return (t) => ({
15896
+ x: from.x + (to.x - from.x) * t,
15897
+ y: from.y + (to.y - from.y) * t,
15898
+ scale: from.scale + (to.scale - from.scale) * t
15899
+ });
15900
+ const fx = (to.x - k * from.x) / (1 - k);
15901
+ const fy = (to.y - k * from.y) / (1 - k);
15902
+ const wx = (fx - from.x) / from.scale;
15903
+ const wy = (fy - from.y) / from.scale;
15904
+ return (t) => {
15905
+ if (t >= 1)
15906
+ return to;
15907
+ const scale = from.scale * k ** t;
15908
+ return { x: fx - wx * scale, y: fy - wy * scale, scale };
15909
+ };
15910
+ }
15911
+ var snapView = (v, dpr) => {
15912
+ const d = dpr > 0 ? dpr : 1;
15913
+ return { x: Math.round(v.x * d) / d, y: Math.round(v.y * d) / d, scale: v.scale };
15914
+ };
15915
+ var smooth = (a, b, x) => {
15916
+ const t = clamp2((x - a) / (b - a), 0, 1);
15917
+ return t * t * (3 - 2 * t);
15918
+ };
15919
+ function dotGrid(scale, step = 24) {
15920
+ const size = step * scale;
15921
+ const alpha = smooth(7, 14, size) * (1 - smooth(72, 128, size));
15922
+ return { size, alpha };
15923
+ }
15924
+ function keyAction(e) {
15925
+ if (e.altKey)
15926
+ return null;
15927
+ switch (e.key) {
15928
+ case "0":
15929
+ return "fit";
15930
+ case "1":
15931
+ return "actual";
15932
+ case "=":
15933
+ case "+":
15934
+ return "in";
15935
+ case "-":
15936
+ case "_":
15937
+ return "out";
15938
+ default:
15939
+ return null;
15940
+ }
15941
+ }
15942
+ var KEY_ZOOM = 1.5;
15943
+ function releaseVelocity(samples, now, window2 = 80) {
15944
+ const last = samples.at(-1);
15945
+ if (!last || now - last.t > 60)
15946
+ return { x: 0, y: 0 };
15947
+ let first = last;
15948
+ for (let i = samples.length - 1;i >= 0; i--) {
15949
+ if (last.t - samples[i].t > window2)
15950
+ break;
15951
+ first = samples[i];
15952
+ }
15953
+ const dt = (last.t - first.t) / 1000;
15954
+ if (dt <= 0)
15955
+ return { x: 0, y: 0 };
15956
+ return { x: (last.x - first.x) / dt, y: (last.y - first.y) / dt };
15957
+ }
15958
+ var COAST_TAU = -1 / (1000 * Math.log(0.998));
15959
+
15960
+ // src/ui/canvas.ts
15961
+ init_look();
15962
+ var FIT = springMotion(SPRINGS.smooth);
15963
+ var KEY = springMotion(SPRINGS.default);
15964
+ var LAYOUT2 = springMotion(SPRINGS.calm);
15965
+ var FOCUS_MAX = 2;
15966
+ var MAX_FLING = 8000;
15967
+ var SETTLE_MS = 200;
15968
+ var INTERACTIVE = "a,button,input,select,textarea,label,summary,[contenteditable],[role=button],[role=slider],[role=switch],[role=checkbox],[role=tab],[role=option],[role=textbox]";
15969
+ var ICON_FIT = svg('<path d="M2.5 6V3.5a1 1 0 0 1 1-1H6M10 2.5h2.5a1 1 0 0 1 1 1V6M13.5 10v2.5a1 1 0 0 1-1 1H10M6 13.5H3.5a1 1 0 0 1-1-1V10"/>');
15970
+ var injectCanvasStyles = (doc) => {
15971
+ if (doc.querySelector("style[data-knobs-canvas]"))
15972
+ return;
15973
+ const style = doc.createElement("style");
15974
+ style.dataset.knobsCanvas = "";
15975
+ style.textContent = aliased(canvasCss);
15976
+ doc.head.append(style);
15977
+ };
15978
+ function canvas(container, options) {
15979
+ const doc = container.ownerDocument;
15980
+ const win = doc.defaultView;
15981
+ const gap = options.gap ?? 48;
15982
+ const pad = options.padding ?? 48;
15983
+ const minZoom = options.minZoom ?? 0.05;
15984
+ const maxZoom = options.maxZoom ?? 8;
15985
+ let items = options.items;
15986
+ const root = doc.createElement("div");
15987
+ root.className = "knobs-canvas";
15988
+ root.tabIndex = 0;
15989
+ root.setAttribute("role", "region");
15990
+ root.setAttribute("aria-roledescription", "canvas");
15991
+ root.setAttribute("aria-label", options.label ?? "Canvas");
15992
+ applyLook2(root, options);
15993
+ injectCanvasStyles(doc);
15994
+ const stage = doc.createElement("div");
15995
+ stage.className = "knobs-canvas-stage";
15996
+ const dots = doc.createElement("div");
15997
+ dots.className = "knobs-canvas-dots";
15998
+ const world = doc.createElement("div");
15999
+ world.className = "knobs-canvas-world";
16000
+ stage.append(dots, world);
16001
+ root.append(stage);
16002
+ let pct = null;
16003
+ if (options.readout !== false) {
16004
+ const bar = doc.createElement("div");
16005
+ bar.className = "knobs-canvas-zoom";
16006
+ const fitBtn = doc.createElement("button");
16007
+ fitBtn.type = "button";
16008
+ fitBtn.className = "knobs-canvas-btn";
16009
+ fitBtn.innerHTML = ICON_FIT;
16010
+ fitBtn.title = "Fit (0)";
16011
+ fitBtn.setAttribute("aria-label", "Fit");
16012
+ fitBtn.addEventListener("click", () => api.fit());
16013
+ pct = doc.createElement("button");
16014
+ pct.type = "button";
16015
+ pct.className = "knobs-canvas-btn knobs-canvas-pct";
16016
+ pct.title = "100% (1)";
16017
+ pct.setAttribute("aria-label", "Zoom to 100%");
16018
+ pct.addEventListener("click", () => api.zoomTo(1));
16019
+ bar.append(fitBtn, pct);
16020
+ bar.addEventListener("dblclick", (e) => e.stopPropagation());
16021
+ root.append(bar);
16022
+ }
16023
+ container.append(root);
16024
+ const motionQuery = win.matchMedia?.("(prefers-reduced-motion: reduce)");
16025
+ const reduced = () => motionQuery?.matches ?? reducedMotion(win);
16026
+ let view = { x: 0, y: 0, scale: 1 };
16027
+ let vw = 0;
16028
+ let vh = 0;
16029
+ let pristine = true;
16030
+ let rowW;
16031
+ let rects = new Map;
16032
+ let jump = null;
16033
+ let coast = null;
16034
+ let frames = 0;
16035
+ const times = [];
16036
+ const tiles = new Map;
16037
+ let drawnView = "";
16038
+ let drawnDots = "";
16039
+ let drawnPct = "";
16040
+ const drawView = () => {
16041
+ const v = view;
16042
+ const key = `${v.x}|${v.y}|${v.scale}`;
16043
+ if (key === drawnView)
16044
+ return;
16045
+ drawnView = key;
16046
+ world.style.transform = `translate(${v.x}px,${v.y}px) scale(${v.scale})`;
16047
+ const g = dotGrid(v.scale);
16048
+ if (g.alpha <= 0.001) {
16049
+ if (drawnDots !== "off")
16050
+ dots.style.visibility = "hidden";
16051
+ drawnDots = "off";
16052
+ } else {
16053
+ const ox = ((v.x + 128 - g.size / 2) % g.size + g.size) % g.size;
16054
+ const oy = ((v.y + 128 - g.size / 2) % g.size + g.size) % g.size;
16055
+ const size = `${g.size}px ${g.size}px`;
16056
+ const a = String(Math.round(g.alpha * 1000) / 1000);
16057
+ if (drawnDots === "off")
16058
+ dots.style.visibility = "";
16059
+ if (dots.style.backgroundSize !== size)
16060
+ dots.style.backgroundSize = size;
16061
+ if (dots.style.opacity !== a)
16062
+ dots.style.opacity = a;
16063
+ dots.style.transform = `translate(${ox}px,${oy}px)`;
16064
+ drawnDots = "on";
16065
+ }
16066
+ if (pct) {
16067
+ const label = `${Math.round(v.scale * 100)}%`;
16068
+ if (label !== drawnPct)
16069
+ pct.textContent = drawnPct = label;
16070
+ }
16071
+ options.onView?.(v);
16072
+ };
16073
+ let settleTimer = 0;
16074
+ let held = false;
16075
+ let layerScale = 0;
16076
+ const settle = () => {
16077
+ settleTimer = 0;
16078
+ if (held)
16079
+ return;
16080
+ if (loop.running) {
16081
+ settleTimer = win.setTimeout(settle, SETTLE_MS);
16082
+ return;
16083
+ }
16084
+ view = snapView(view, win.devicePixelRatio || 1);
16085
+ if (Math.abs(view.scale / layerScale - 1) > 0.000001) {
16086
+ delete root.dataset.moving;
16087
+ layerScale = 0;
16088
+ }
16089
+ drawView();
16090
+ };
16091
+ const moved = () => {
16092
+ if (!layerScale) {
16093
+ root.dataset.moving = "";
16094
+ layerScale = view.scale;
16095
+ }
16096
+ win.clearTimeout(settleTimer);
16097
+ settleTimer = win.setTimeout(settle, SETTLE_MS);
16098
+ };
16099
+ const setView = (v) => {
16100
+ view = v;
16101
+ moved();
16102
+ drawView();
16103
+ };
16104
+ const drawTile = (t) => {
16105
+ const key = `${t.x.x}|${t.y.x}`;
16106
+ if (key === t.drawn)
16107
+ return;
16108
+ t.drawn = key;
16109
+ t.el.style.translate = `${t.x.x}px ${t.y.x}px`;
16110
+ };
16111
+ const sizeCard = (t) => {
16112
+ const { w, h } = t.item;
16113
+ t.card.style.width = w === undefined ? "" : `${w}px`;
16114
+ t.card.style.height = h === undefined ? "" : `${h}px`;
16115
+ };
16116
+ const setTitle = (t) => {
16117
+ const text = t.item.title;
16118
+ if (text && !t.title) {
16119
+ t.title = doc.createElement("div");
16120
+ t.title.className = "knobs-canvas-title";
16121
+ t.el.prepend(t.title);
16122
+ }
16123
+ if (t.title) {
16124
+ if (text)
16125
+ t.title.textContent = text;
16126
+ else {
16127
+ t.title.remove();
16128
+ t.title = null;
16129
+ }
16130
+ }
16131
+ };
16132
+ const mount = (it, enter) => {
16133
+ const el = doc.createElement("div");
16134
+ el.className = "knobs-canvas-tile";
16135
+ el.dataset.id = it.id;
16136
+ const card = doc.createElement("div");
16137
+ card.className = "knobs-canvas-card";
16138
+ el.append(card);
16139
+ const t = {
16140
+ id: it.id,
16141
+ item: it,
16142
+ el,
16143
+ title: null,
16144
+ card,
16145
+ cleanup: undefined,
16146
+ x: new Spring(0, LAYOUT2.omega, LAYOUT2.zeta, 0.05),
16147
+ y: new Spring(0, LAYOUT2.omega, LAYOUT2.zeta, 0.05),
16148
+ w: 0,
16149
+ h: 0,
16150
+ placed: false,
16151
+ leaving: false,
16152
+ gone: false,
16153
+ drawn: ""
16154
+ };
16155
+ setTitle(t);
16156
+ sizeCard(t);
16157
+ world.append(el);
16158
+ t.cleanup = it.render(card) || undefined;
16159
+ tiles.set(it.id, t);
16160
+ ro.observe(el);
16161
+ if (enter) {
16162
+ const r = reduced();
16163
+ el.animate(r ? [{ opacity: 0 }, { opacity: 1 }] : [
16164
+ { opacity: 0, scale: 0.96, filter: "blur(3px)" },
16165
+ { opacity: 1, scale: 1, filter: "blur(0px)" }
16166
+ ], { duration: r ? 160 : 260, easing: "cubic-bezier(.2,.8,.2,1)" });
16167
+ }
16168
+ return t;
16169
+ };
16170
+ const unmount = (t) => {
16171
+ if (t.gone)
16172
+ return;
16173
+ t.gone = true;
16174
+ ro.unobserve(t.el);
16175
+ t.cleanup?.();
16176
+ t.el.remove();
16177
+ if (tiles.get(t.id) === t)
16178
+ tiles.delete(t.id);
16179
+ };
16180
+ const leave = (t) => {
16181
+ t.leaving = true;
16182
+ tiles.delete(t.id);
16183
+ t.el.style.pointerEvents = "none";
16184
+ const r = reduced();
16185
+ const a = t.el.animate(r ? [{ opacity: 1 }, { opacity: 0 }] : [
16186
+ { opacity: 1, scale: 1, filter: "blur(0px)" },
16187
+ { opacity: 0, scale: 0.96, filter: "blur(3px)" }
16188
+ ], { duration: r ? 160 : 200, easing: "cubic-bezier(.4,0,1,1)", fill: "forwards" });
16189
+ a.onfinish = () => {
16190
+ leaving.delete(t);
16191
+ unmount(t);
16192
+ };
16193
+ leaving.add(t);
16194
+ };
16195
+ const leaving = new Set;
16196
+ const measure = () => {
16197
+ for (const t of tiles.values()) {
16198
+ t.w = t.el.offsetWidth;
16199
+ t.h = t.el.offsetHeight;
16200
+ }
16201
+ };
16202
+ const fitTarget = () => {
16203
+ const b = boundsOf([...rects.values()]);
16204
+ if (!b || !vw || !vh)
16205
+ return null;
16206
+ return fitView(b, vw, vh, { pad, min: minZoom, max: Math.min(maxZoom, 1) });
16207
+ };
16208
+ const relayout = (glide) => {
16209
+ const live = items.map((it) => tiles.get(it.id)).filter(Boolean);
16210
+ const sizes = live.map((t) => ({ x: t.item.x, y: t.item.y, w: t.w, h: t.h }));
16211
+ if (pristine)
16212
+ rowW = options.width;
16213
+ const out = layoutCanvas(sizes, {
16214
+ gap,
16215
+ width: rowW,
16216
+ aspect: vw && vh ? vw / vh : undefined
16217
+ });
16218
+ rowW = out.width;
16219
+ const next = new Map;
16220
+ const animate = glide && !pristine && !reduced();
16221
+ let moving = false;
16222
+ live.forEach((t, i) => {
16223
+ const r = out.rects[i];
16224
+ next.set(t.id, r);
16225
+ if (!animate || !t.placed) {
16226
+ t.x.snap(r.x);
16227
+ t.y.snap(r.y);
16228
+ } else {
16229
+ tune(t.x, LAYOUT2).set(r.x);
16230
+ tune(t.y, LAYOUT2).set(r.y);
16231
+ moving ||= !t.x.resting || !t.y.resting;
16232
+ }
16233
+ t.placed = true;
16234
+ drawTile(t);
16235
+ });
16236
+ rects = next;
16237
+ if (pristine) {
16238
+ const f = fitTarget();
16239
+ if (f) {
16240
+ view = snapView(f, win.devicePixelRatio || 1);
16241
+ drawView();
16242
+ }
16243
+ }
16244
+ if (moving)
16245
+ loop.wake();
16246
+ };
16247
+ let relayoutQueued = 0;
16248
+ const ro = new win.ResizeObserver((entries) => {
16249
+ let changed = false;
16250
+ for (const e of entries) {
16251
+ const t = tiles.get(e.target.dataset.id ?? "");
16252
+ if (!t || t.el !== e.target)
16253
+ continue;
16254
+ const w = t.el.offsetWidth;
16255
+ const h = t.el.offsetHeight;
16256
+ if (w !== t.w || h !== t.h) {
16257
+ t.w = w;
16258
+ t.h = h;
16259
+ changed = true;
16260
+ }
16261
+ }
16262
+ if (changed && !relayoutQueued)
16263
+ relayoutQueued = win.requestAnimationFrame(() => {
16264
+ relayoutQueued = 0;
16265
+ relayout(true);
16266
+ });
16267
+ });
16268
+ const rootRo = new win.ResizeObserver(() => {
16269
+ const w = root.clientWidth;
16270
+ const h = root.clientHeight;
16271
+ if (w === vw && h === vh)
16272
+ return;
16273
+ const was = { w: vw, h: vh };
16274
+ vw = w;
16275
+ vh = h;
16276
+ if (pristine)
16277
+ return relayout(false);
16278
+ if (was.w && was.h) {
16279
+ const dx = (w - was.w) / 2;
16280
+ const dy = (h - was.h) / 2;
16281
+ if (jump)
16282
+ startJump({ x: jump.to.x + dx, y: jump.to.y + dy, scale: jump.to.scale }, jump.t.omega === FIT.omega ? FIT : KEY);
16283
+ view = { x: view.x + dx, y: view.y + dy, scale: view.scale };
16284
+ drawView();
16285
+ }
16286
+ });
16287
+ const frame = (dt) => {
16288
+ const t0 = performance.now();
16289
+ frames++;
16290
+ let moving = false;
16291
+ if (jump) {
16292
+ jump.t.step(dt);
16293
+ if (jump.t.resting) {
16294
+ view = jump.to;
16295
+ jump = null;
16296
+ } else {
16297
+ view = jump.path(Math.min(1, jump.t.x));
16298
+ moving = true;
16299
+ }
16300
+ moved();
16301
+ }
16302
+ if (coast) {
16303
+ const decay = Math.exp(-dt / COAST_TAU);
16304
+ const travel = COAST_TAU * (1 - decay);
16305
+ let v = {
16306
+ x: view.x + coast.vx * travel,
16307
+ y: view.y + coast.vy * travel,
16308
+ scale: view.scale
16309
+ };
16310
+ if (coast.vs)
16311
+ v = zoomAround(v, v.scale * Math.exp(coast.vs * travel), coast.at, minZoom, maxZoom);
16312
+ view = v;
16313
+ coast.vx *= decay;
16314
+ coast.vy *= decay;
16315
+ coast.vs *= decay;
16316
+ if (Math.hypot(coast.vx, coast.vy) < 10 && Math.abs(coast.vs) < 0.02)
16317
+ coast = null;
16318
+ else
16319
+ moving = true;
16320
+ moved();
16321
+ }
16322
+ drawView();
16323
+ for (const t of tiles.values())
16324
+ if (!t.x.resting || !t.y.resting) {
16325
+ t.x.step(dt);
16326
+ t.y.step(dt);
16327
+ drawTile(t);
16328
+ moving = true;
16329
+ }
16330
+ times.push(performance.now() - t0);
16331
+ if (times.length > 600)
16332
+ times.splice(0, times.length - 600);
16333
+ return moving;
16334
+ };
16335
+ const loop = frameLoop(win, frame);
16336
+ const pathLength = (a, b) => Math.abs(Math.log(b.scale / a.scale)) + Math.hypot(b.x - a.x, b.y - a.y) / Math.max(1, vw, vh);
16337
+ const startJump = (to, m) => {
16338
+ pristine = false;
16339
+ coast = null;
16340
+ if (reduced()) {
16341
+ jump = null;
16342
+ setView(to);
16343
+ return;
16344
+ }
16345
+ let v = 0;
16346
+ if (jump) {
16347
+ const len = pathLength(view, to);
16348
+ if (len > 0.000001)
16349
+ v = clamp2(jump.t.v * pathLength(jump.from, jump.to) / len, 0, 40);
16350
+ }
16351
+ const t = new Spring(0, m.omega, m.zeta, 0.0005).set(1);
16352
+ t.v = v;
16353
+ jump = { from: view, to, path: viewPath(view, to), t };
16354
+ moved();
16355
+ loop.wake();
16356
+ };
16357
+ const target = () => jump?.to ?? view;
16358
+ const centre = () => ({ x: vw / 2, y: vh / 2 });
16359
+ const grab = () => {
16360
+ pristine = false;
16361
+ jump = null;
16362
+ coast = null;
16363
+ };
16364
+ const local = (e) => {
16365
+ const r = root.getBoundingClientRect();
16366
+ return { x: e.clientX - r.left, y: e.clientY - r.top };
16367
+ };
16368
+ let pointer = null;
16369
+ let pinching = false;
16370
+ const onWheel = (e) => {
16371
+ e.preventDefault();
16372
+ const k = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? vh : 1;
16373
+ if (e.ctrlKey || e.metaKey) {
16374
+ if (pinching)
16375
+ return;
16376
+ const dy = e.deltaY * k;
16377
+ const at = local(e);
16378
+ grab();
16379
+ setView(zoomAround(view, view.scale * wheelZoom(dy, e.ctrlKey), at, minZoom, maxZoom));
16380
+ return;
16381
+ }
16382
+ let dx = e.deltaX * k;
16383
+ let dy = e.deltaY * k;
16384
+ if (e.shiftKey && !dx) {
16385
+ dx = dy;
16386
+ dy = 0;
16387
+ }
16388
+ grab();
16389
+ setView({ x: view.x - dx, y: view.y - dy, scale: view.scale });
16390
+ };
16391
+ let gestureScale = 1;
16392
+ const onGesture = (e) => {
16393
+ e.preventDefault();
16394
+ const g = e;
16395
+ if (e.type === "gesturestart") {
16396
+ gestureScale = 1;
16397
+ pinching = true;
16398
+ grab();
16399
+ return;
16400
+ }
16401
+ if (e.type === "gestureend") {
16402
+ pinching = false;
16403
+ return;
16404
+ }
16405
+ const at = g.clientX !== undefined ? local(g) : centre();
16406
+ setView(zoomAround(view, view.scale * g.scale / gestureScale, at, minZoom, maxZoom));
16407
+ gestureScale = g.scale;
16408
+ };
16409
+ let drag = null;
16410
+ const touches = new Map;
16411
+ let pinch = null;
16412
+ let space = false;
16413
+ let spaceDragged = false;
16414
+ let hovering = false;
16415
+ let pressed = null;
16416
+ const capped = (v) => {
16417
+ const speed = Math.hypot(v.x, v.y);
16418
+ return speed > MAX_FLING ? { x: v.x / speed * MAX_FLING, y: v.y / speed * MAX_FLING } : v;
16419
+ };
16420
+ const onBackground = (target) => {
16421
+ const el = target;
16422
+ return !el?.closest?.(".knobs-canvas-card,.knobs-canvas-zoom");
16423
+ };
16424
+ const beginDrag = (e, grabbing) => {
16425
+ grab();
16426
+ held = true;
16427
+ drag = { id: e.pointerId, last: local(e), samples: [] };
16428
+ root.dataset.panning = "";
16429
+ if (space)
16430
+ spaceDragged = true;
16431
+ if (grabbing)
16432
+ root.dataset.grabbing = "";
16433
+ try {
16434
+ root.setPointerCapture(e.pointerId);
16435
+ } catch {}
16436
+ moved();
16437
+ };
16438
+ const endDrag = (coastOn) => {
16439
+ const d = drag;
16440
+ drag = null;
16441
+ held = false;
16442
+ delete root.dataset.panning;
16443
+ delete root.dataset.grabbing;
16444
+ if (!d)
16445
+ return;
16446
+ const v = coastOn ? capped(releaseVelocity(d.samples, performance.now())) : { x: 0, y: 0 };
16447
+ if (Math.hypot(v.x, v.y) > 60) {
16448
+ coast = { vx: v.x, vy: v.y, vs: 0, at: centre() };
16449
+ loop.wake();
16450
+ }
16451
+ moved();
16452
+ };
16453
+ const spread = () => {
16454
+ const [a, b] = [...touches.values()];
16455
+ return a && b ? { d: Math.hypot(a.x - b.x, a.y - b.y), mid: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 } } : null;
16456
+ };
16457
+ const onPointerDown = (e) => {
16458
+ pristine = false;
16459
+ if (e.isPrimary)
16460
+ pressed = e.target;
16461
+ if (e.pointerType === "touch") {
16462
+ touches.set(e.pointerId, local(e));
16463
+ if (touches.size === 2) {
16464
+ if (drag)
16465
+ endDrag(false);
16466
+ const s = spread();
16467
+ grab();
16468
+ held = true;
16469
+ pinch = { d: s.d, mid: s.mid, samples: [] };
16470
+ root.dataset.panning = "";
16471
+ for (const id of touches.keys())
16472
+ try {
16473
+ root.setPointerCapture(id);
16474
+ } catch {}
16475
+ e.preventDefault();
16476
+ e.stopPropagation();
16477
+ return;
16478
+ }
16479
+ if (touches.size === 1 && onBackground(e.target))
16480
+ beginDrag(e, false);
16481
+ return;
16482
+ }
16483
+ const middle = e.button === 1;
16484
+ if (!middle && e.button !== 0)
16485
+ return;
16486
+ if (!middle && !space && !onBackground(e.target))
16487
+ return;
16488
+ e.preventDefault();
16489
+ if (!middle && onBackground(e.target))
16490
+ root.focus({ preventScroll: true });
16491
+ beginDrag(e, middle || space);
16492
+ };
16493
+ const onPointerMove = (e) => {
16494
+ if (e.pointerType !== "touch")
16495
+ pointer = local(e);
16496
+ if (touches.has(e.pointerId))
16497
+ touches.set(e.pointerId, local(e));
16498
+ if (pinch) {
16499
+ const s = spread();
16500
+ if (!s)
16501
+ return;
16502
+ const v = pinchView(view, pinch.mid, s.mid, s.d / Math.max(1, pinch.d), minZoom, maxZoom);
16503
+ pinch.d = s.d;
16504
+ pinch.mid = s.mid;
16505
+ pinch.samples.push({ x: s.mid.x, y: s.mid.y, t: performance.now(), s: Math.log(v.scale) });
16506
+ if (pinch.samples.length > 12)
16507
+ pinch.samples.shift();
16508
+ setView(v);
16509
+ return;
16510
+ }
16511
+ if (!drag || drag.id !== e.pointerId)
16512
+ return;
16513
+ const p = local(e);
16514
+ const dx = p.x - drag.last.x;
16515
+ const dy = p.y - drag.last.y;
16516
+ drag.last = p;
16517
+ drag.samples.push({ x: p.x, y: p.y, t: performance.now() });
16518
+ if (drag.samples.length > 12)
16519
+ drag.samples.shift();
16520
+ setView({ x: view.x + dx, y: view.y + dy, scale: view.scale });
16521
+ };
16522
+ const onPointerUp = (e) => {
16523
+ if (touches.delete(e.pointerId) && pinch) {
16524
+ if (touches.size < 2) {
16525
+ const p = pinch;
16526
+ pinch = null;
16527
+ held = false;
16528
+ delete root.dataset.panning;
16529
+ const now = performance.now();
16530
+ const v = capped(releaseVelocity(p.samples, now));
16531
+ const first = p.samples.find((s) => now - s.t <= 80) ?? p.samples[0];
16532
+ const last = p.samples.at(-1);
16533
+ const dt = first && last ? (last.t - first.t) / 1000 : 0;
16534
+ const vs = dt > 0 && now - last.t < 60 ? clamp2((last.s - first.s) / dt, -4, 4) : 0;
16535
+ if (Math.hypot(v.x, v.y) > 60 || Math.abs(vs) > 0.3) {
16536
+ coast = { vx: v.x, vy: v.y, vs, at: p.mid };
16537
+ loop.wake();
16538
+ }
16539
+ moved();
16540
+ }
16541
+ return;
16542
+ }
16543
+ if (drag && drag.id === e.pointerId)
16544
+ endDrag(e.type === "pointerup");
16545
+ };
16546
+ const onDblClick = (e) => {
16547
+ if (space)
16548
+ return;
16549
+ const el = e.target === root && pressed && root.contains(pressed) ? pressed : e.target;
16550
+ const tileEl = el.closest?.(".knobs-canvas-tile");
16551
+ if (tileEl) {
16552
+ const card = el.closest(".knobs-canvas-card");
16553
+ if (card && (el.closest(INTERACTIVE) || isTypingTarget(el)))
16554
+ return;
16555
+ const id = tileEl.dataset.id;
16556
+ const f = focusTarget(id);
16557
+ if (!f)
16558
+ return;
16559
+ const t = target();
16560
+ const there = Math.abs(t.scale / f.scale - 1) < 0.02 && Math.abs(t.x - f.x) < 4 && Math.abs(t.y - f.y) < 4;
16561
+ if (there)
16562
+ api.fit();
16563
+ else
16564
+ startJump(f, FIT);
16565
+ return;
16566
+ }
16567
+ if (onBackground(el))
16568
+ api.fit();
16569
+ };
16570
+ const keyAnchor = () => hovering && pointer ? pointer : centre();
16571
+ const onKey = (e) => {
16572
+ if (isTypingTarget(e.target))
16573
+ return;
16574
+ const a = keyAction(e);
16575
+ if (!a || a === "actual" && (e.metaKey || e.ctrlKey))
16576
+ return;
16577
+ e.preventDefault();
16578
+ e.stopPropagation();
16579
+ if (a === "fit")
16580
+ return api.fit();
16581
+ const from = target();
16582
+ const scale = a === "actual" ? 1 : a === "in" ? from.scale * KEY_ZOOM : from.scale / KEY_ZOOM;
16583
+ startJump(zoomAround(from, scale, keyAnchor(), minZoom, maxZoom), KEY);
16584
+ };
16585
+ const spaceOwner = (e) => e.key === " " && !e.metaKey && !e.ctrlKey && !e.altKey && !isTypingTarget(e.target) && (hovering || root.contains(doc.activeElement));
16586
+ const onControl = () => {
16587
+ const el = doc.activeElement;
16588
+ return !!el && el !== root && root.contains(el) && !!el.closest(INTERACTIVE);
16589
+ };
16590
+ const onDocKeyDown = (e) => {
16591
+ if (!spaceOwner(e))
16592
+ return;
16593
+ if (!onControl())
16594
+ e.preventDefault();
16595
+ if (space)
16596
+ return;
16597
+ space = true;
16598
+ spaceDragged = false;
16599
+ root.dataset.space = "";
16600
+ };
16601
+ const endSpace = () => {
16602
+ space = false;
16603
+ delete root.dataset.space;
16604
+ if (!drag)
16605
+ delete root.dataset.grabbing;
16606
+ };
16607
+ const onDocKeyUp = (e) => {
16608
+ if (e.key !== " " || !space)
16609
+ return;
16610
+ if (spaceDragged || !onControl())
16611
+ e.preventDefault();
16612
+ endSpace();
16613
+ };
16614
+ root.addEventListener("wheel", onWheel, { passive: false });
16615
+ root.addEventListener("gesturestart", onGesture);
16616
+ root.addEventListener("gesturechange", onGesture);
16617
+ root.addEventListener("gestureend", onGesture);
16618
+ root.addEventListener("pointerdown", onPointerDown, { capture: true });
16619
+ root.addEventListener("pointermove", onPointerMove);
16620
+ root.addEventListener("pointerup", onPointerUp);
16621
+ root.addEventListener("pointercancel", onPointerUp);
16622
+ root.addEventListener("pointerenter", () => hovering = true);
16623
+ root.addEventListener("pointerleave", () => {
16624
+ hovering = false;
16625
+ });
16626
+ root.addEventListener("auxclick", (e) => e.button === 1 && e.preventDefault());
16627
+ root.addEventListener("dblclick", onDblClick);
16628
+ root.addEventListener("keydown", onKey);
16629
+ doc.addEventListener("keydown", onDocKeyDown, { capture: true });
16630
+ doc.addEventListener("keyup", onDocKeyUp, { capture: true });
16631
+ win.addEventListener("blur", endSpace);
16632
+ root.addEventListener("knobs-restyle", () => {
16633
+ measure();
16634
+ relayout(true);
16635
+ });
16636
+ const focusTarget = (id) => {
16637
+ const r = rects.get(id);
16638
+ if (!r || !vw || !vh)
16639
+ return null;
16640
+ return fitView(r, vw, vh, { pad, min: minZoom, max: Math.min(maxZoom, FOCUS_MAX) });
16641
+ };
16642
+ for (const it of items)
16643
+ mount(it, false);
16644
+ vw = root.clientWidth;
16645
+ vh = root.clientHeight;
16646
+ measure();
16647
+ relayout(false);
16648
+ rootRo.observe(root);
16649
+ drawView();
16650
+ const api = {
16651
+ element: root,
16652
+ get view() {
16653
+ return view;
16654
+ },
16655
+ get rects() {
16656
+ return rects;
16657
+ },
16658
+ fit() {
16659
+ const f = fitTarget();
16660
+ if (f)
16661
+ startJump(f, FIT);
16662
+ },
16663
+ focus(id) {
16664
+ const f = focusTarget(id);
16665
+ if (f)
16666
+ startJump(f, FIT);
16667
+ },
16668
+ zoomTo(scale, at) {
16669
+ const from = target();
16670
+ startJump(zoomAround(from, scale, at ?? centre(), minZoom, maxZoom), KEY);
16671
+ },
16672
+ update(next) {
16673
+ const ids = new Set(next.map((it) => it.id));
16674
+ for (const t of [...tiles.values()])
16675
+ if (!ids.has(t.id))
16676
+ leave(t);
16677
+ for (const it of next) {
16678
+ const t = tiles.get(it.id);
16679
+ if (!t) {
16680
+ mount(it, true);
16681
+ continue;
16682
+ }
16683
+ if (t.item.render !== it.render) {
16684
+ t.cleanup?.();
16685
+ t.card.replaceChildren();
16686
+ t.cleanup = it.render(t.card) || undefined;
16687
+ }
16688
+ t.item = it;
16689
+ setTitle(t);
16690
+ sizeCard(t);
16691
+ }
16692
+ items = next;
16693
+ measure();
16694
+ relayout(true);
16695
+ },
16696
+ get moving() {
16697
+ return loop.running;
16698
+ },
16699
+ stats() {
16700
+ const s = [...times].sort((a, b) => a - b);
16701
+ const q = (p) => Math.round((s[Math.floor(p * (s.length - 1))] ?? 0) * 1000) / 1000;
16702
+ return { frames, p50: q(0.5), p95: q(0.95), max: q(1) };
16703
+ },
16704
+ dispose() {
16705
+ loop.stop();
16706
+ ro.disconnect();
16707
+ rootRo.disconnect();
16708
+ win.clearTimeout(settleTimer);
16709
+ if (relayoutQueued)
16710
+ win.cancelAnimationFrame(relayoutQueued);
16711
+ doc.removeEventListener("keydown", onDocKeyDown, { capture: true });
16712
+ doc.removeEventListener("keyup", onDocKeyUp, { capture: true });
16713
+ win.removeEventListener("blur", endSpace);
16714
+ for (const t of [...tiles.values(), ...leaving])
16715
+ unmount(t);
16716
+ root.remove();
16717
+ }
16718
+ };
16719
+ return api;
16720
+ }
16721
+
15747
16722
  // src/ui/react.tsx
15748
16723
  init_diff();
15749
16724
 
@@ -23150,6 +24125,33 @@ function Bento(props) {
23150
24125
  style: { height: "100%", ...props.style }
23151
24126
  });
23152
24127
  }
24128
+ function Canvas(props) {
24129
+ const ref = useRef(null);
24130
+ const handle = useRef(null);
24131
+ const latest = useRef(props);
24132
+ latest.current = props;
24133
+ useLayoutEffect(() => {
24134
+ const c = canvas(ref.current, {
24135
+ ...latest.current,
24136
+ onView: (v) => latest.current.onView?.(v)
24137
+ });
24138
+ handle.current = c;
24139
+ latest.current.onReady?.(c);
24140
+ return () => {
24141
+ c.dispose();
24142
+ handle.current = null;
24143
+ };
24144
+ }, []);
24145
+ useFollow({ items: props.items }, (c) => {
24146
+ if (c.items)
24147
+ handle.current?.update(c.items);
24148
+ });
24149
+ return /* @__PURE__ */ jsx("div", {
24150
+ ref,
24151
+ className: props.className,
24152
+ style: { height: "100%", ...props.style }
24153
+ });
24154
+ }
23153
24155
  function Activity(props) {
23154
24156
  const ref = useRef(null);
23155
24157
  const handle = useRef(null);
@@ -23299,6 +24301,7 @@ function MediaSlot(props) {
23299
24301
  export {
23300
24302
  Activity,
23301
24303
  Bento,
24304
+ Canvas,
23302
24305
  CodeDiff,
23303
24306
  Compare,
23304
24307
  CompareStrip,