@kitlangton/knobs 0.4.0-next.15 → 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.
Files changed (41) hide show
  1. package/README.md +46 -14
  2. package/dist/dev/{chunk-v2g91mrm.js → chunk-2g0znytq.js} +1 -0
  3. package/dist/dev/{chunk-bejz4qcg.js → chunk-3vag4n3q.js} +47 -10
  4. package/dist/dev/{chunk-49hkdggk.js → chunk-5cv3g7ca.js} +395 -104
  5. package/dist/dev/{chunk-902v1jgd.js → chunk-xzm0zawh.js} +6 -6
  6. package/dist/dev/index.js +9 -8
  7. package/dist/index.js +7 -5
  8. package/dist/panel.js +549 -121
  9. package/dist/review-react.js +1341 -880
  10. package/dist/review-solid.js +1341 -880
  11. package/dist/review.js +1711 -1248
  12. package/dist/themes.js +8 -0
  13. package/dist/types/core/schema.d.ts +11 -2
  14. package/dist/types/core/store.d.ts +29 -1
  15. package/dist/types/index.d.ts +1 -1
  16. package/dist/types/panel/active.d.ts +53 -0
  17. package/dist/types/panel/description-ids.d.ts +6 -0
  18. package/dist/types/panel/folds.d.ts +7 -4
  19. package/dist/types/panel/gpu/color.d.ts +2 -0
  20. package/dist/types/panel/gpu/renderer.d.ts +8 -2
  21. package/dist/types/panel/index.d.ts +1 -0
  22. package/dist/types/panel/page-hints.d.ts +3 -2
  23. package/dist/types/panel/page.d.ts +2 -15
  24. package/dist/types/panel/position.d.ts +23 -0
  25. package/dist/types/panel/summary.d.ts +22 -0
  26. package/dist/types/tokens/schema.d.ts +9 -0
  27. package/dist/types/ui/compare-css.d.ts +1 -1
  28. package/dist/types/ui/compare-frames.d.ts +64 -2
  29. package/dist/types/ui/compare-model.d.ts +23 -0
  30. package/dist/types/ui/compare.d.ts +6 -4
  31. package/dist/types/ui/player-css.d.ts +1 -1
  32. package/dist/types/ui/player.d.ts +6 -0
  33. package/dist/types/ui/react.d.ts +1 -1
  34. package/dist/types/ui/solid.d.ts +1 -1
  35. package/dist/types/ui/tooltip-model.d.ts +35 -0
  36. package/dist/types/ui/tooltip.d.ts +1 -1
  37. package/dist/types/ui/walkthrough-model.d.ts +4 -2
  38. package/dist/ui-react.js +2015 -1507
  39. package/dist/ui-solid.js +2014 -1506
  40. package/dist/ui.js +2013 -1505
  41. package/package.json +1 -1
@@ -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-bejz4qcg.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,
@@ -4734,6 +4735,33 @@ class GpuOverlayManager {
4734
4735
  let clipped = width > 0 && el.scrollWidth > el.clientWidth;
4735
4736
  return this.clipCache.set(el, { key, text, width, clipped }), clipped;
4736
4737
  }
4738
+ lineCache = /* @__PURE__ */ new WeakMap;
4739
+ linesOf(el, width) {
4740
+ let text = el.textContent ?? "", hit = this.lineCache.get(el);
4741
+ if (hit && hit.text === text && hit.width === width)
4742
+ return hit.lines;
4743
+ let lines = [], node = el.firstChild;
4744
+ if (node?.nodeType === Node.TEXT_NODE) {
4745
+ let top = el.getBoundingClientRect().top, range = el.ownerDocument.createRange(), line, flush = () => {
4746
+ if (line)
4747
+ lines.push({
4748
+ text: text.slice(line.from, line.to),
4749
+ y: (line.top + line.bottom) / 2 - top
4750
+ });
4751
+ };
4752
+ for (let word of text.matchAll(/\S+/g)) {
4753
+ let from = word.index, to = from + word[0].length;
4754
+ range.setStart(node, from), range.setEnd(node, to);
4755
+ let r = range.getBoundingClientRect();
4756
+ if (line && Math.abs(r.top - line.top) < 1)
4757
+ line.to = to, line.bottom = Math.max(line.bottom, r.bottom);
4758
+ else
4759
+ flush(), line = { from, to, top: r.top, bottom: r.bottom };
4760
+ }
4761
+ flush();
4762
+ }
4763
+ return this.lineCache.set(el, { text, width, lines }), lines;
4764
+ }
4737
4765
  curReduced = !1;
4738
4766
  curLocks;
4739
4767
  curSelected = 0;
