@kitlangton/knobs 0.4.0-next.11 → 0.4.0-next.13

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