@kitlangton/knobs 0.4.0-next.16 → 0.4.0-next.17

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.
@@ -46,7 +46,7 @@ import {
46
46
  pushReadout,
47
47
  READOUT,
48
48
  storageKey
49
- } from "./chunk-v2g91mrm.js";
49
+ } from "./chunk-2g0znytq.js";
50
50
  import {
51
51
  resolveLook,
52
52
  themeOf,
@@ -119,7 +119,7 @@ import {
119
119
  createHintController,
120
120
  pageHints,
121
121
  toggleLayer
122
- } from "./chunk-9y93net8.js";
122
+ } from "./chunk-3vag4n3q.js";
123
123
 
124
124
  // src/panel/die.ts
125
125
  var ROUND = 16 * Math.sqrt(5), DIE = { half: 64 + ROUND, round: ROUND, pip: 12 }, PIPS = {
@@ -2666,7 +2666,7 @@ function readPanelTokens(panel, version = -1) {
2666
2666
  if (hit && hit.version === version)
2667
2667
  return hit.tokens;
2668
2668
  }
2669
- let style = getComputedStyle(panel), light = isLightStyle(style), theme = gpuDefaults(themeOf(panel), light), bg = parseCssColor(token(style, "bg"), theme.bg), isLight = light || bg[0] * 0.2126 + bg[1] * 0.7152 + bg[2] * 0.0722 > 0.55, surface = parseCssColor(token(style, "fill"), theme.fill), surfaceHi = parseCssColor(token(style, "fill-selected"), theme["fill-selected"]), line = parseCssColor(token(style, "border"), theme.border), lineHi = parseCssColor(token(style, "border-strong"), theme["border-strong"]), text = parseCssColor(token(style, "fg"), theme.fg), label = parseCssColor(token(style, "fg-2"), theme["fg-2"]), dim = parseCssColor(token(style, "fg-3"), theme["fg-3"]), accent = parseCssColor(token(style, "accent"), theme.accent), modified = parseCssColor(token(style, "modified"), theme.modified), segBg = parseCssColor(token(style, "segmented-track"), theme["segmented-track"]), segActive = parseCssColor(token(style, "segmented-active"), theme["segmented-active"]), toggleTrack = parseCssColor(token(style, "toggle-track"), theme["toggle-track"]), fillBase = style.getPropertyValue("--k-fill-base").trim() ? parseCssColor(token(style, "fill-base"), surfaceHi) : surfaceHi, toggleThumb = parseCssColor(token(style, "toggle-thumb"), theme["toggle-thumb"]), [duration, bounce] = style.getPropertyValue("--k-spring").trim().split(/\s+/).map(Number), fill = Number.isFinite(duration) && Number.isFinite(bounce) ? springMotion2({ duration, bounce }) : MOTION.default, fontFamily = style.fontFamily || style.getPropertyValue("--_k-font").trim() || 'system-ui, -apple-system, "SF Pro Text", sans-serif', monoFamily = style.getPropertyValue("--_k-mono").trim() || '"SF Mono", SFMono-Regular, ui-monospace, Menlo, monospace', unit = numProp(style, "unit", 4), pad = numProp(style, "pad", 10), inset = numProp(style, "inset", pad), row = numProp(style, "row", 32), gap = numProp(style, "gap", 4), trayPad = numProp(style, "tray-pad", unit * 0.5), radius = numProp(style, "radius", 8), panelRadius = numProp(style, "radius-panel", radius + unit), radiusMd = Math.max(0, radius - trayPad), radiusSm = Math.max(0, radius - trayPad * 2), fontSize = parseFloat(style.fontSize) || numProp(style, "font-size", 12), roundRaw = style.getPropertyValue("--_k-round").trim(), round = /^-?[\d.]+(px)?$/.test(roundRaw) ? Math.max(0, parseFloat(roundRaw)) : radius > 0 ? 1e4 : 0, tokens = {
2669
+ let style = getComputedStyle(panel), light = isLightStyle(style), theme = gpuDefaults(themeOf(panel), light), bg = parseCssColor(token(style, "bg"), theme.bg), isLight = light || bg[0] * 0.2126 + bg[1] * 0.7152 + bg[2] * 0.0722 > 0.55, surface = parseCssColor(token(style, "fill"), theme.fill), surfaceHi = parseCssColor(token(style, "fill-selected"), theme["fill-selected"]), line = parseCssColor(token(style, "border"), theme.border), lineHi = parseCssColor(token(style, "border-strong"), theme["border-strong"]), text = parseCssColor(token(style, "fg"), theme.fg), label = parseCssColor(token(style, "fg-2"), theme["fg-2"]), dim = parseCssColor(token(style, "fg-3"), theme["fg-3"]), accent = parseCssColor(token(style, "accent"), theme.accent), modified = parseCssColor(token(style, "modified"), theme.modified), segBg = parseCssColor(token(style, "segmented-track"), theme["segmented-track"]), segActive = parseCssColor(token(style, "segmented-active"), theme["segmented-active"]), toggleTrack = parseCssColor(token(style, "toggle-track"), theme["toggle-track"]), fillBase = style.getPropertyValue("--k-fill-base").trim() ? parseCssColor(token(style, "fill-base"), surfaceHi) : surfaceHi, toggleThumb = parseCssColor(token(style, "toggle-thumb"), theme["toggle-thumb"]), [duration, bounce] = style.getPropertyValue("--k-spring").trim().split(/\s+/).map(Number), fill = Number.isFinite(duration) && Number.isFinite(bounce) ? springMotion2({ duration, bounce }) : MOTION.default, fontFamily = style.fontFamily || style.getPropertyValue("--_k-font").trim() || 'system-ui, -apple-system, "SF Pro Text", sans-serif', monoFamily = style.getPropertyValue("--_k-mono").trim() || '"SF Mono", SFMono-Regular, ui-monospace, Menlo, monospace', unit = numProp(style, "unit", 4), pad = numProp(style, "pad", 10), inset = numProp(style, "inset", pad), row = numProp(style, "row", 32), gap = numProp(style, "gap", 4), trayPad = numProp(style, "tray-pad", unit * 0.5), radius = numProp(style, "radius", 8), panelRadius = numProp(style, "radius-panel", radius + unit), frameRadius = numProp(style, "radius-frame", radius > 0 ? radius + pad + 1 : 0), radiusMd = Math.max(0, radius - trayPad), radiusSm = Math.max(0, radius - trayPad * 2), fontSize = parseFloat(style.fontSize) || numProp(style, "font-size", 12), roundRaw = style.getPropertyValue("--_k-round").trim(), round = /^-?[\d.]+(px)?$/.test(roundRaw) ? Math.max(0, parseFloat(roundRaw)) : radius > 0 ? 1e4 : 0, tokens = {
2670
2670
  bg,
2671
2671
  surface,
2672
2672
  surfaceHi,
@@ -2717,6 +2717,7 @@ function readPanelTokens(panel, version = -1) {
2717
2717
  trayPad,
2718
2718
  radius,
2719
2719
  panelRadius,
2720
+ frameRadius,
2720
2721
  radiusMd,
2721
2722
  radiusSm,
2722
2723
  fontSize,
@@ -5902,7 +5903,7 @@ class GpuOverlayManager {
5902
5903
  this.curAlpha = 1;
5903
5904
  if (revealing)
5904
5905
  anyMoving = !0;
5905
- let blurMul = reducedMotion ? 0 : tokens.blurStrength, outerClip = panel.inline ? getEffectiveClipRect(panel.root, win, this.styleVersion) : [0, 0, win.innerWidth, win.innerHeight], morph = panel.morph?.(this.frameNow), shown = morph ? clamp(morph.progress, 0, 1) : 1, card = morph ? new DOMRect(morph.x, morph.y, morph.w, morph.h) : pRect, cardRadius = morph ? morph.radius : tokens.panelRadius;
5906
+ let blurMul = reducedMotion ? 0 : tokens.blurStrength, outerClip = panel.inline ? getEffectiveClipRect(panel.root, win, this.styleVersion) : [0, 0, win.innerWidth, win.innerHeight], morph = panel.morph?.(this.frameNow), shown = morph ? clamp(morph.progress, 0, 1) : 1, card = morph ? new DOMRect(morph.x, morph.y, morph.w, morph.h) : pRect, cardRadius = morph ? morph.radius : tokens.frameRadius;
5906
5907
  if (morph)
5907
5908
  anyMoving = !0;
5908
5909
  let masked = morph !== void 0 && this.maskCount < MAX_MASKS, panelClip = intersectClip(outerClip, [pRect.left, pRect.top, pRect.right, pRect.bottom]);
@@ -10689,7 +10690,6 @@ var compareCss = `
10689
10690
  :where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_ku-on-media) 16%,transparent);color:var(--_ku-on-media);transition-duration:calc(var(--_k-hover-in) * 1ms)}
10690
10691
  .knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);pointer-events:none}
10691
10692
  .knobs-cmp-note:empty{display:none}
10692
- .knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
10693
10693
  .knobs-cmp-timeline-host{flex:none;min-height:0}
10694
10694
  /* The frame strip: Before over After per column, a difference bar under them, a dot where it peaks. */
10695
10695
  .knobs-ui.knobs-cmp-strip{--_ku-cmp-diff:var(--k-compare-diff,var(--_k-ok));position:relative;width:100%;height:100%;min-height:40px;overflow:hidden;cursor:col-resize;touch-action:none;
@@ -10709,6 +10709,9 @@ var playerCss = `
10709
10709
  .knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
10710
10710
  opacity:0;filter:blur(4px);transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
10711
10711
  .knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}
10712
+ /* Mid-scrub, the cached frame under the hand, over the video until it lands. */
10713
+ .knobs-player-preview{position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;pointer-events:none}
10714
+ .knobs-player-preview[hidden]{display:none}
10712
10715
  .knobs-player-line{flex:none;min-height:0}
10713
10716
  /* The timeline's own stylesheet is look.ts's; what the player's row and every timeline share
10714
10717
  about the hand is set here, after it. No hand cursor anywhere (the arrow, or a resize where it
@@ -11425,8 +11428,51 @@ function place2(o) {
11425
11428
  }
11426
11429
  var clamp2 = (v, lo, hi) => hi < lo ? lo : Math.max(lo, Math.min(hi, v));
11427
11430
 
11431
+ // src/ui/tooltip-model.ts
11432
+ function tipSchedule(clock, view) {
11433
+ let shown = null, pending = null, timer, hiddenAt = -1 / 0, cancel = () => {
11434
+ if (pending)
11435
+ clock.clear(timer);
11436
+ pending = null;
11437
+ }, hide = (now = !1) => {
11438
+ if (cancel(), !shown)
11439
+ return;
11440
+ shown = null, hiddenAt = clock.now(), view.hide(now);
11441
+ };
11442
+ return {
11443
+ get shown() {
11444
+ return shown;
11445
+ },
11446
+ get pending() {
11447
+ return pending;
11448
+ },
11449
+ want(tip, options = {}) {
11450
+ if (tip.el === shown?.el || tip.el === pending?.el)
11451
+ return;
11452
+ cancel();
11453
+ let handoff = shown !== null || clock.now() - hiddenAt < 400;
11454
+ if (handoff || options.now) {
11455
+ shown = tip, view.show(tip, handoff);
11456
+ return;
11457
+ }
11458
+ pending = tip, timer = clock.set(() => {
11459
+ if (pending = null, !view.connected(tip))
11460
+ return;
11461
+ shown = tip, view.show(tip, !1);
11462
+ }, options.delay ?? 500);
11463
+ },
11464
+ hide,
11465
+ leave(within) {
11466
+ if (pending && within(pending))
11467
+ cancel();
11468
+ if (shown && within(shown))
11469
+ hide();
11470
+ }
11471
+ };
11472
+ }
11473
+
11428
11474
  // src/ui/tooltip.ts
11429
- var tips = /* @__PURE__ */ new WeakMap, HANDOFF_MS = 400, controllers = /* @__PURE__ */ new WeakMap;
11475
+ var tips = /* @__PURE__ */ new WeakMap, controllers = /* @__PURE__ */ new WeakMap;
11430
11476
  function tipOf(start) {
11431
11477
  let node = start instanceof Element ? start : null;
11432
11478
  while (node) {
@@ -11452,7 +11498,7 @@ function controllerOf(doc) {
11452
11498
  let existing = controllers.get(doc);
11453
11499
  if (existing)
11454
11500
  return existing;
11455
- let win = doc.defaultView, layer, motion, text, keys, target = null, pending = null, timer = 0, hiddenAt = -1 / 0, quiet = null, keyboard = !1, build = (look) => {
11501
+ let win = doc.defaultView, layer, motion, text, keys, target = null, quiet = null, keyboard = !1, build = (look) => {
11456
11502
  layer = layerElement(doc, "knobs-layer knobs-tooltip", look), layer.id = `knobs-tooltip-${Math.random().toString(36).slice(2, 8)}`, layer.setAttribute("role", "tooltip"), text = doc.createElement("span"), text.className = "knobs-tooltip-text", keys = doc.createElement("span"), keys.className = "knobs-tooltip-keys", layer.append(text, keys), motion = presence(layer, { scale: 0.96, blur: 3, duration: 0.24 });
11457
11503
  }, show = (el, tip, instant) => {
11458
11504
  if (!layer?.isConnected)
@@ -11474,30 +11520,28 @@ function controllerOf(doc) {
11474
11520
  motion.snap(1);
11475
11521
  else
11476
11522
  motion.show();
11477
- }, hide = (now = !1) => {
11478
- if (win.clearTimeout(timer), pending = null, !target)
11479
- return;
11480
- if (target.removeAttribute("aria-describedby"), target = null, hiddenAt = performance.now(), now)
11481
- motion?.snap(0);
11482
- else
11483
- motion?.hide();
11484
- }, want = (found) => {
11523
+ }, schedule = tipSchedule({
11524
+ set: (run, ms) => win.setTimeout(run, ms),
11525
+ clear: (handle) => win.clearTimeout(handle),
11526
+ now: () => performance.now()
11527
+ }, {
11528
+ show: ({ el, tip }, snap) => show(el, tip, snap),
11529
+ hide: (now) => {
11530
+ if (target?.removeAttribute("aria-describedby"), target = null, now)
11531
+ motion?.snap(0);
11532
+ else
11533
+ motion?.hide();
11534
+ },
11535
+ connected: ({ el }) => el.isConnected
11536
+ }), hide = (now = !1) => schedule.hide(now), want = (found, now = !1) => {
11485
11537
  if (!found)
11486
11538
  return hide();
11487
- if (found.el === target || found.el === pending?.el)
11488
- return;
11489
11539
  if (found.el === quiet)
11490
11540
  return;
11491
- if (win.clearTimeout(timer), target !== null || performance.now() - hiddenAt < HANDOFF_MS) {
11492
- pending = null, show(found.el, found.tip, !0);
11493
- return;
11494
- }
11495
- if (target)
11496
- hide();
11497
- pending = found, timer = win.setTimeout(() => {
11498
- if (pending = null, found.el.isConnected)
11499
- show(found.el, found.tip, !1);
11500
- }, found.tip.opts.delay ?? 500);
11541
+ schedule.want(found, {
11542
+ now,
11543
+ ...found.tip.opts.delay !== void 0 ? { delay: found.tip.opts.delay } : {}
11544
+ });
11501
11545
  };
11502
11546
  doc.addEventListener("pointerover", (e) => {
11503
11547
  if (e.pointerType === "touch")
@@ -11524,10 +11568,9 @@ function controllerOf(doc) {
11524
11568
  }, !0), doc.addEventListener("focusin", (e) => {
11525
11569
  let found = tipOf(e.target);
11526
11570
  if (found && keyboard && e.target.matches(":focus-visible"))
11527
- want(found);
11571
+ want(found, !0);
11528
11572
  }), doc.addEventListener("focusout", (e) => {
11529
- if (target && target.contains(e.target))
11530
- hide();
11573
+ schedule.leave(({ el }) => el.contains(e.target));
11531
11574
  }), doc.addEventListener("scroll", () => target && hide(!0), !0);
11532
11575
  let c = { hide };
11533
11576
  return controllers.set(doc, c), c;
@@ -12458,6 +12501,13 @@ function toolbarRow(c, press, keyOf = toolbarShortcut2) {
12458
12501
  };
12459
12502
  }
12460
12503
 
12504
+ // src/panel/description-ids.ts
12505
+ var panels = 0;
12506
+ function descriptionIds(storeId) {
12507
+ let prefix = `knobs-desc-${++panels}-${storeId.replace(/[^\w-]/g, "-")}`, n = 0;
12508
+ return () => `${prefix}-${++n}`;
12509
+ }
12510
+
12461
12511
  // src/panel/search.ts
12462
12512
  function createSearch(doc, options) {
12463
12513
  let element = doc.createElement("div");
@@ -14688,7 +14738,7 @@ function morphFromChip(chip, root, wake, restored) {
14688
14738
  }, ensureShape = () => {
14689
14739
  if (shape)
14690
14740
  return shape;
14691
- let cs = getComputedStyle(root), r = parseFloat(cs.getPropertyValue("--kv-radius-panel"));
14741
+ let cs = getComputedStyle(root), r = parseFloat(cs.getPropertyValue("--kv-radius-frame"));
14692
14742
  if (Number.isFinite(r))
14693
14743
  panelRadius = r;
14694
14744
  shape = doc.createElement("div"), shape.className = "knobs-root knobs-morph", shape.setAttribute("aria-hidden", "true");
@@ -14818,6 +14868,7 @@ function foldMemory(id, storage) {
14818
14868
  let next = {
14819
14869
  sections: { ...data.sections },
14820
14870
  groups: { ...data.groups },
14871
+ ...data.subsections ? { subsections: { ...data.subsections } } : {},
14821
14872
  [kind]: { ...data[kind], [at]: folded }
14822
14873
  };
14823
14874
  try {
@@ -14828,14 +14879,27 @@ function foldMemory(id, storage) {
14828
14879
  }
14829
14880
 
14830
14881
  // src/panel/position.ts
14831
- function positionPanel(panel, header, anchor, id, storage) {
14832
- let doc = panel.ownerDocument, win = doc.defaultView, bounds = anchor.getBoundingClientRect(), w = panel.offsetWidth || 288, clampX = (v) => Math.max(8, Math.min(v, win.innerWidth - w - 8)), x = bounds.right + w <= win.innerWidth - 8 ? bounds.right + 8 : clampX(bounds.left - w - 8), y = Math.max(8, bounds.top), key = storageKey(id, "position"), hasSaved = !1;
14882
+ function savedPosition(left, top, width, view) {
14883
+ let right = left + width / 2 > view / 2;
14884
+ return right ? { x: view - left - width, y: top, right } : { x: left, y: top };
14885
+ }
14886
+ var leftOf = (saved, width, view) => saved.right ? view - width - saved.x : saved.x;
14887
+ function readPosition(raw) {
14833
14888
  try {
14834
- let saved = JSON.parse(storage()?.getItem(key) ?? "null");
14889
+ let saved = JSON.parse(raw ?? "null");
14835
14890
  if (saved && Number.isFinite(saved.x) && Number.isFinite(saved.y))
14836
- x = saved.x, y = saved.y, hasSaved = !0;
14891
+ return { x: saved.x, y: saved.y, ...saved.right === !0 ? { right: !0 } : {} };
14892
+ } catch {}
14893
+ return;
14894
+ }
14895
+ function positionPanel(panel, header, anchor, id, storage) {
14896
+ let doc = panel.ownerDocument, win = doc.defaultView, bounds = anchor.getBoundingClientRect(), w = panel.offsetWidth || 288, clampX = (v) => Math.max(8, Math.min(v, win.innerWidth - w - 8)), x = bounds.right + w <= win.innerWidth - 8 ? bounds.right + 8 : clampX(bounds.left - w - 8), y = Math.max(8, bounds.top), key = storageKey(id, "position"), saved;
14897
+ try {
14898
+ saved = readPosition(storage()?.getItem(key));
14837
14899
  } catch {}
14838
- if (!hasSaved) {
14900
+ if (saved)
14901
+ x = leftOf(saved, w, win.innerWidth), y = saved.y;
14902
+ else {
14839
14903
  let others = [...doc.querySelectorAll(".knobs-panel:not(.knobs-inline)")].filter((el) => el !== panel).map((el) => el.getBoundingClientRect());
14840
14904
  if (others.length > 0) {
14841
14905
  let overlaps = (cx, cy) => others.some((r) => Math.abs(cx - r.left) < w - 24 && Math.abs(cy - r.top) < 120);
@@ -14855,6 +14919,8 @@ function positionPanel(panel, header, anchor, id, storage) {
14855
14919
  }
14856
14920
  }
14857
14921
  let shown = { x, y }, constrain = () => {
14922
+ if (saved && !drag)
14923
+ x = leftOf(saved, panel.offsetWidth, win.innerWidth);
14858
14924
  shown = {
14859
14925
  x: Math.max(8, Math.min(x, win.innerWidth - panel.offsetWidth - 8)),
14860
14926
  y: Math.max(8, Math.min(y, win.innerHeight - panel.offsetHeight - 8))
@@ -14863,17 +14929,27 @@ function positionPanel(panel, header, anchor, id, storage) {
14863
14929
  header.onpointerdown = (event) => {
14864
14930
  if (event.button !== 0 || event.target.closest("button,select,input"))
14865
14931
  return;
14866
- event.preventDefault(), drag = { id: event.pointerId, x: event.clientX, y: event.clientY, left: shown.x, top: shown.y }, header.setPointerCapture(event.pointerId);
14932
+ event.preventDefault(), drag = { id: event.pointerId, x: event.clientX, y: event.clientY, left: shown.x, top: shown.y };
14867
14933
  }, header.onpointermove = (event) => {
14868
14934
  if (!drag || event.pointerId !== drag.id)
14869
14935
  return;
14936
+ if (!("dragging" in panel.dataset)) {
14937
+ if (!(event.buttons & 1))
14938
+ return void (drag = void 0);
14939
+ if (Math.hypot(event.clientX - drag.x, event.clientY - drag.y) <= 2)
14940
+ return;
14941
+ panel.dataset.dragging = "", header.setPointerCapture(event.pointerId);
14942
+ }
14870
14943
  x = drag.left + event.clientX - drag.x, y = drag.top + event.clientY - drag.y, constrain();
14944
+ }, header.onpointerup = header.onpointercancel = () => {
14945
+ if (drag && !("dragging" in panel.dataset))
14946
+ drag = void 0;
14871
14947
  }, header.onlostpointercapture = () => {
14872
14948
  if (!drag)
14873
14949
  return;
14874
- drag = void 0, { x, y } = shown;
14950
+ drag = void 0, delete panel.dataset.dragging, { x, y } = shown, saved = savedPosition(x, y, panel.offsetWidth, win.innerWidth);
14875
14951
  try {
14876
- storage()?.setItem(key, JSON.stringify({ x, y }));
14952
+ storage()?.setItem(key, JSON.stringify(saved));
14877
14953
  } catch {}
14878
14954
  };
14879
14955
  let observer = new ResizeObserver(constrain);
@@ -14987,7 +15063,7 @@ function createPresence(root, options) {
14987
15063
  if (!root.dataset.presence || root.hidden)
14988
15064
  return;
14989
15065
  let look = presenceLook(spring.x, inline, reduced), r = root.getBoundingClientRect();
14990
- radius ??= parseFloat(win.getComputedStyle(root).getPropertyValue("--kv-radius-panel")) || 12;
15066
+ radius ??= parseFloat(win.getComputedStyle(root).getPropertyValue("--kv-radius-frame")) || 12;
14991
15067
  let s = look.scale, ox = r.left + r.width / 2, oy = inline ? r.top : r.top + r.height / 2;
14992
15068
  return {
14993
15069
  x: ox + (r.left - ox) * s,
@@ -15459,8 +15535,10 @@ function buildPanel(controls, host, options) {
15459
15535
  if (page) {
15460
15536
  if (remember)
15461
15537
  foldsKept?.set("groups", group, next);
15462
- } else if (sessionKept)
15463
- writeSession(controls.id, { groups: { ...readSession(controls.id).groups, [group]: next } });
15538
+ } else if (sessionKept) {
15539
+ if (writeSession(controls.id, { groups: { ...readSession(controls.id).groups, [group]: next } }), remember)
15540
+ panelFolds?.set("groups", group, next);
15541
+ }
15464
15542
  refreshFolds(), positioned?.constrain(), wakeGpu();
15465
15543
  }
15466
15544
  function foldTile(card, shut, animate) {
@@ -15488,9 +15566,7 @@ function buildPanel(controls, host, options) {
15488
15566
  });
15489
15567
  gReset.className = "knobs-group-reset", gReset.dataset.hint = `${title} reset`, bloom(gReset, !1), groupResetButtons.set(groupName, gReset);
15490
15568
  let gDie = createHeaderDie(doc, () => wakeGpu(), GROUP_DIE), gRandom = iconButton("random", `Randomize ${title}`, "g Alt+g", () => {});
15491
- gRandom.className = "knobs-group-die", gRandom.dataset.hint = `${title} random`, gRandom.querySelector(".knobs-icon")?.replaceWith(gDie.node), gRandom.onclick = (event) => run(() => rerollGroup(groupName, event.altKey));
15492
- let rollable = Object.values(controls.schema).some((f) => f.group === groupName && hasValue2(f));
15493
- if (!bound("random") && !bound("mutate") || !rollable)
15569
+ if (gRandom.className = "knobs-group-die", gRandom.dataset.hint = `${title} random`, gRandom.querySelector(".knobs-icon")?.replaceWith(gDie.node), gRandom.onclick = (event) => run(() => rerollGroup(groupName, event.altKey)), !bound("random") && !bound("mutate"))
15494
15570
  gRandom.hidden = !0;
15495
15571
  groupDice.set(groupName, { button: gRandom, die: gDie });
15496
15572
  let presets = page ? (controls.groupPresets[groupName] ?? []).slice(0, 9) : [], ui, gPeek = el("span");
@@ -15560,7 +15636,7 @@ function buildPanel(controls, host, options) {
15560
15636
  if (summary = el("span"), summary.className = "knobs-group-summary", page)
15561
15637
  gRight.append(summary);
15562
15638
  groupSummaries.set(groupName, summary), caret = caretSvg(), gRight.append(caret), gHead.dataset.foldable = "", gHead.tabIndex = -1, gHead.setAttribute("role", "button"), gHead.setAttribute("aria-label", title);
15563
- let folded = (page ? foldsKept?.get("groups", groupName) : session.groups?.[groupName]) ?? foldAt;
15639
+ let folded = (page ? foldsKept?.get("groups", groupName) : session.groups?.[groupName] ?? panelFolds?.get("groups", groupName)) ?? foldAt;
15564
15640
  if (folds.set(groupName, folded), gHead.setAttribute("aria-expanded", String(!folded)), !page)
15565
15641
  gHead.toggleAttribute("data-folded", folded);
15566
15642
  gHead.addEventListener("click", (event) => {
@@ -15616,7 +15692,7 @@ function buildPanel(controls, host, options) {
15616
15692
  if (!preset)
15617
15693
  return;
15618
15694
  cue(root, coalesce ? "select" : "preset"), controls.groupPreset(group, index, { coalesce }), peek(group, preset.name, 1100);
15619
- }, remember = options.session ?? controls.storage !== null, sessionKept = remember, session = remember ? readSession(controls.id) : {}, foldPort = page?.foldStorage !== void 0 ? page.foldStorage : controls.persist === "none" ? null : controls.storage, foldsKept = page && foldPort ? foldMemory(controls.id, () => foldPort) : null, tracks = /* @__PURE__ */ new Map, toolbars = /* @__PURE__ */ new Map, keyedActions = /* @__PURE__ */ new Map, actionKey = (key, field) => {
15695
+ }, remember = options.session ?? controls.storage !== null, sessionKept = remember, session = remember ? readSession(controls.id) : {}, foldPort = page?.foldStorage !== void 0 ? page.foldStorage : controls.persist === "none" ? null : controls.storage, foldsKept = page && foldPort ? foldMemory(controls.id, () => foldPort) : null, panelFolds = !page && remember && foldPort ? foldMemory(controls.id, () => foldPort) : null, tracks = /* @__PURE__ */ new Map, toolbars = /* @__PURE__ */ new Map, keyedActions = /* @__PURE__ */ new Map, actionKey = (key, field) => {
15620
15696
  let combo = controls.shortcut(key);
15621
15697
  return combo ? { key: combo, scope: actionShortcut2(field)?.scope ?? "panel" } : void 0;
15622
15698
  }, readouts = /* @__PURE__ */ new Map, readoutPort = controls[READOUT], planRows = (schema) => {
@@ -15645,7 +15721,8 @@ function buildPanel(controls, host, options) {
15645
15721
  label,
15646
15722
  ...duration.group !== void 0 ? { group: duration.group } : {},
15647
15723
  ...duration.section !== void 0 ? { section: duration.section } : {},
15648
- ...duration.shortcut !== void 0 ? { shortcut: duration.shortcut } : {}
15724
+ ...duration.shortcut !== void 0 ? { shortcut: duration.shortcut } : {},
15725
+ ...duration.description !== void 0 ? { description: duration.description } : {}
15649
15726
  },
15650
15727
  parts: { duration, bounce },
15651
15728
  group: duration.group,
@@ -16075,7 +16152,33 @@ function buildPanel(controls, host, options) {
16075
16152
  return;
16076
16153
  slot.replaceChildren(...node ? [node] : []);
16077
16154
  }
16078
- let fieldFlag = (field, name) => {
16155
+ let descriptions = el("div");
16156
+ descriptions.hidden = !0, root.append(descriptions);
16157
+ let described = /* @__PURE__ */ new Map, descriptionId = descriptionIds(controls.id);
16158
+ function describe(row) {
16159
+ let text = row.field.description?.trim() ?? "", had = described.get(row);
16160
+ if ((had?.text ?? "") === text)
16161
+ return;
16162
+ let inputs = row.choice ? [row.choice.seg, row.choice.menu] : [row.input];
16163
+ if (had) {
16164
+ had.untip(), had.node.remove(), described.delete(row);
16165
+ for (let input of inputs)
16166
+ input.removeAttribute("aria-describedby");
16167
+ }
16168
+ if (!text)
16169
+ return;
16170
+ let node = el("span", text);
16171
+ node.id = descriptionId(), descriptions.append(node);
16172
+ for (let input of inputs)
16173
+ input.setAttribute("aria-describedby", node.id);
16174
+ described.set(row, { text, node, untip: tooltip(row.row, text) });
16175
+ }
16176
+ let undescribe = (row) => {
16177
+ let had = described.get(row);
16178
+ if (!had)
16179
+ return;
16180
+ had.untip(), had.node.remove(), described.delete(row);
16181
+ }, fieldFlag = (field, name) => {
16079
16182
  let v = field[name];
16080
16183
  return typeof v === "function" ? Boolean(v(controls.get())) : Boolean(v);
16081
16184
  };
@@ -16106,7 +16209,7 @@ function buildPanel(controls, host, options) {
16106
16209
  announceSections();
16107
16210
  }, delay), wakeGpu();
16108
16211
  }, dropGpu = (row) => {
16109
- row.dispose?.(), row.slider?.dispose(), row.editor?.dispose();
16212
+ undescribe(row), row.dispose?.(), row.slider?.dispose(), row.editor?.dispose();
16110
16213
  for (let g of row.gpu) {
16111
16214
  let at = gpuRows.indexOf(g);
16112
16215
  if (at >= 0)
@@ -16147,11 +16250,11 @@ function buildPanel(controls, host, options) {
16147
16250
  for (let spec of specs) {
16148
16251
  let existing = kept.get(spec.id);
16149
16252
  if (existing && existing.shape === shapeOf(spec)) {
16150
- kept.delete(spec.id), existing.retune(spec), next.push(existing);
16253
+ kept.delete(spec.id), existing.retune(spec), describe(existing), next.push(existing);
16151
16254
  continue;
16152
16255
  }
16153
16256
  let row = buildRow(spec);
16154
- decorate(row), gpuRows.push(...row.gpu), entering.add(row), next.push(row);
16257
+ decorate(row), describe(row), gpuRows.push(...row.gpu), entering.add(row), next.push(row);
16155
16258
  }
16156
16259
  let leaving = [...kept.values()];
16157
16260
  rows = next;
@@ -16310,7 +16413,7 @@ function buildPanel(controls, host, options) {
16310
16413
  label.className = "knobs-subhead-label";
16311
16414
  let caret = caretSvg();
16312
16415
  if (node.append(label, caret), !subFolds.has(id))
16313
- subFolds.set(id, session.subsections?.[id] ?? options.subsectionFolded?.[name] ?? !1);
16416
+ subFolds.set(id, session.subsections?.[id] ?? panelFolds?.get("subsections", id) ?? options.subsectionFolded?.[name] ?? !1);
16314
16417
  node.setAttribute("aria-expanded", String(!subFolds.get(id))), node.addEventListener("click", (event) => {
16315
16418
  if (event.detail < 2)
16316
16419
  foldSub(id);
@@ -16341,7 +16444,7 @@ function buildPanel(controls, host, options) {
16341
16444
  if (applyVisibility("reveal"), remember)
16342
16445
  writeSession(controls.id, {
16343
16446
  subsections: { ...readSession(controls.id).subsections, [id]: next }
16344
- });
16447
+ }), panelFolds?.set("subsections", id, next);
16345
16448
  }
16346
16449
  let groupTab = (group) => {
16347
16450
  for (let f of Object.values(controls.schema))
@@ -16410,7 +16513,7 @@ function buildPanel(controls, host, options) {
16410
16513
  return selectTab(list[(at + direction + list.length) % list.length].id, !1), !0;
16411
16514
  }, searching = () => search?.filtering() ?? !1, rowMatches = (r) => {
16412
16515
  let query = search.query();
16413
- if (query && !matchesFilter(query, r.display, r.key, r.group, r.group && groupTitle(r.group), r.group && groupDescription(r.group), r.field.subsection))
16516
+ if (query && !matchesFilter(query, r.display, r.key, r.group, r.group && groupTitle(r.group), r.group && groupDescription(r.group), r.field.subsection, r.field.description))
16414
16517
  return !1;
16415
16518
  for (let chip of search.active())
16416
16519
  if (!r.keys.some((k) => chipTests.get(chip)?.(k)))
@@ -16509,7 +16612,10 @@ function buildPanel(controls, host, options) {
16509
16612
  lock.dataset.enter = "", lock.getBoundingClientRect();
16510
16613
  }
16511
16614
  return lock;
16512
- }, keysOf = (indices) => indices.flatMap((i) => rows[i]?.keys ?? []), groupKeys = (group) => rows.filter((r) => r.group === group).flatMap((r) => r.keys), rerollGroup = (group, nudge) => {
16615
+ }, keysOf = (indices) => indices.flatMap((i) => rows[i]?.keys ?? []), groupKeys = (group) => rows.filter((r) => r.group === group).flatMap((r) => r.keys), mayRoll = (key) => {
16616
+ let field = controls.schema[key];
16617
+ return Boolean(field && hasValue2(field) && field.random !== !1);
16618
+ }, rerollGroup = (group, nudge) => {
16513
16619
  let roller = groupDice.get(group)?.die;
16514
16620
  if (!roller)
16515
16621
  return;
@@ -16615,15 +16721,22 @@ function buildPanel(controls, host, options) {
16615
16721
  });
16616
16722
  apply.className = "knobs-apply-btn", apply.setAttribute("aria-label", "Import JSON"), apply.hidden = !0;
16617
16723
  let copy = async (value, inPlace = !0) => {
16618
- try {
16619
- if (await doc.defaultView.navigator.clipboard.writeText(value), inPlace && !copyButton.hidden && headerShown())
16724
+ let done = () => {
16725
+ if (inPlace && !copyButton.hidden && headerShown())
16620
16726
  copied();
16621
16727
  else
16622
16728
  say("Copied.");
16729
+ };
16730
+ try {
16731
+ await doc.defaultView.navigator.clipboard.writeText(value), done();
16623
16732
  } catch {
16624
- if (text.value = value, page)
16625
- togglePopover(!0);
16626
- reveal(text, !0), apply.hidden = !0, positioned?.constrain(), text.focus(), text.select(), say("Clipboard unavailable. Copy the selected text.", 6000);
16733
+ if (copyByCommand(doc, value))
16734
+ done();
16735
+ else {
16736
+ if (text.value = value, page)
16737
+ togglePopover(!0);
16738
+ reveal(text, !0), apply.hidden = !0, positioned?.constrain(), text.focus(), text.select(), say("Clipboard unavailable. Copy the selected text.", 6000);
16739
+ }
16627
16740
  }
16628
16741
  wakeGpu();
16629
16742
  }, gpuActionButtons = [], panelActions = allActions.filter((a) => !a.group), customActions = el("div");
@@ -16803,10 +16916,16 @@ function buildPanel(controls, host, options) {
16803
16916
  }
16804
16917
  if (randomButton.disabled !== (unlocked === 0))
16805
16918
  randomButton.disabled = unlocked === 0;
16919
+ let headerHidden = !((bound("random") || bound("mutate")) && shows("random")) || !rows.some((r) => r.keys.some(mayRoll));
16920
+ if (randomButton.hidden !== headerHidden)
16921
+ randomButton.hidden = headerHidden;
16806
16922
  for (let [group, { button }] of groupDice) {
16807
16923
  let off = !options.groupRandom && !groupKeys(group).some((k) => controls.canRoll(k));
16808
16924
  if (button.disabled !== off)
16809
16925
  button.disabled = off;
16926
+ let hidden = !bound("random") && !bound("mutate") || !options.groupRandom && !groupKeys(group).some(mayRoll);
16927
+ if (button.hidden !== hidden)
16928
+ button.hidden = hidden;
16810
16929
  }
16811
16930
  if (root.hasAttribute("data-comparing") !== controls.comparing)
16812
16931
  root.toggleAttribute("data-comparing", controls.comparing);
@@ -16959,7 +17078,7 @@ function buildPanel(controls, host, options) {
16959
17078
  filtered = query, root.toggleAttribute("data-filtering", Boolean(query)), layout.change(() => {
16960
17079
  layout.openFolds(Boolean(query));
16961
17080
  for (let r of rows) {
16962
- let show = !query || matchesFilter(query, r.display, r.key, r.group, r.group && groupTitle(r.group, r.section), r.group && groupDescription(r.group), r.section, r.section && sectionTitle(r.section), r.section && sectionDescription(r.section));
17081
+ let show = !query || matchesFilter(query, r.display, r.key, r.group, r.group && groupTitle(r.group, r.section), r.group && groupDescription(r.group), r.section, r.section && sectionTitle(r.section), r.section && sectionDescription(r.section), r.field.description);
16963
17082
  if (r.row.hidden === show)
16964
17083
  r.row.hidden = !show;
16965
17084
  }
@@ -17436,8 +17555,19 @@ function buildPanel(controls, host, options) {
17436
17555
  setTimeout(() => {
17437
17556
  pointerHeld = !1, queueScope();
17438
17557
  });
17558
+ }, onPressOutside = (event) => {
17559
+ let target = event.target;
17560
+ if (page || !target || root.contains(target))
17561
+ return;
17562
+ if (target.closest?.(".knobs-root,.knobs-layers"))
17563
+ return;
17564
+ setTimeout(() => {
17565
+ let active = doc.activeElement;
17566
+ if (active && root.contains(active))
17567
+ active.blur();
17568
+ });
17439
17569
  };
17440
- doc.addEventListener("pointerdown", onPress, !0), doc.addEventListener("pointerup", onRelease, !0), doc.addEventListener("pointercancel", onRelease, !0), root.addEventListener("keydown", () => {
17570
+ doc.addEventListener("pointerdown", onPress, !0), doc.addEventListener("pointerdown", onPressOutside, !0), doc.addEventListener("pointerup", onRelease, !0), doc.addEventListener("pointercancel", onRelease, !0), root.addEventListener("keydown", () => {
17441
17571
  pointerOut = !1;
17442
17572
  }, !0);
17443
17573
  let syncScope = () => {
@@ -18263,7 +18393,7 @@ function buildPanel(controls, host, options) {
18263
18393
  doc.removeEventListener(type, onPointerUp, !0);
18264
18394
  for (let type of ["pointerup", "pointercancel"])
18265
18395
  doc.removeEventListener(type, endScrub, !0);
18266
- doc.defaultView?.removeEventListener("blur", endScrub), endScrub(), unregisterHints?.(), hintCtrl.stop(), modeObserver.disconnect(), unregisterGpu?.(), lens.dispose(), win?.cancelAnimationFrame(lensFrame), doc.removeEventListener("pointerdown", onPress, !0), doc.removeEventListener("pointerup", onRelease, !0), doc.removeEventListener("pointercancel", onRelease, !0), crumbs?.dispose(), active?.dispose(), clearTimeout(escTimer), win?.removeEventListener("blur", queueScope), win?.removeEventListener("focus", queueScope), unsubscribe(), unlisten?.(), rowsObserver.disconnect(), gpuObserver.disconnect(), doc.defaultView?.cancelAnimationFrame(landTimer);
18396
+ doc.defaultView?.removeEventListener("blur", endScrub), endScrub(), unregisterHints?.(), hintCtrl.stop(), modeObserver.disconnect(), unregisterGpu?.(), lens.dispose(), win?.cancelAnimationFrame(lensFrame), doc.removeEventListener("pointerdown", onPress, !0), doc.removeEventListener("pointerdown", onPressOutside, !0), doc.removeEventListener("pointerup", onRelease, !0), doc.removeEventListener("pointercancel", onRelease, !0), crumbs?.dispose(), active?.dispose(), clearTimeout(escTimer), win?.removeEventListener("blur", queueScope), win?.removeEventListener("focus", queueScope), unsubscribe(), unlisten?.(), rowsObserver.disconnect(), gpuObserver.disconnect(), doc.defaultView?.cancelAnimationFrame(landTimer);
18267
18397
  for (let k of inHand)
18268
18398
  controls.hold(k, !1);
18269
18399
  inHand.clear(), positioned?.dispose(), die.dispose();
@@ -18414,6 +18544,17 @@ function buildPanel(controls, host, options) {
18414
18544
  }
18415
18545
  return pageApi;
18416
18546
  }
18547
+ function copyByCommand(doc, value) {
18548
+ let was = doc.activeElement, area = doc.createElement("textarea");
18549
+ area.value = value, area.setAttribute("readonly", ""), area.style.cssText = "position:fixed;top:0;left:0;opacity:0;pointer-events:none", doc.body.append(area), area.select();
18550
+ let ok = !1;
18551
+ try {
18552
+ ok = doc.execCommand("copy");
18553
+ } catch {
18554
+ ok = !1;
18555
+ }
18556
+ return area.remove(), was?.focus({ preventScroll: !0 }), ok;
18557
+ }
18417
18558
  function createPanel(controls, anchor, close, options) {
18418
18559
  return buildPanel(controls, anchor.ownerDocument.body, {
18419
18560
  ...options,
@@ -6,7 +6,7 @@ import {
6
6
  writeSession,
7
7
  injectStyles,
8
8
  toggleLayer
9
- } from "./chunk-9y93net8.js";
9
+ } from "./chunk-3vag4n3q.js";
10
10
 
11
11
  // src/panel/attach.ts
12
12
  function attach(controls, element, options = {}) {
@@ -69,26 +69,26 @@ function attach(controls, element, options = {}) {
69
69
  update();
70
70
  }, open = async (restored = !1) => {
71
71
  if (panel) {
72
- unpress(), panel.focus();
72
+ unpress(), focusPanel(panel);
73
73
  return;
74
74
  }
75
75
  if (leaving?.reopen()) {
76
76
  if (unpress(), panel = leaving, leaving = void 0, remember)
77
77
  writeSession(controls.id, { open: !0 });
78
- chip.setAttribute("aria-expanded", "true"), update(), panel.focus();
78
+ chip.setAttribute("aria-expanded", "true"), update(), focusPanel(panel);
79
79
  return;
80
80
  }
81
81
  if (leaving = void 0, loading)
82
82
  return;
83
83
  loading = !0;
84
84
  try {
85
- let { createPanel } = await import("./chunk-e5hbs8fe.js");
85
+ let { createPanel } = await import("./chunk-5cv3g7ca.js");
86
86
  if (disposed || !controls.enabled || panel)
87
87
  return;
88
88
  if (panel = createPanel(controls, element, close, { restored, chip }), remember)
89
89
  writeSession(controls.id, { open: !0 });
90
90
  if (chip.setAttribute("aria-expanded", "true"), update(), !restored)
91
- panel.focus();
91
+ focusPanel(panel);
92
92
  } catch (cause) {
93
93
  chip.title = String(cause), renderFace("Retry knobs");
94
94
  } finally {
@@ -124,7 +124,7 @@ function attach(controls, element, options = {}) {
124
124
  element.removeAttribute("tabindex");
125
125
  };
126
126
  }
127
- var MODE_KEY = "knobs:mode", MODE_EVENT = "knobs-mode";
127
+ var focusPanel = (panel) => panel.element.focus({ preventScroll: !0 }), MODE_KEY = "knobs:mode", MODE_EVENT = "knobs-mode";
128
128
  function knobsMode(win) {
129
129
  try {
130
130
  return win.sessionStorage.getItem(MODE_KEY) === "on";
package/dist/dev/index.js CHANGED
@@ -77,7 +77,7 @@ import {
77
77
  browserStorage,
78
78
  urlFlag2,
79
79
  storageKey
80
- } from "./chunk-v2g91mrm.js";
80
+ } from "./chunk-2g0znytq.js";
81
81
  // src/core/random.ts
82
82
  var lattice = (min, max, step, r) => min + Math.floor(r * (Math.floor((max - min) / step + 0.000000001) + 1)) * step, between = (min, max, r) => min + (max - min) * r;
83
83
  function pick(field, random) {
@@ -945,11 +945,12 @@ function defineControls(id, input, options = {}) {
945
945
  ...setOpts?.base ? { base: !0 } : {}
946
946
  });
947
947
  }
948
- function step(to, source) {
949
- let before = load(), left = { values: before, base: activeBase() }, diff = changes(before, to.values);
948
+ function step(to, source, into) {
949
+ let before = load();
950
+ into.push({ values: before, base: activeBase() });
951
+ let diff = changes(before, to.values);
950
952
  if (base = validBase(to.base), markChanged(diff), commit(to.values), Object.keys(diff).length)
951
953
  options.onChange?.(diff, to.values, { source });
952
- return left;
953
954
  }
954
955
  function place(values, replace, land) {
955
956
  let previous = load(), applied = applyPatch(replace ? locked : previous, previous, values);
@@ -1285,7 +1286,7 @@ function defineControls(id, input, options = {}) {
1285
1286
  let previous = past.pop();
1286
1287
  if (!previous)
1287
1288
  return;
1288
- future.push(step(previous, "undo"));
1289
+ step(previous, "undo", future);
1289
1290
  },
1290
1291
  redo() {
1291
1292
  if (release(), hostHistory)
@@ -1293,7 +1294,7 @@ function defineControls(id, input, options = {}) {
1293
1294
  let next = future.pop();
1294
1295
  if (!next)
1295
1296
  return;
1296
- past.push(step(next, "redo"));
1297
+ step(next, "redo", past);
1297
1298
  },
1298
1299
  getServerSnapshot: () => locked,
1299
1300
  subscribe(cb) {
@@ -1526,11 +1527,11 @@ function defineControls(id, input, options = {}) {
1526
1527
  code: (format, values) => formatCode(values ?? get(), format),
1527
1528
  variants,
1528
1529
  attach(element, chip) {
1529
- let load = options.ship ? () => options.ship().then((m) => m.attach) : () => import("./chunk-exaya50m.js").then((m) => m.attach);
1530
+ let load = options.ship ? () => options.ship().then((m) => m.attach) : () => import("./chunk-xzm0zawh.js").then((m) => m.attach);
1530
1531
  return live(load, (attach) => attach(controls, element, chip), (detach) => detach());
1531
1532
  },
1532
1533
  mount(container, mountOptions) {
1533
- let load = options.ship ? () => options.ship().then((m) => m.mountPanel) : () => import("./chunk-e5hbs8fe.js").then((m) => m.mountPanel);
1534
+ let load = options.ship ? () => options.ship().then((m) => m.mountPanel) : () => import("./chunk-5cv3g7ca.js").then((m) => m.mountPanel);
1534
1535
  return live(load, (mountPanel) => {
1535
1536
  let panel = mountPanel(controls, container, mountOptions);
1536
1537
  return mountOptions?.onReady?.(panel), panel;