@@ -5875,7 +5903,7 @@ class GpuOverlayManager {
5875
5903
  this.curAlpha = 1;
5876
5904
  if (revealing)
5877
5905
  anyMoving = !0;
5878
- 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;
5879
5907
  if (morph)
5880
5908
  anyMoving = !0;
5881
5909
  let masked = morph !== void 0 && this.maskCount < MAX_MASKS, panelClip = intersectClip(outerClip, [pRect.left, pRect.top, pRect.right, pRect.bottom]);
@@ -6146,12 +6174,10 @@ class GpuOverlayManager {
6146
6174
  this.drawText(this.fit(g.labelEl.textContent ?? "", lr.width, px, tokens.titleWeight), lr.left, cy, withAlpha(tokens.text, 1 - peek), { px, weight: tokens.titleWeight, track: -0.01 });
6147
6175
  if (g.peek && peek > 0.005)
6148
6176
  this.drawText(this.fit(g.peek.textContent ?? "", lr.width, px, tokens.titleWeight), lr.left, cy, withAlpha(tokens.text, peek), { px, weight: tokens.titleWeight, track: -0.01 });
6149
- } else
6150
- this.drawText(tokens.groupUpper ? (g.labelEl.textContent ?? "").toUpperCase() : g.labelEl.textContent ?? "", lr.left, lr.top + lr.height * 0.5, tokens.groupColor, {
6151
- px: Math.max(9, tokens.groupSize),
6152
- weight: tokens.titleWeight,
6153
- track: tokens.groupTracking
6154
- });
6177
+ } else {
6178
+ let label = tokens.groupUpper ? (g.labelEl.textContent ?? "").toUpperCase() : g.labelEl.textContent ?? "", px = Math.max(9, tokens.groupSize);
6179
+ this.drawText(this.domClipped(g.labelEl, label, lr.width) ? this.fit(label, lr.width, px, tokens.titleWeight, void 0, tokens.groupTracking, !0) : label, lr.left, lr.top + lr.height * 0.5, tokens.groupColor, { px, weight: tokens.titleWeight, track: tokens.groupTracking });
6180
+ }
6155
6181
  if (!g.title && tokens.groupDivider[3] > 0 && g.element.previousElementSibling)
6156
6182
  this.pushPrim({
6157
6183
  x: gr.left,
@@ -6162,13 +6188,14 @@ class GpuOverlayManager {
6162
6188
  kind: 1
6163
6189
  });
6164
6190
  if (g.desc && !g.desc.hidden) {
6165
- let r = this.rectOf(g.desc), cs = getComputedStyle(g.desc), left = r.left + (parseFloat(cs.paddingLeft) || 0), w = r.width - (parseFloat(cs.paddingLeft) || 0) - (parseFloat(cs.paddingRight) || 0);
6166
- this.drawText(this.fit(g.desc.textContent ?? "", w + 0.5, tokens.textMeta, tokens.labelWeight), left, r.top + r.height * 0.5, tokens.dim, { px: tokens.textMeta, weight: tokens.labelWeight });
6191
+ let r = this.rectOf(g.desc), cs = getComputedStyle(g.desc), left = r.left + (parseFloat(cs.paddingLeft) || 0), w = r.width - (parseFloat(cs.paddingLeft) || 0) - (parseFloat(cs.paddingRight) || 0), lines = cs.whiteSpace.includes("nowrap") ? [{ text: g.desc.textContent ?? "", y: r.height * 0.5 }] : this.linesOf(g.desc, r.width);
6192
+ for (let line of lines)
6193
+ this.drawText(this.fit(line.text, w + 0.5, tokens.textMeta, tokens.labelWeight), left, r.top + line.y, tokens.dim, { px: tokens.textMeta, weight: tokens.labelWeight });
6167
6194
  }
6168
6195
  let fadeIn = (node) => node.parentElement?.closest("[data-fold-motion]") ? Number(getComputedStyle(node).opacity) || 0 : 1;
6169
6196
  if (g.summary && g.summary.getClientRects().length) {
6170
- let r = this.rectOf(g.summary), a = fadeIn(g.summary);
6171
- this.drawText(this.fit(g.summary.textContent ?? "", r.width + 0.5, tokens.textControl, 500), r.left, r.top + r.height * 0.5, withAlpha(tokens.dim, a), { px: tokens.textControl, weight: 500 });
6197
+ let r = this.rectOf(g.summary), a = fadeIn(g.summary), px = g.title ? tokens.textControl : tokens.textMeta;
6198
+ this.drawText(this.fit(g.summary.textContent ?? "", r.width + 0.5, px, 500), r.left, r.top + r.height * 0.5, withAlpha(tokens.dim, a), { px, weight: 500 });
6172
6199
  }
6173
6200
  let ctlShown = !g.controls || g.controls.getClientRects().length > 0, headAlpha = this.curAlpha;
6174
6201
  if (g.controls && ctlShown)
@@ -10663,7 +10690,6 @@ var compareCss = `
10663
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)}
10664
10691
  .knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);pointer-events:none}
10665
10692
  .knobs-cmp-note:empty{display:none}
10666
- .knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
10667
10693
  .knobs-cmp-timeline-host{flex:none;min-height:0}
10668
10694
  /* The frame strip: Before over After per column, a difference bar under them, a dot where it peaks. */
10669
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;
@@ -10683,6 +10709,9 @@ var playerCss = `
10683
10709
  .knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
10684
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)}
10685
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}
10686
10715
  .knobs-player-line{flex:none;min-height:0}
10687
10716
  /* The timeline's own stylesheet is look.ts's; what the player's row and every timeline share
10688
10717
  about the hand is set here, after it. No hand cursor anywhere (the arrow, or a resize where it
@@ -11399,8 +11428,51 @@ function place2(o) {
11399
11428
  }
11400
11429
  var clamp2 = (v, lo, hi) => hi < lo ? lo : Math.max(lo, Math.min(hi, v));
11401
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
+
11402
11474
  // src/ui/tooltip.ts
11403
- var tips = /* @__PURE__ */ new WeakMap, HANDOFF_MS = 400, controllers = /* @__PURE__ */ new WeakMap;
11475
+ var tips = /* @__PURE__ */ new WeakMap, controllers = /* @__PURE__ */ new WeakMap;
11404
11476
  function tipOf(start) {
11405
11477
  let node = start instanceof Element ? start : null;
11406
11478
  while (node) {
@@ -11426,7 +11498,7 @@ function controllerOf(doc) {
11426
11498
  let existing = controllers.get(doc);
11427
11499
  if (existing)
11428
11500
  return existing;
11429
- 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) => {
11430
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 });
11431
11503
  }, show = (el, tip, instant) => {
11432
11504
  if (!layer?.isConnected)
@@ -11448,30 +11520,28 @@ function controllerOf(doc) {
11448
11520
  motion.snap(1);
11449
11521
  else
11450
11522
  motion.show();
11451
- }, hide = (now = !1) => {
11452
- if (win.clearTimeout(timer), pending = null, !target)
11453
- return;
11454
- if (target.removeAttribute("aria-describedby"), target = null, hiddenAt = performance.now(), now)
11455
- motion?.snap(0);
11456
- else
11457
- motion?.hide();
11458
- }, 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) => {
11459
11537
  if (!found)
11460
11538
  return hide();
11461
- if (found.el === target || found.el === pending?.el)
11462
- return;
11463
11539
  if (found.el === quiet)
11464
11540
  return;
11465
- if (win.clearTimeout(timer), target !== null || performance.now() - hiddenAt < HANDOFF_MS) {
11466
- pending = null, show(found.el, found.tip, !0);
11467
- return;
11468
- }
11469
- if (target)
11470
- hide();
11471
- pending = found, timer = win.setTimeout(() => {
11472
- if (pending = null, found.el.isConnected)
11473
- show(found.el, found.tip, !1);
11474
- }, found.tip.opts.delay ?? 500);
11541
+ schedule.want(found, {
11542
+ now,
11543
+ ...found.tip.opts.delay !== void 0 ? { delay: found.tip.opts.delay } : {}
11544
+ });
11475
11545
  };
11476
11546
  doc.addEventListener("pointerover", (e) => {
11477
11547
  if (e.pointerType === "touch")
@@ -11498,10 +11568,9 @@ function controllerOf(doc) {
11498
11568
  }, !0), doc.addEventListener("focusin", (e) => {
11499
11569
  let found = tipOf(e.target);
11500
11570
  if (found && keyboard && e.target.matches(":focus-visible"))
11501
- want(found);
11571
+ want(found, !0);
11502
11572
  }), doc.addEventListener("focusout", (e) => {
11503
- if (target && target.contains(e.target))
11504
- hide();
11573
+ schedule.leave(({ el }) => el.contains(e.target));
11505
11574
  }), doc.addEventListener("scroll", () => target && hide(!0), !0);
11506
11575
  let c = { hide };
11507
11576
  return controllers.set(doc, c), c;
@@ -12432,6 +12501,13 @@ function toolbarRow(c, press, keyOf = toolbarShortcut2) {
12432
12501
  };
12433
12502
  }
12434
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
+
12435
12511
  // src/panel/search.ts
12436
12512
  function createSearch(doc, options) {
12437
12513
  let element = doc.createElement("div");
@@ -14439,6 +14515,79 @@ function createLens(o) {
14439
14515
  };
14440
14516
  }
14441
14517
 
14518
+ // src/panel/active.ts
14519
+ var same2 = (a, b) => a === b || a !== null && b !== null && a.group === b.group && a.key === b.key;
14520
+ function activeReporter(resolve, report, schedule) {
14521
+ let hover = null, focus = null, lead = "pointer", last = null, cancel, flush = () => {
14522
+ cancel = void 0;
14523
+ let [first, second] = lead === "pointer" ? [hover, focus] : [focus, hover], at = resolve(first) ?? resolve(second);
14524
+ if (same2(at, last))
14525
+ return;
14526
+ last = at, report(at);
14527
+ }, touch = () => {
14528
+ cancel ??= schedule(flush);
14529
+ };
14530
+ return {
14531
+ pointer(target) {
14532
+ if (hover = target, target !== null)
14533
+ lead = "pointer";
14534
+ touch();
14535
+ },
14536
+ focus(target) {
14537
+ if (focus = target, target !== null)
14538
+ lead = "focus";
14539
+ touch();
14540
+ },
14541
+ dispose() {
14542
+ if (cancel?.(), cancel = void 0, hover = focus = null, last !== null)
14543
+ report(last = null);
14544
+ }
14545
+ };
14546
+ }
14547
+ function activeEvents(reporter, where) {
14548
+ let inMenu = !1;
14549
+ return {
14550
+ over(e) {
14551
+ reporter.pointer(e.target);
14552
+ },
14553
+ move(e) {
14554
+ if (e.movementX || e.movementY)
14555
+ reporter.pointer(e.target);
14556
+ },
14557
+ out(e) {
14558
+ if (where(e.relatedTarget) !== "inside")
14559
+ reporter.pointer(null);
14560
+ },
14561
+ focusIn(e) {
14562
+ let at = where(e.target);
14563
+ if (inMenu = at === "menu", at === "inside")
14564
+ reporter.focus(e.target);
14565
+ else if (at === "outside")
14566
+ reporter.focus(null);
14567
+ },
14568
+ focusOut(e) {
14569
+ if (e.relatedTarget !== null)
14570
+ return;
14571
+ let at = where(e.target);
14572
+ if (at === "inside" || at === "menu" && inMenu)
14573
+ inMenu = !1, reporter.focus(null);
14574
+ }
14575
+ };
14576
+ }
14577
+ function watchActive(root, resolve, report) {
14578
+ let doc = root.ownerDocument, win = doc.defaultView, reporter = activeReporter(resolve, report, (flush) => {
14579
+ let id = win.requestAnimationFrame(flush);
14580
+ return () => win.cancelAnimationFrame(id);
14581
+ }), events = activeEvents(reporter, (node) => node instanceof Node && root.contains(node) ? "inside" : node instanceof Element && node.closest(".knobs-menu") ? "menu" : "outside"), el = (node) => node, over = (e) => events.over({ target: el(e.target) }), move = (e) => events.move({ target: el(e.target), movementX: e.movementX, movementY: e.movementY }), out = (e) => events.out({ relatedTarget: el(e.relatedTarget) }), focusIn = (e) => events.focusIn({ target: el(e.target) }), focusOut = (e) => events.focusOut({ target: el(e.target), relatedTarget: el(e.relatedTarget) });
14582
+ if (root.addEventListener("pointerover", over), root.addEventListener("pointermove", move), root.addEventListener("pointerout", out), doc.addEventListener("focusin", focusIn), doc.addEventListener("focusout", focusOut), doc.activeElement && root.contains(doc.activeElement))
14583
+ reporter.focus(doc.activeElement);
14584
+ return {
14585
+ dispose() {
14586
+ root.removeEventListener("pointerover", over), root.removeEventListener("pointermove", move), root.removeEventListener("pointerout", out), doc.removeEventListener("focusin", focusIn), doc.removeEventListener("focusout", focusOut), reporter.dispose();
14587
+ }
14588
+ };
14589
+ }
14590
+
14442
14591
  // src/panel/has.ts
14443
14592
  function trackHas(root) {
14444
14593
  let doc = root.ownerDocument, win = doc.defaultView, marked = { fv: [], active: [] }, mark = (list, name, nodes, value = "") => {
@@ -14589,7 +14738,7 @@ function morphFromChip(chip, root, wake, restored) {
14589
14738
  }, ensureShape = () => {
14590
14739
  if (shape)
14591
14740
  return shape;
14592
- let cs = getComputedStyle(root), r = parseFloat(cs.getPropertyValue("--kv-radius-panel"));
14741
+ let cs = getComputedStyle(root), r = parseFloat(cs.getPropertyValue("--kv-radius-frame"));
14593
14742
  if (Number.isFinite(r))
14594
14743
  panelRadius = r;
14595
14744
  shape = doc.createElement("div"), shape.className = "knobs-root knobs-morph", shape.setAttribute("aria-hidden", "true");
@@ -14719,6 +14868,7 @@ function foldMemory(id, storage) {
14719
14868
  let next = {
14720
14869
  sections: { ...data.sections },
14721
14870
  groups: { ...data.groups },
14871
+ ...data.subsections ? { subsections: { ...data.subsections } } : {},
14722
14872
  [kind]: { ...data[kind], [at]: folded }
14723
14873
  };
14724
14874
  try {
@@ -14729,14 +14879,27 @@ function foldMemory(id, storage) {
14729
14879
  }
14730
14880
 
14731
14881
  // src/panel/position.ts
14732
- function positionPanel(panel, header, anchor, id, storage) {
14733
- 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) {
14734
14888
  try {
14735
- let saved = JSON.parse(storage()?.getItem(key) ?? "null");
14889
+ let saved = JSON.parse(raw ?? "null");
14736
14890
  if (saved && Number.isFinite(saved.x) && Number.isFinite(saved.y))
14737
- x = saved.x, y = saved.y, hasSaved = !0;
14891
+ return { x: saved.x, y: saved.y, ...saved.right === !0 ? { right: !0 } : {} };
14738
14892
  } catch {}
14739
- if (!hasSaved) {
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));
14899
+ } catch {}
14900
+ if (saved)
14901
+ x = leftOf(saved, w, win.innerWidth), y = saved.y;
14902
+ else {
14740
14903
  let others = [...doc.querySelectorAll(".knobs-panel:not(.knobs-inline)")].filter((el) => el !== panel).map((el) => el.getBoundingClientRect());
14741
14904
  if (others.length > 0) {
14742
14905
  let overlaps = (cx, cy) => others.some((r) => Math.abs(cx - r.left) < w - 24 && Math.abs(cy - r.top) < 120);
@@ -14756,6 +14919,8 @@ function positionPanel(panel, header, anchor, id, storage) {
14756
14919
  }
14757
14920
  }
14758
14921
  let shown = { x, y }, constrain = () => {
14922
+ if (saved && !drag)
14923
+ x = leftOf(saved, panel.offsetWidth, win.innerWidth);
14759
14924
  shown = {
14760
14925
  x: Math.max(8, Math.min(x, win.innerWidth - panel.offsetWidth - 8)),
14761
14926
  y: Math.max(8, Math.min(y, win.innerHeight - panel.offsetHeight - 8))
@@ -14764,17 +14929,27 @@ function positionPanel(panel, header, anchor, id, storage) {
14764
14929
  header.onpointerdown = (event) => {
14765
14930
  if (event.button !== 0 || event.target.closest("button,select,input"))
14766
14931
  return;
14767
- 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 };
14768
14933
  }, header.onpointermove = (event) => {
14769
14934
  if (!drag || event.pointerId !== drag.id)
14770
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
+ }
14771
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;
14772
14947
  }, header.onlostpointercapture = () => {
14773
14948
  if (!drag)
14774
14949
  return;
14775
- 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);
14776
14951
  try {
14777
- storage()?.setItem(key, JSON.stringify({ x, y }));
14952
+ storage()?.setItem(key, JSON.stringify(saved));
14778
14953
  } catch {}
14779
14954
  };
14780
14955
  let observer = new ResizeObserver(constrain);
@@ -14888,7 +15063,7 @@ function createPresence(root, options) {
14888
15063
  if (!root.dataset.presence || root.hidden)
14889
15064
  return;
14890
15065
  let look = presenceLook(spring.x, inline, reduced), r = root.getBoundingClientRect();
14891
- radius ??= parseFloat(win.getComputedStyle(root).getPropertyValue("--kv-radius-panel")) || 12;
15066
+ radius ??= parseFloat(win.getComputedStyle(root).getPropertyValue("--kv-radius-frame")) || 12;
14892
15067
  let s = look.scale, ox = r.left + r.width / 2, oy = inline ? r.top : r.top + r.height / 2;
14893
15068
  return {
14894
15069
  x: ox + (r.left - ox) * s,
@@ -14915,6 +15090,19 @@ function createPresence(root, options) {
14915
15090
  };
14916
15091
  }
14917
15092
 
15093
+ // src/panel/summary.ts
15094
+ function panelFoldSummary(rows, preset) {
15095
+ if (rows.length === 1 && rows[0].value)
15096
+ return rows[0].value;
15097
+ let lead = preset ?? rows.find((r) => r.changed && r.value)?.value ?? rows.find((r) => r.choice && r.value)?.value, changed = rows.filter((r) => r.changed).length;
15098
+ return [lead, changed ? `${changed} changed` : ""].filter(Boolean).join(" · ");
15099
+ }
15100
+ var baseSignature = (controls) => JSON.stringify([
15101
+ controls.activePreset(),
15102
+ Object.keys(controls.scopes).map((s) => controls.activePreset(s)),
15103
+ Object.keys(controls.groupPresets).map((g) => controls.activeGroupPreset(g))
15104
+ ]);
15105
+
14918
15106
  // src/panel/panel.ts
14919
15107
  var layer = 2147480001, COPIED_MS = 1000, lastTab = /* @__PURE__ */ new Map, REVEAL_QUIET_MS = 2000, NUDGE_BURST_MS = 300;
14920
15108
  function readableTitle(id) {
@@ -15339,16 +15527,19 @@ function buildPanel(controls, host, options) {
15339
15527
  foldTile(card, next && !filterEl.value.trim(), animate);
15340
15528
  else
15341
15529
  applyVisibility(animate ? "reveal" : "instant");
15530
+ let marked = page ? card.parentElement : head;
15531
+ if (marked) {
15532
+ if (marked.toggleAttribute("data-folded", next), animate)
15533
+ marked.setAttribute("data-fold-motion", ""), clearTimeout(foldMotions.get(marked)), foldMotions.set(marked, setTimeout(() => marked.removeAttribute("data-fold-motion"), 400));
15534
+ }
15342
15535
  if (page) {
15343
- let tile = card.parentElement;
15344
- if (tile.toggleAttribute("data-folded", next), animate)
15345
- tile.setAttribute("data-fold-motion", ""), clearTimeout(foldMotions.get(tile)), foldMotions.set(tile, setTimeout(() => tile.removeAttribute("data-fold-motion"), 400));
15346
15536
  if (remember)
15347
15537
  foldsKept?.set("groups", group, next);
15348
- refreshFolds();
15349
- } else if (sessionKept)
15350
- writeSession(controls.id, { groups: { ...readSession(controls.id).groups, [group]: next } });
15351
- positioned?.constrain(), wakeGpu();
15538
+ } else if (sessionKept) {
15539
+ if (writeSession(controls.id, { groups: { ...readSession(controls.id).groups, [group]: next } }), remember)
15540
+ panelFolds?.set("groups", group, next);
15541
+ }
15542
+ refreshFolds(), positioned?.constrain(), wakeGpu();
15352
15543
  }
15353
15544
  function foldTile(card, shut, animate) {
15354
15545
  if ((!card.hidden && card.dataset.leaving !== "true") !== shut)
@@ -15375,9 +15566,7 @@ function buildPanel(controls, host, options) {
15375
15566
  });
15376
15567
  gReset.className = "knobs-group-reset", gReset.dataset.hint = `${title} reset`, bloom(gReset, !1), groupResetButtons.set(groupName, gReset);
15377
15568
  let gDie = createHeaderDie(doc, () => wakeGpu(), GROUP_DIE), gRandom = iconButton("random", `Randomize ${title}`, "g Alt+g", () => {});
15378
- 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));
15379
- let rollable = Object.values(controls.schema).some((f) => f.group === groupName && hasValue2(f));
15380
- 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"))
15381
15570
  gRandom.hidden = !0;
15382
15571
  groupDice.set(groupName, { button: gRandom, die: gDie });
15383
15572
  let presets = page ? (controls.groupPresets[groupName] ?? []).slice(0, 9) : [], ui, gPeek = el("span");
@@ -15444,11 +15633,13 @@ function buildPanel(controls, host, options) {
15444
15633
  desc.className = "knobs-group-desc", groupDescs.set(groupName, desc), describeGroup(groupName);
15445
15634
  }
15446
15635
  if (foldAt !== void 0) {
15447
- if (page)
15448
- summary = el("span"), summary.className = "knobs-group-summary", gRight.append(summary), groupSummaries.set(groupName, summary);
15449
- caret = caretSvg(), gRight.append(caret), gHead.dataset.foldable = "", gHead.tabIndex = -1, gHead.setAttribute("role", "button"), gHead.setAttribute("aria-label", title);
15450
- let folded = (page ? foldsKept?.get("groups", groupName) : session.groups?.[groupName]) ?? foldAt;
15451
- folds.set(groupName, folded), gHead.setAttribute("aria-expanded", String(!folded)), gHead.addEventListener("click", (event) => {
15636
+ if (summary = el("span"), summary.className = "knobs-group-summary", page)
15637
+ gRight.append(summary);
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);
15639
+ let folded = (page ? foldsKept?.get("groups", groupName) : session.groups?.[groupName] ?? panelFolds?.get("groups", groupName)) ?? foldAt;
15640
+ if (folds.set(groupName, folded), gHead.setAttribute("aria-expanded", String(!folded)), !page)
15641
+ gHead.toggleAttribute("data-folded", folded);
15642
+ gHead.addEventListener("click", (event) => {
15452
15643
  if (event.detail > 1 || event.target.closest("button"))
15453
15644
  return;
15454
15645
  byUser(() => fold(groupName));
@@ -15457,8 +15648,10 @@ function buildPanel(controls, host, options) {
15457
15648
  if (page) {
15458
15649
  let gTitle = el("span");
15459
15650
  gTitle.className = "knobs-group-title", gTitle.append(gLabel, gPeek), gHead.append(gTitle, gRight);
15460
- } else
15461
- gHead.append(gLabel, gRight);
15651
+ } else {
15652
+ let desc = el("span");
15653
+ desc.className = "knobs-group-desc", groupDescs.set(groupName, desc), describeGroup(groupName), gHead.append(gLabel, desc, ...summary ? [summary] : [], gRight);
15654
+ }
15462
15655
  return gpuGroups.push({
15463
15656
  element: gHead,
15464
15657
  labelEl: gLabel,
@@ -15499,7 +15692,7 @@ function buildPanel(controls, host, options) {
15499
15692
  if (!preset)
15500
15693
  return;
15501
15694
  cue(root, coalesce ? "select" : "preset"), controls.groupPreset(group, index, { coalesce }), peek(group, preset.name, 1100);
15502
- }, 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) => {
15503
15696
  let combo = controls.shortcut(key);
15504
15697
  return combo ? { key: combo, scope: actionShortcut2(field)?.scope ?? "panel" } : void 0;
15505
15698
  }, readouts = /* @__PURE__ */ new Map, readoutPort = controls[READOUT], planRows = (schema) => {
@@ -15528,7 +15721,8 @@ function buildPanel(controls, host, options) {
15528
15721
  label,
15529
15722
  ...duration.group !== void 0 ? { group: duration.group } : {},
15530
15723
  ...duration.section !== void 0 ? { section: duration.section } : {},
15531
- ...duration.shortcut !== void 0 ? { shortcut: duration.shortcut } : {}
15724
+ ...duration.shortcut !== void 0 ? { shortcut: duration.shortcut } : {},
15725
+ ...duration.description !== void 0 ? { description: duration.description } : {}
15532
15726
  },
15533
15727
  parts: { duration, bounce },
15534
15728
  group: duration.group,
@@ -15958,7 +16152,33 @@ function buildPanel(controls, host, options) {
15958
16152
  return;
15959
16153
  slot.replaceChildren(...node ? [node] : []);
15960
16154
  }
15961
- 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) => {
15962
16182
  let v = field[name];
15963
16183
  return typeof v === "function" ? Boolean(v(controls.get())) : Boolean(v);
15964
16184
  };
@@ -15989,7 +16209,7 @@ function buildPanel(controls, host, options) {
15989
16209
  announceSections();
15990
16210
  }, delay), wakeGpu();
15991
16211
  }, dropGpu = (row) => {
15992
- row.dispose?.(), row.slider?.dispose(), row.editor?.dispose();
16212
+ undescribe(row), row.dispose?.(), row.slider?.dispose(), row.editor?.dispose();
15993
16213
  for (let g of row.gpu) {
15994
16214
  let at = gpuRows.indexOf(g);
15995
16215
  if (at >= 0)
@@ -16030,11 +16250,11 @@ function buildPanel(controls, host, options) {
16030
16250
  for (let spec of specs) {
16031
16251
  let existing = kept.get(spec.id);
16032
16252
  if (existing && existing.shape === shapeOf(spec)) {
16033
- kept.delete(spec.id), existing.retune(spec), next.push(existing);
16253
+ kept.delete(spec.id), existing.retune(spec), describe(existing), next.push(existing);
16034
16254
  continue;
16035
16255
  }
16036
16256
  let row = buildRow(spec);
16037
- 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);
16038
16258
  }
16039
16259
  let leaving = [...kept.values()];
16040
16260
  rows = next;
@@ -16116,6 +16336,8 @@ function buildPanel(controls, host, options) {
16116
16336
  motion.push({ node: r.row, show: !0 });
16117
16337
  }
16118
16338
  }
16339
+ for (let group of groupDescs.keys())
16340
+ describeGroup(group);
16119
16341
  for (let [id, sh] of subheads)
16120
16342
  if (!usedSubheads.has(id)) {
16121
16343
  sh.el.remove();
@@ -16166,9 +16388,7 @@ function buildPanel(controls, host, options) {
16166
16388
  let stagger = cascade(motion.length);
16167
16389
  motion.forEach((m, i) => present(m.node, m.show, i * stagger, m.after));
16168
16390
  }
16169
- if (applyVisibility(animate ? "reveal" : "instant"), page)
16170
- refreshFolds();
16171
- if (focused && root.contains(focused) && doc.activeElement !== focused && focused.isConnected)
16391
+ if (applyVisibility(animate ? "reveal" : "instant"), refreshFolds(), focused && root.contains(focused) && doc.activeElement !== focused && focused.isConnected)
16172
16392
  focused.focus({ preventScroll: !0 });
16173
16393
  let gone = focused ? leaving.find((r) => r.row.contains(focused)) : void 0;
16174
16394
  if (gone) {
@@ -16193,7 +16413,7 @@ function buildPanel(controls, host, options) {
16193
16413
  label.className = "knobs-subhead-label";
16194
16414
  let caret = caretSvg();
16195
16415
  if (node.append(label, caret), !subFolds.has(id))
16196
- 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);
16197
16417
  node.setAttribute("aria-expanded", String(!subFolds.get(id))), node.addEventListener("click", (event) => {
16198
16418
  if (event.detail < 2)
16199
16419
  foldSub(id);
@@ -16224,7 +16444,7 @@ function buildPanel(controls, host, options) {
16224
16444
  if (applyVisibility("reveal"), remember)
16225
16445
  writeSession(controls.id, {
16226
16446
  subsections: { ...readSession(controls.id).subsections, [id]: next }
16227
- });
16447
+ }), panelFolds?.set("subsections", id, next);
16228
16448
  }
16229
16449
  let groupTab = (group) => {
16230
16450
  for (let f of Object.values(controls.schema))
@@ -16293,7 +16513,7 @@ function buildPanel(controls, host, options) {
16293
16513
  return selectTab(list[(at + direction + list.length) % list.length].id, !1), !0;
16294
16514
  }, searching = () => search?.filtering() ?? !1, rowMatches = (r) => {
16295
16515
  let query = search.query();
16296
- if (query && !matchesFilter(query, r.display, r.key, r.group, r.group && groupTitle(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))
16297
16517
  return !1;
16298
16518
  for (let chip of search.active())
16299
16519
  if (!r.keys.some((k) => chipTests.get(chip)?.(k)))
@@ -16392,7 +16612,10 @@ function buildPanel(controls, host, options) {
16392
16612
  lock.dataset.enter = "", lock.getBoundingClientRect();
16393
16613
  }
16394
16614
  return lock;
16395
- }, 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) => {
16396
16619
  let roller = groupDice.get(group)?.die;
16397
16620
  if (!roller)
16398
16621
  return;
@@ -16498,15 +16721,22 @@ function buildPanel(controls, host, options) {
16498
16721
  });
16499
16722
  apply.className = "knobs-apply-btn", apply.setAttribute("aria-label", "Import JSON"), apply.hidden = !0;
16500
16723
  let copy = async (value, inPlace = !0) => {
16501
- try {
16502
- if (await doc.defaultView.navigator.clipboard.writeText(value), inPlace && !copyButton.hidden && headerShown())
16724
+ let done = () => {
16725
+ if (inPlace && !copyButton.hidden && headerShown())
16503
16726
  copied();
16504
16727
  else
16505
16728
  say("Copied.");
16729
+ };
16730
+ try {
16731
+ await doc.defaultView.navigator.clipboard.writeText(value), done();
16506
16732
  } catch {
16507
- if (text.value = value, page)
16508
- togglePopover(!0);
16509
- 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
+ }
16510
16740
  }
16511
16741
  wakeGpu();
16512
16742
  }, gpuActionButtons = [], panelActions = allActions.filter((a) => !a.group), customActions = el("div");
@@ -16585,11 +16815,7 @@ function buildPanel(controls, host, options) {
16585
16815
  if (changed)
16586
16816
  wakeGpu();
16587
16817
  }
16588
- let shownBase = "", baseSignature = () => JSON.stringify([
16589
- controls.activePreset(),
16590
- Object.keys(controls.scopes).map((s) => controls.activePreset(s)),
16591
- Object.keys(controls.groupPresets).map((g) => controls.activeGroupPreset(g))
16592
- ]);
16818
+ let shownBase = "";
16593
16819
  function update() {
16594
16820
  if (!controls.enabled) {
16595
16821
  close?.();
@@ -16608,7 +16834,7 @@ function buildPanel(controls, host, options) {
16608
16834
  if (!fresh(r) && changed(r))
16609
16835
  changedCount++;
16610
16836
  }
16611
- let stagger = initialized ? cascade(changedCount) : 0, moving = initialized ? rows.filter(changed) : rows;
16837
+ let stagger = initialized ? cascade(changedCount) : 0, moving = initialized ? rows.filter(changed) : rows, rebasing = baseSignature(controls) !== shownBase;
16612
16838
  if (updateStrips(), batchMeasures(() => {
16613
16839
  for (let row of moving)
16614
16840
  if (row.slider?.prime(), row.editor?.prime?.(), row.choice)
@@ -16616,7 +16842,7 @@ function buildPanel(controls, host, options) {
16616
16842
  for (let chips of groupChips.values())
16617
16843
  primeSegPill(chips.seg, chips.buttons);
16618
16844
  showValues(values, moving, stagger, landing);
16619
- }), moving.length || !initialized)
16845
+ }), moving.length || !initialized || rebasing)
16620
16846
  refreshFolds();
16621
16847
  shown = values, initialized = !0, wakeGpu();
16622
16848
  }
@@ -16636,7 +16862,7 @@ function buildPanel(controls, host, options) {
16636
16862
  }
16637
16863
  }
16638
16864
  function showValues(values, moving, stagger, landing) {
16639
- let baseline = controls.baseValues(), fresh = (r) => r.row.dataset.shown === void 0, now = performance.now(), turn = 0, based = baseSignature(), rebased = based !== shownBase;
16865
+ let baseline = controls.baseValues(), fresh = (r) => r.row.dataset.shown === void 0, now = performance.now(), turn = 0, based = baseSignature(controls), rebased = based !== shownBase;
16640
16866
  shownBase = based;
16641
16867
  for (let row of moving) {
16642
16868
  let instant = !initialized || landing || fresh(row);
@@ -16690,10 +16916,16 @@ function buildPanel(controls, host, options) {
16690
16916
  }
16691
16917
  if (randomButton.disabled !== (unlocked === 0))
16692
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;
16693
16922
  for (let [group, { button }] of groupDice) {
16694
16923
  let off = !options.groupRandom && !groupKeys(group).some((k) => controls.canRoll(k));
16695
16924
  if (button.disabled !== off)
16696
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;
16697
16929
  }
16698
16930
  if (root.hasAttribute("data-comparing") !== controls.comparing)
16699
16931
  root.toggleAttribute("data-comparing", controls.comparing);
@@ -16846,7 +17078,7 @@ function buildPanel(controls, host, options) {
16846
17078
  filtered = query, root.toggleAttribute("data-filtering", Boolean(query)), layout.change(() => {
16847
17079
  layout.openFolds(Boolean(query));
16848
17080
  for (let r of rows) {
16849
- 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);
16850
17082
  if (r.row.hidden === show)
16851
17083
  r.row.hidden = !show;
16852
17084
  }
@@ -16889,16 +17121,45 @@ function buildPanel(controls, host, options) {
16889
17121
  preset: null,
16890
17122
  changed: changedRows((r) => r.section === section),
16891
17123
  keys: rows.filter((r) => r.section === section).flatMap((r) => r.keys)
16892
- }), changedText = (n) => n && marks ? `${n} changed` : "";
17124
+ }), changedText = (n) => n && marks ? `${n} changed` : "", valueText = (r, values) => {
17125
+ let f = r.field, v = r.value(values), num = (n) => String(Number(n.toFixed(2)));
17126
+ switch (f.kind) {
17127
+ case "choice":
17128
+ return f.labels?.[v] ?? String(v);
17129
+ case "toggle":
17130
+ return v ? "On" : "Off";
17131
+ case "knob":
17132
+ return `${v.toFixed(f.decimals ?? decimals(f.step))}${f.unit ? ` ${f.unit}` : ""}`;
17133
+ case "spring": {
17134
+ let { duration, bounce } = v;
17135
+ return `${num(duration)} s${bounce ? `, bounce ${num(bounce)}` : ""}`;
17136
+ }
17137
+ case "color":
17138
+ return colorHex2(v);
17139
+ case "text":
17140
+ return String(v);
17141
+ default:
17142
+ return "";
17143
+ }
17144
+ }, groupSummary = (group) => {
17145
+ let info = groupInfo(group);
17146
+ if (options.groupSummary)
17147
+ return options.groupSummary(group, info);
17148
+ if (page)
17149
+ return [info.preset, changedText(info.changed)].filter(Boolean).join(" · ") || null;
17150
+ let values = controls.get(), own = rows.filter((r) => r.group === group && !r.leaving && r.keys.length);
17151
+ return panelFoldSummary(own.map((r) => ({
17152
+ value: valueText(r, values),
17153
+ changed: marks && r.keys.some((k) => controls.isModified(k)),
17154
+ choice: r.field.kind === "choice"
17155
+ })), info.preset) || null;
17156
+ };
16893
17157
  function refreshFolds() {
17158
+ for (let [group, node] of groupSummaries)
17159
+ if (folds.get(group))
17160
+ putSummary(node, groupSummary(group));
16894
17161
  if (!layout || !page)
16895
17162
  return;
16896
- for (let [group, node] of groupSummaries) {
16897
- if (!folds.get(group))
16898
- continue;
16899
- let info = groupInfo(group);
16900
- putSummary(node, page.groupSummary ? page.groupSummary(group, info) : [info.preset, changedText(info.changed)].filter(Boolean).join(" · ") || null);
16901
- }
16902
17163
  for (let s of layout.sectionList()) {
16903
17164
  let node = s.label?.querySelector(":scope > .knobs-section-summary");
16904
17165
  if (!node || !s.folded)
@@ -17294,8 +17555,19 @@ function buildPanel(controls, host, options) {
17294
17555
  setTimeout(() => {
17295
17556
  pointerHeld = !1, queueScope();
17296
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
+ });
17297
17569
  };
17298
- 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", () => {
17299
17571
  pointerOut = !1;
17300
17572
  }, !0);
17301
17573
  let syncScope = () => {
@@ -18050,7 +18322,15 @@ function buildPanel(controls, host, options) {
18050
18322
  attributes: !0,
18051
18323
  attributeFilter: ["data-knobs-renderer", "data-theme", "data-appearance", "class", "style"]
18052
18324
  });
18053
- let panel = {
18325
+ let onActive = options.onActive, active = onActive ? watchActive(root, (node) => {
18326
+ if (!node || !root.contains(node))
18327
+ return null;
18328
+ let r = rows.find((r) => !r.leaving && r.row.contains(node));
18329
+ if (r)
18330
+ return r.group === void 0 ? null : { group: r.group, key: r.key };
18331
+ let card = node.closest("[data-group]");
18332
+ return card && root.contains(card) ? { group: card.dataset.group } : null;
18333
+ }, onActive) : void 0, panel = {
18054
18334
  element: root,
18055
18335
  presetStrip,
18056
18336
  focus() {
@@ -18113,7 +18393,7 @@ function buildPanel(controls, host, options) {
18113
18393
  doc.removeEventListener(type, onPointerUp, !0);
18114
18394
  for (let type of ["pointerup", "pointercancel"])
18115
18395
  doc.removeEventListener(type, endScrub, !0);
18116
- 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(), 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);
18117
18397
  for (let k of inHand)
18118
18398
  controls.hold(k, !1);
18119
18399
  inHand.clear(), positioned?.dispose(), die.dispose();
@@ -18264,6 +18544,17 @@ function buildPanel(controls, host, options) {
18264
18544
  }
18265
18545
  return pageApi;
18266
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
+ }
18267
18558
  function createPanel(controls, anchor, close, options) {
18268
18559
  return buildPanel(controls, anchor.ownerDocument.body, {
18269
18560
  ...options,