@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
package/dist/panel.js CHANGED
@@ -1825,6 +1825,14 @@ var init_schema = __esm(() => {
1825
1825
  doc: "Panel and card radius, concentric with controls.",
1826
1826
  deprecated: ["--k-panel-radius", "--ku-panel-radius"]
1827
1827
  },
1828
+ {
1829
+ name: "radius-frame",
1830
+ tier: "semantic",
1831
+ type: "length",
1832
+ group: "Shape",
1833
+ source: css("calc(var(--k-radius) + min(var(--k-radius) * 999, var(--_k-pad) + 1px))"),
1834
+ doc: "A tuning panel's outer corners: its cards' radius plus its padding and border, so the cards, the preset strip and the focus lens inside run concentric. Squares when --k-radius is 0."
1835
+ },
1828
1836
  {
1829
1837
  name: "round",
1830
1838
  tier: "semantic",
@@ -2943,7 +2951,16 @@ var init_glide = __esm(() => {
2943
2951
  "border-subtle",
2944
2952
  "bg-page"
2945
2953
  ];
2946
- LENGTH_TOKENS = ["pad", "inset", "row", "gap", "radius", "radius-panel", "font-size"];
2954
+ LENGTH_TOKENS = [
2955
+ "pad",
2956
+ "inset",
2957
+ "row",
2958
+ "gap",
2959
+ "radius",
2960
+ "radius-panel",
2961
+ "radius-frame",
2962
+ "font-size"
2963
+ ];
2947
2964
  NUMBER_TOKENS = ["weight-label", "blur", "glow", "glass", "glass-tone"];
2948
2965
  ANIMATED = [...COLOR_TOKENS, ...LENGTH_TOKENS, ...NUMBER_TOKENS, "width"];
2949
2966
  TOKEN_TRANSITIONS = ANIMATED.map((t) => `--kt-${t} var(--k-token-t)`).join(",");
@@ -3129,13 +3146,15 @@ ${GRAD_PROPS}
3129
3146
  catches its top. The light rides on the edge (a shadow raised by its width, so only its top
3130
3147
  shows), not inside the card's 1px border, where it read as a second edge. */
3131
3148
  .knobs-panel{-webkit-user-select:none;user-select:none;transition:${TOKEN_TRANSITIONS},--k-armed ${ARMED_T};position:fixed;z-index:2147480001;width:var(--_k-width);max-width:calc(100vw - 16px);max-height:calc(100dvh - 16px);overflow:auto;padding:var(--_k-pad);
3132
- background:color-mix(in srgb,var(--_k-bg) calc(100% - var(--_k-glass) * 20%),transparent);border:1px solid transparent;border-radius:var(--_k-radius-panel);box-shadow:var(--_k-shadow-overlay),0 calc(var(--k-rim-width) * -1) 0 var(--_k-rim),0 0 0 var(--k-rim-width) var(--ka-edge),var(--ka-glow);scrollbar-width:thin;
3149
+ background:color-mix(in srgb,var(--_k-bg) calc(100% - var(--_k-glass) * 20%),transparent);border:1px solid transparent;border-radius:var(--_k-radius-frame);box-shadow:var(--_k-shadow-overlay),0 calc(var(--k-rim-width) * -1) 0 var(--_k-rim),0 0 0 var(--k-rim-width) var(--ka-edge),var(--ka-glow);scrollbar-width:thin;
3133
3150
  /* Armed (focus inside: keys come here), the edge takes the accent and a faint glow lifts it. */
3134
3151
  --ka-edge:color-mix(in srgb,var(--_k-accent) calc(var(--k-armed) * var(--k-armed-edge) * 100%),var(--_k-border-subtle));
3135
3152
  --ka-glow:0 0 calc(var(--k-armed-glow) * 1px) color-mix(in srgb,var(--_k-accent) calc(var(--k-armed) * var(--k-armed-glow-a) * var(--k-chrome,1) * 100%),transparent);
3136
3153
  --k-backdrop:blur(calc(var(--_k-glass) * 24px)) saturate(calc(1 + var(--_k-glass) * .8)) brightness(calc(1 + (var(--_k-glass-tone) - 1) * var(--_k-glass)));
3137
3154
  -webkit-backdrop-filter:var(--k-backdrop);backdrop-filter:var(--k-backdrop);
3138
3155
  animation:knobs-in .16s var(--k-ease);}
3156
+ /* On a phone a floating panel takes the width, less a gutter, and scrolls inside the height. */
3157
+ @media(max-width:420px){.knobs-panel:not(.knobs-inline){width:calc(100vw - 16px)}}
3139
3158
  .knobs-panel[data-restored]{animation:none}
3140
3159
  /* Opening from its chip, the panel's chrome rides a separate shape (see morph.ts) while the
3141
3160
  content, laid out at full size, is clipped to it and revealed top first. */
@@ -3169,8 +3188,9 @@ ${GRAD_PROPS}
3169
3188
  :is(.knobs-panel,.knobs-menu) :is(:focus,:focus-visible):where(:not(.knobs-accessory *)){outline:none}
3170
3189
 
3171
3190
  /* Header */
3172
- .knobs-panel header{position:relative;display:flex;align-items:center;justify-content:space-between;gap:6px;cursor:grab;touch-action:none;padding:0 var(--_k-inset) var(--k-section-gap);min-height:var(--k-icon-btn)}
3173
- .knobs-panel.knobs-inline header,.knobs-panel.knobs-inline header:active{cursor:default}
3191
+ /* A floating panel's header carries it; the hand shows only while it's carrying (data-dragging). */
3192
+ .knobs-panel header{position:relative;display:flex;align-items:center;justify-content:space-between;gap:6px;touch-action:none;padding:0 var(--_k-inset) var(--k-section-gap);min-height:var(--k-icon-btn)}
3193
+ .knobs-panel[data-dragging]>header,.knobs-panel[data-dragging]>header *{cursor:grabbing}
3174
3194
  /* The title row stays pinned while the rows scroll under it, in the panel (it scrolls once it's
3175
3195
  bounded) or, with .knobs-pin-ancestor (which also makes the panel overflow:visible), in the
3176
3196
  nearest scroller around it. Once rows are under it, and only then, it frosts (backdrop blur over
@@ -3185,7 +3205,7 @@ ${GRAD_PROPS}
3185
3205
  @supports (animation-timeline:scroll()){
3186
3206
  .knobs-panel:not(.knobs-page){scroll-timeline:--knobs-self block}
3187
3207
  .knobs-panel.knobs-pin-ancestor:not(.knobs-page){overflow:visible;view-timeline:--knobs-view block}
3188
- .knobs-panel:not(.knobs-page)>header{position:sticky;top:0;z-index:4;border-radius:max(0px,calc(var(--_k-radius-panel) - 1px)) max(0px,calc(var(--_k-radius-panel) - 1px)) 0 0;
3208
+ .knobs-panel:not(.knobs-page)>header{position:sticky;top:0;z-index:4;border-radius:max(0px,calc(var(--_k-radius-frame) - 1px)) max(0px,calc(var(--_k-radius-frame) - 1px)) 0 0;
3189
3209
  --k-head-on:max(var(--k-head-s),var(--k-head-v));--k-head-bg:color-mix(in srgb,var(--_k-bg) 88%,transparent);
3190
3210
  background:color-mix(in srgb,var(--k-head-bg) calc(var(--k-head-on) * 100%),transparent);
3191
3211
  animation:knobs-head-s linear both;animation-timeline:--knobs-self;animation-range:0px 24px}
@@ -3195,7 +3215,6 @@ ${GRAD_PROPS}
3195
3215
  .knobs-panel[data-gpu-active="true"]:not(.knobs-page)>header{background:transparent!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important}
3196
3216
  .knobs-panel[data-gpu-active="true"]:not(.knobs-page)>header::after{display:none}
3197
3217
  }
3198
- .knobs-panel header:active{cursor:grabbing}
3199
3218
  .knobs-panel header strong{flex:1;min-width:0;font-weight:var(--_k-weight-title);font-size:var(--_k-text-title);letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:opacity .2s var(--k-ease),color ${ARMED_T};
3200
3219
  color:color-mix(in srgb,var(--_k-fg) calc(var(--k-title-rest) * 100%),var(--_k-fg-2))}
3201
3220
  /* At rest a panel's title is a little quiet; armed (keys come here) it's at full strength. */
@@ -3335,6 +3354,13 @@ ${GRAD_PROPS}
3335
3354
  .knobs-group-head .knobs-group-die{width:20px;padding:0;display:inline-flex;align-items:center;justify-content:center;border-radius:var(--_k-round);color:var(--_k-fg-3)}
3336
3355
  .knobs-group-head .knobs-group-die[disabled]{opacity:var(--k-disabled-opacity);cursor:default}
3337
3356
  .knobs-panel .knobs-group-head button:where(:not(.knobs-group-reset)){transition:background var(--k-hover-t),color var(--k-hover-t),opacity .2s,transform .12s}
3357
+ /* A panel card's header: its title, a folded card's summary and its buttons on one line, and its
3358
+ description on the next. The title takes its room first; the summary gets what's left. */
3359
+ .knobs-panel:not(.knobs-page) .knobs-group-head{flex-wrap:wrap;row-gap:0}
3360
+ .knobs-panel:not(.knobs-page) .knobs-group-head>.knobs-group-label{flex:1000 1 0;max-width:max-content;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3361
+ .knobs-group-head>.knobs-group-desc{order:1;flex:0 0 100%;padding:0;line-height:16px;white-space:normal;overflow:visible;text-wrap:pretty}
3362
+ .knobs-group-head>.knobs-group-summary{flex:1 1 0;max-width:max-content;margin-left:auto;font-size:var(--_k-text-meta);text-transform:none}
3363
+ .knobs-group-head[data-folded]>.knobs-group-summary:not(:empty){display:block}
3338
3364
 
3339
3365
  /* Grouped cards: rows share one surface, split by hairlines that stay put whatever a row is doing */
3340
3366
  .knobs-card{position:relative;margin-bottom:var(--k-section-gap);border-radius:var(--_k-radius);background:var(--_k-fill);overflow:hidden}
@@ -4041,7 +4067,7 @@ ${POINTER_ROW} .knobs-slider.knobs-modified:not(:hover,.knobs-active) .knobs-val
4041
4067
  .knobs-group-summary{display:none;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--_k-fg-3);font-size:var(--_k-text-control);font-weight:500;letter-spacing:0;font-variant-numeric:tabular-nums}
4042
4068
  .knobs-tile[data-folded]>.knobs-group-head .knobs-group-summary:not(:empty){display:block}
4043
4069
  .knobs-tile[data-folded]>.knobs-group-head .knobs-group-ctl{display:none}
4044
- .knobs-tile[data-fold-motion]>.knobs-group-head :is(.knobs-group-summary,.knobs-group-ctl){animation:knobs-fade .2s var(--k-ease) both}
4070
+ .knobs-tile[data-fold-motion]>.knobs-group-head :is(.knobs-group-summary,.knobs-group-ctl),.knobs-group-head[data-fold-motion]>.knobs-group-summary{animation:knobs-fade .2s var(--k-ease) both}
4045
4071
  /* A highlight (page.highlight): the rest dims under a veil of the page's colour (--k-page-dim at
4046
4072
  full), and what's highlighted on its own gets a dot in the gutter by its title. The veil and
4047
4073
  dot are written each frame on the panel spring (see page.ts). */
@@ -5139,12 +5165,24 @@ function createRegistry(doc) {
5139
5165
  return;
5140
5166
  if (e.metaKey || e.ctrlKey || e.altKey || e.key !== PAGE_HINT_KEY)
5141
5167
  return;
5142
- if (isTyping(e.target) || !wide())
5168
+ if (isTyping(e.target) || !wide(e.target))
5143
5169
  return;
5144
5170
  if (registry.start())
5145
5171
  e.preventDefault();
5146
5172
  };
5147
- const wide = () => listening && (sources.length > 1 || [...doc.querySelectorAll("[data-hint]")].some((el) => !el.closest(".knobs-panel")));
5173
+ const hostHints = () => [...doc.querySelectorAll("[data-hint]")].some((el) => !el.closest(".knobs-panel"));
5174
+ const wide = (target) => {
5175
+ if (!listening)
5176
+ return false;
5177
+ if (sources.some((s) => s.listen && s.rank >= 2) || hostHints())
5178
+ return true;
5179
+ if (sources.length < 2)
5180
+ return false;
5181
+ if (target === undefined)
5182
+ return true;
5183
+ const piece = target?.closest?.(".knobs-ui,.knobs-pane-body");
5184
+ return Boolean(piece?.classList.contains("knobs-ui"));
5185
+ };
5148
5186
  const relisten = () => {
5149
5187
  const next = sources.some((s) => s.listen);
5150
5188
  if (next === listening)
@@ -9128,6 +9166,7 @@ function readPanelTokens(panel, version = -1) {
9128
9166
  const trayPad = numProp(style, "tray-pad", unit * 0.5);
9129
9167
  const radius = numProp(style, "radius", 8);
9130
9168
  const panelRadius = numProp(style, "radius-panel", radius + unit);
9169
+ const frameRadius = numProp(style, "radius-frame", radius > 0 ? radius + pad + 1 : 0);
9131
9170
  const radiusMd = Math.max(0, radius - trayPad);
9132
9171
  const radiusSm = Math.max(0, radius - trayPad * 2);
9133
9172
  const fontSize = parseFloat(style.fontSize) || numProp(style, "font-size", 12);
@@ -9184,6 +9223,7 @@ function readPanelTokens(panel, version = -1) {
9184
9223
  trayPad,
9185
9224
  radius,
9186
9225
  panelRadius,
9226
+ frameRadius,
9187
9227
  radiusMd,
9188
9228
  radiusSm,
9189
9229
  fontSize,
@@ -11521,6 +11561,44 @@ class GpuOverlayManager {
11521
11561
  this.clipCache.set(el, { key, text, width, clipped });
11522
11562
  return clipped;
11523
11563
  }
11564
+ lineCache = new WeakMap;
11565
+ linesOf(el, width) {
11566
+ const text = el.textContent ?? "";
11567
+ const hit = this.lineCache.get(el);
11568
+ if (hit && hit.text === text && hit.width === width)
11569
+ return hit.lines;
11570
+ const lines = [];
11571
+ const node = el.firstChild;
11572
+ if (node?.nodeType === Node.TEXT_NODE) {
11573
+ const top = el.getBoundingClientRect().top;
11574
+ const range = el.ownerDocument.createRange();
11575
+ let line;
11576
+ const flush = () => {
11577
+ if (line)
11578
+ lines.push({
11579
+ text: text.slice(line.from, line.to),
11580
+ y: (line.top + line.bottom) / 2 - top
11581
+ });
11582
+ };
11583
+ for (const word of text.matchAll(/\S+/g)) {
11584
+ const from = word.index;
11585
+ const to = from + word[0].length;
11586
+ range.setStart(node, from);
11587
+ range.setEnd(node, to);
11588
+ const r = range.getBoundingClientRect();
11589
+ if (line && Math.abs(r.top - line.top) < 1) {
11590
+ line.to = to;
11591
+ line.bottom = Math.max(line.bottom, r.bottom);
11592
+ } else {
11593
+ flush();
11594
+ line = { from, to, top: r.top, bottom: r.bottom };
11595
+ }
11596
+ }
11597
+ flush();
11598
+ }
11599
+ this.lineCache.set(el, { text, width, lines });
11600
+ return lines;
11601
+ }
11524
11602
  curReduced = false;
11525
11603
  curLocks;
11526
11604
  curSelected = 0;
@@ -12994,7 +13072,7 @@ class GpuOverlayManager {
12994
13072
  const morph = panel.morph?.(this.frameNow);
12995
13073
  const shown = morph ? clamp2(morph.progress, 0, 1) : 1;
12996
13074
  const card = morph ? new DOMRect(morph.x, morph.y, morph.w, morph.h) : pRect;
12997
- const cardRadius = morph ? morph.radius : tokens.panelRadius;
13075
+ const cardRadius = morph ? morph.radius : tokens.frameRadius;
12998
13076
  if (morph)
12999
13077
  anyMoving = true;
13000
13078
  const masked = morph !== undefined && this.maskCount < MAX_MASKS;
@@ -13320,12 +13398,11 @@ class GpuOverlayManager {
13320
13398
  this.drawText(this.fit(g.labelEl.textContent ?? "", lr.width, px, tokens.titleWeight), lr.left, cy, withAlpha2(tokens.text, 1 - peek), { px, weight: tokens.titleWeight, track: -0.01 });
13321
13399
  if (g.peek && peek > 0.005)
13322
13400
  this.drawText(this.fit(g.peek.textContent ?? "", lr.width, px, tokens.titleWeight), lr.left, cy, withAlpha2(tokens.text, peek), { px, weight: tokens.titleWeight, track: -0.01 });
13323
- } else
13324
- this.drawText(tokens.groupUpper ? (g.labelEl.textContent ?? "").toUpperCase() : g.labelEl.textContent ?? "", lr.left, lr.top + lr.height * 0.5, tokens.groupColor, {
13325
- px: Math.max(9, tokens.groupSize),
13326
- weight: tokens.titleWeight,
13327
- track: tokens.groupTracking
13328
- });
13401
+ } else {
13402
+ const label = tokens.groupUpper ? (g.labelEl.textContent ?? "").toUpperCase() : g.labelEl.textContent ?? "";
13403
+ const px = Math.max(9, tokens.groupSize);
13404
+ this.drawText(this.domClipped(g.labelEl, label, lr.width) ? this.fit(label, lr.width, px, tokens.titleWeight, undefined, tokens.groupTracking, true) : label, lr.left, lr.top + lr.height * 0.5, tokens.groupColor, { px, weight: tokens.titleWeight, track: tokens.groupTracking });
13405
+ }
13329
13406
  if (!g.title && tokens.groupDivider[3] > 0 && g.element.previousElementSibling)
13330
13407
  this.pushPrim({
13331
13408
  x: gr.left,
@@ -13340,13 +13417,16 @@ class GpuOverlayManager {
13340
13417
  const cs = getComputedStyle(g.desc);
13341
13418
  const left = r.left + (parseFloat(cs.paddingLeft) || 0);
13342
13419
  const w = r.width - (parseFloat(cs.paddingLeft) || 0) - (parseFloat(cs.paddingRight) || 0);
13343
- 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 });
13420
+ const lines = cs.whiteSpace.includes("nowrap") ? [{ text: g.desc.textContent ?? "", y: r.height * 0.5 }] : this.linesOf(g.desc, r.width);
13421
+ for (const line of lines)
13422
+ 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 });
13344
13423
  }
13345
13424
  const fadeIn = (node) => node.parentElement?.closest("[data-fold-motion]") ? Number(getComputedStyle(node).opacity) || 0 : 1;
13346
13425
  if (g.summary && g.summary.getClientRects().length) {
13347
13426
  const r = this.rectOf(g.summary);
13348
13427
  const a = fadeIn(g.summary);
13349
- this.drawText(this.fit(g.summary.textContent ?? "", r.width + 0.5, tokens.textControl, 500), r.left, r.top + r.height * 0.5, withAlpha2(tokens.dim, a), { px: tokens.textControl, weight: 500 });
13428
+ const px = g.title ? tokens.textControl : tokens.textMeta;
13429
+ this.drawText(this.fit(g.summary.textContent ?? "", r.width + 0.5, px, 500), r.left, r.top + r.height * 0.5, withAlpha2(tokens.dim, a), { px, weight: 500 });
13350
13430
  }
13351
13431
  const ctlShown = !g.controls || g.controls.getClientRects().length > 0;
13352
13432
  const headAlpha = this.curAlpha;
@@ -19833,7 +19913,6 @@ var compareCss = `
19833
19913
  :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)}
19834
19914
  .knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);pointer-events:none}
19835
19915
  .knobs-cmp-note:empty{display:none}
19836
- .knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
19837
19916
  .knobs-cmp-timeline-host{flex:none;min-height:0}
19838
19917
  /* The frame strip: Before over After per column, a difference bar under them, a dot where it peaks. */
19839
19918
  .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;
@@ -19853,6 +19932,9 @@ var playerCss = `
19853
19932
  .knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
19854
19933
  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)}
19855
19934
  .knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}
19935
+ /* Mid-scrub, the cached frame under the hand, over the video until it lands. */
19936
+ .knobs-player-preview{position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;pointer-events:none}
19937
+ .knobs-player-preview[hidden]{display:none}
19856
19938
  .knobs-player-line{flex:none;min-height:0}
19857
19939
  /* The timeline's own stylesheet is look.ts's; what the player's row and every timeline share
19858
19940
  about the hand is set here, after it. No hand cursor anywhere (the arrow, or a resize where it
@@ -20659,6 +20741,62 @@ var init_overlay_model = __esm(() => {
20659
20741
  opposite = { top: "bottom", bottom: "top", left: "right", right: "left" };
20660
20742
  });
20661
20743
 
20744
+ // src/ui/tooltip-model.ts
20745
+ function tipSchedule(clock, view) {
20746
+ let shown = null;
20747
+ let pending = null;
20748
+ let timer;
20749
+ let hiddenAt = -Infinity;
20750
+ const cancel = () => {
20751
+ if (pending)
20752
+ clock.clear(timer);
20753
+ pending = null;
20754
+ };
20755
+ const hide = (now = false) => {
20756
+ cancel();
20757
+ if (!shown)
20758
+ return;
20759
+ shown = null;
20760
+ hiddenAt = clock.now();
20761
+ view.hide(now);
20762
+ };
20763
+ return {
20764
+ get shown() {
20765
+ return shown;
20766
+ },
20767
+ get pending() {
20768
+ return pending;
20769
+ },
20770
+ want(tip, options = {}) {
20771
+ if (tip.el === shown?.el || tip.el === pending?.el)
20772
+ return;
20773
+ cancel();
20774
+ const handoff = shown !== null || clock.now() - hiddenAt < HANDOFF_MS;
20775
+ if (handoff || options.now) {
20776
+ shown = tip;
20777
+ view.show(tip, handoff);
20778
+ return;
20779
+ }
20780
+ pending = tip;
20781
+ timer = clock.set(() => {
20782
+ pending = null;
20783
+ if (!view.connected(tip))
20784
+ return;
20785
+ shown = tip;
20786
+ view.show(tip, false);
20787
+ }, options.delay ?? TIP_DELAY_MS);
20788
+ },
20789
+ hide,
20790
+ leave(within) {
20791
+ if (pending && within(pending))
20792
+ cancel();
20793
+ if (shown && within(shown))
20794
+ hide();
20795
+ }
20796
+ };
20797
+ }
20798
+ var HANDOFF_MS = 400, TIP_DELAY_MS = 500;
20799
+
20662
20800
  // src/ui/tooltip.ts
20663
20801
  function tipOf(start) {
20664
20802
  let node = start instanceof Element ? start : null;
@@ -20692,9 +20830,6 @@ function controllerOf(doc) {
20692
20830
  let text;
20693
20831
  let keys;
20694
20832
  let target = null;
20695
- let pending = null;
20696
- let timer = 0;
20697
- let hiddenAt = -Infinity;
20698
20833
  let quiet = null;
20699
20834
  let keyboard = false;
20700
20835
  const build = (look) => {
@@ -20739,41 +20874,32 @@ function controllerOf(doc) {
20739
20874
  else
20740
20875
  motion.show();
20741
20876
  };
20742
- const hide = (now = false) => {
20743
- win.clearTimeout(timer);
20744
- pending = null;
20745
- if (!target)
20746
- return;
20747
- target.removeAttribute("aria-describedby");
20748
- target = null;
20749
- hiddenAt = performance.now();
20750
- if (now)
20751
- motion?.snap(0);
20752
- else
20753
- motion?.hide();
20754
- };
20755
- const want = (found) => {
20877
+ const schedule = tipSchedule({
20878
+ set: (run, ms) => win.setTimeout(run, ms),
20879
+ clear: (handle) => win.clearTimeout(handle),
20880
+ now: () => performance.now()
20881
+ }, {
20882
+ show: ({ el, tip }, snap) => show(el, tip, snap),
20883
+ hide: (now) => {
20884
+ target?.removeAttribute("aria-describedby");
20885
+ target = null;
20886
+ if (now)
20887
+ motion?.snap(0);
20888
+ else
20889
+ motion?.hide();
20890
+ },
20891
+ connected: ({ el }) => el.isConnected
20892
+ });
20893
+ const hide = (now = false) => schedule.hide(now);
20894
+ const want = (found, now = false) => {
20756
20895
  if (!found)
20757
20896
  return hide();
20758
- if (found.el === target || found.el === pending?.el)
20759
- return;
20760
20897
  if (found.el === quiet)
20761
20898
  return;
20762
- win.clearTimeout(timer);
20763
- const handoff = target !== null || performance.now() - hiddenAt < HANDOFF_MS;
20764
- if (handoff) {
20765
- pending = null;
20766
- show(found.el, found.tip, true);
20767
- return;
20768
- }
20769
- if (target)
20770
- hide();
20771
- pending = found;
20772
- timer = win.setTimeout(() => {
20773
- pending = null;
20774
- if (found.el.isConnected)
20775
- show(found.el, found.tip, false);
20776
- }, found.tip.opts.delay ?? 500);
20899
+ schedule.want(found, {
20900
+ now,
20901
+ ...found.tip.opts.delay !== undefined ? { delay: found.tip.opts.delay } : {}
20902
+ });
20777
20903
  };
20778
20904
  doc.addEventListener("pointerover", (e) => {
20779
20905
  if (e.pointerType === "touch")
@@ -20807,11 +20933,10 @@ function controllerOf(doc) {
20807
20933
  doc.addEventListener("focusin", (e) => {
20808
20934
  const found = tipOf(e.target);
20809
20935
  if (found && keyboard && e.target.matches(":focus-visible"))
20810
- want(found);
20936
+ want(found, true);
20811
20937
  });
20812
20938
  doc.addEventListener("focusout", (e) => {
20813
- if (target && target.contains(e.target))
20814
- hide();
20939
+ schedule.leave(({ el }) => el.contains(e.target));
20815
20940
  });
20816
20941
  doc.addEventListener("scroll", () => target && hide(true), true);
20817
20942
  const c = { hide };
@@ -20830,7 +20955,7 @@ function tooltip(el, content, options = {}) {
20830
20955
  c.hide(true);
20831
20956
  };
20832
20957
  }
20833
- var tips, HANDOFF_MS = 400, controllers;
20958
+ var tips, controllers;
20834
20959
  var init_tooltip = __esm(() => {
20835
20960
  init_keys();
20836
20961
  init_layer();
@@ -22246,6 +22371,14 @@ var init_inspector = __esm(() => {
22246
22371
  CAP_MORPH = springLinear(SPRINGS.snappy);
22247
22372
  });
22248
22373
 
22374
+ // src/panel/description-ids.ts
22375
+ function descriptionIds(storeId) {
22376
+ const prefix = `knobs-desc-${++panels2}-${storeId.replace(/[^\w-]/g, "-")}`;
22377
+ let n = 0;
22378
+ return () => `${prefix}-${++n}`;
22379
+ }
22380
+ var panels2 = 0;
22381
+
22249
22382
  // src/panel/search.ts
22250
22383
  function createSearch(doc, options) {
22251
22384
  const element = doc.createElement("div");
@@ -25131,6 +25264,114 @@ var init_lens = __esm(() => {
25131
25264
  };
25132
25265
  });
25133
25266
 
25267
+ // src/panel/active.ts
25268
+ function activeReporter(resolve, report, schedule) {
25269
+ let hover = null;
25270
+ let focus = null;
25271
+ let lead = "pointer";
25272
+ let last = null;
25273
+ let cancel;
25274
+ const flush = () => {
25275
+ cancel = undefined;
25276
+ const [first, second] = lead === "pointer" ? [hover, focus] : [focus, hover];
25277
+ const at = resolve(first) ?? resolve(second);
25278
+ if (same3(at, last))
25279
+ return;
25280
+ last = at;
25281
+ report(at);
25282
+ };
25283
+ const touch = () => {
25284
+ cancel ??= schedule(flush);
25285
+ };
25286
+ return {
25287
+ pointer(target) {
25288
+ hover = target;
25289
+ if (target !== null)
25290
+ lead = "pointer";
25291
+ touch();
25292
+ },
25293
+ focus(target) {
25294
+ focus = target;
25295
+ if (target !== null)
25296
+ lead = "focus";
25297
+ touch();
25298
+ },
25299
+ dispose() {
25300
+ cancel?.();
25301
+ cancel = undefined;
25302
+ hover = focus = null;
25303
+ if (last !== null)
25304
+ report(last = null);
25305
+ }
25306
+ };
25307
+ }
25308
+ function activeEvents(reporter, where) {
25309
+ let inMenu = false;
25310
+ return {
25311
+ over(e) {
25312
+ reporter.pointer(e.target);
25313
+ },
25314
+ move(e) {
25315
+ if (e.movementX || e.movementY)
25316
+ reporter.pointer(e.target);
25317
+ },
25318
+ out(e) {
25319
+ if (where(e.relatedTarget) !== "inside")
25320
+ reporter.pointer(null);
25321
+ },
25322
+ focusIn(e) {
25323
+ const at = where(e.target);
25324
+ inMenu = at === "menu";
25325
+ if (at === "inside")
25326
+ reporter.focus(e.target);
25327
+ else if (at === "outside")
25328
+ reporter.focus(null);
25329
+ },
25330
+ focusOut(e) {
25331
+ if (e.relatedTarget !== null)
25332
+ return;
25333
+ const at = where(e.target);
25334
+ if (at === "inside" || at === "menu" && inMenu) {
25335
+ inMenu = false;
25336
+ reporter.focus(null);
25337
+ }
25338
+ }
25339
+ };
25340
+ }
25341
+ function watchActive(root, resolve, report) {
25342
+ const doc = root.ownerDocument;
25343
+ const win = doc.defaultView;
25344
+ const reporter = activeReporter(resolve, report, (flush) => {
25345
+ const id = win.requestAnimationFrame(flush);
25346
+ return () => win.cancelAnimationFrame(id);
25347
+ });
25348
+ const events = activeEvents(reporter, (node) => node instanceof Node && root.contains(node) ? "inside" : node instanceof Element && node.closest(".knobs-menu") ? "menu" : "outside");
25349
+ const el = (node) => node;
25350
+ const over = (e) => events.over({ target: el(e.target) });
25351
+ const move = (e) => events.move({ target: el(e.target), movementX: e.movementX, movementY: e.movementY });
25352
+ const out = (e) => events.out({ relatedTarget: el(e.relatedTarget) });
25353
+ const focusIn = (e) => events.focusIn({ target: el(e.target) });
25354
+ const focusOut = (e) => events.focusOut({ target: el(e.target), relatedTarget: el(e.relatedTarget) });
25355
+ root.addEventListener("pointerover", over);
25356
+ root.addEventListener("pointermove", move);
25357
+ root.addEventListener("pointerout", out);
25358
+ doc.addEventListener("focusin", focusIn);
25359
+ doc.addEventListener("focusout", focusOut);
25360
+ if (doc.activeElement && root.contains(doc.activeElement))
25361
+ reporter.focus(doc.activeElement);
25362
+ return {
25363
+ dispose() {
25364
+ root.removeEventListener("pointerover", over);
25365
+ root.removeEventListener("pointermove", move);
25366
+ root.removeEventListener("pointerout", out);
25367
+ doc.removeEventListener("focusin", focusIn);
25368
+ doc.removeEventListener("focusout", focusOut);
25369
+ reporter.dispose();
25370
+ }
25371
+ };
25372
+ }
25373
+ var same3 = (a, b) => a === b || a !== null && b !== null && a.group === b.group && a.key === b.key;
25374
+
25134
25375
  // src/panel/has.ts
25135
25376
  function trackHas(root) {
25136
25377
  const doc = root.ownerDocument;
@@ -25376,7 +25617,7 @@ function morphFromChip(chip, root, wake, restored) {
25376
25617
  if (shape)
25377
25618
  return shape;
25378
25619
  const cs = getComputedStyle(root);
25379
- const r = parseFloat(cs.getPropertyValue("--kv-radius-panel"));
25620
+ const r = parseFloat(cs.getPropertyValue("--kv-radius-frame"));
25380
25621
  if (Number.isFinite(r))
25381
25622
  panelRadius = r;
25382
25623
  shape = doc.createElement("div");
@@ -25592,6 +25833,7 @@ function foldMemory(id, storage) {
25592
25833
  const next = {
25593
25834
  sections: { ...data.sections },
25594
25835
  groups: { ...data.groups },
25836
+ ...data.subsections ? { subsections: { ...data.subsections } } : {},
25595
25837
  [kind]: { ...data[kind], [at]: folded }
25596
25838
  };
25597
25839
  try {
@@ -25603,6 +25845,18 @@ function foldMemory(id, storage) {
25603
25845
  var init_folds = () => {};
25604
25846
 
25605
25847
  // src/panel/position.ts
25848
+ function savedPosition(left, top, width, view) {
25849
+ const right = left + width / 2 > view / 2;
25850
+ return right ? { x: view - left - width, y: top, right } : { x: left, y: top };
25851
+ }
25852
+ function readPosition(raw) {
25853
+ try {
25854
+ const saved = JSON.parse(raw ?? "null");
25855
+ if (saved && Number.isFinite(saved.x) && Number.isFinite(saved.y))
25856
+ return { x: saved.x, y: saved.y, ...saved.right === true ? { right: true } : {} };
25857
+ } catch {}
25858
+ return;
25859
+ }
25606
25860
  function positionPanel(panel, header, anchor, id, storage) {
25607
25861
  const doc = panel.ownerDocument;
25608
25862
  const win = doc.defaultView;
@@ -25612,16 +25866,14 @@ function positionPanel(panel, header, anchor, id, storage) {
25612
25866
  let x = bounds.right + w <= win.innerWidth - 8 ? bounds.right + 8 : clampX(bounds.left - w - 8);
25613
25867
  let y = Math.max(8, bounds.top);
25614
25868
  const key = storageKey(id, "position");
25615
- let hasSaved = false;
25869
+ let saved;
25616
25870
  try {
25617
- const saved = JSON.parse(storage()?.getItem(key) ?? "null");
25618
- if (saved && Number.isFinite(saved.x) && Number.isFinite(saved.y)) {
25619
- x = saved.x;
25620
- y = saved.y;
25621
- hasSaved = true;
25622
- }
25871
+ saved = readPosition(storage()?.getItem(key));
25623
25872
  } catch {}
25624
- if (!hasSaved) {
25873
+ if (saved) {
25874
+ x = leftOf(saved, w, win.innerWidth);
25875
+ y = saved.y;
25876
+ } else {
25625
25877
  const others = [...doc.querySelectorAll(".knobs-panel:not(.knobs-inline)")].filter((el) => el !== panel).map((el) => el.getBoundingClientRect());
25626
25878
  if (others.length > 0) {
25627
25879
  const overlaps = (cx, cy) => others.some((r) => Math.abs(cx - r.left) < w - 24 && Math.abs(cy - r.top) < 120);
@@ -25645,6 +25897,8 @@ function positionPanel(panel, header, anchor, id, storage) {
25645
25897
  }
25646
25898
  let shown = { x, y };
25647
25899
  const constrain = () => {
25900
+ if (saved && !drag)
25901
+ x = leftOf(saved, panel.offsetWidth, win.innerWidth);
25648
25902
  shown = {
25649
25903
  x: Math.max(8, Math.min(x, win.innerWidth - panel.offsetWidth - 8)),
25650
25904
  y: Math.max(8, Math.min(y, win.innerHeight - panel.offsetHeight - 8))
@@ -25658,22 +25912,35 @@ function positionPanel(panel, header, anchor, id, storage) {
25658
25912
  return;
25659
25913
  event.preventDefault();
25660
25914
  drag = { id: event.pointerId, x: event.clientX, y: event.clientY, left: shown.x, top: shown.y };
25661
- header.setPointerCapture(event.pointerId);
25662
25915
  };
25663
25916
  header.onpointermove = (event) => {
25664
25917
  if (!drag || event.pointerId !== drag.id)
25665
25918
  return;
25919
+ if (!("dragging" in panel.dataset)) {
25920
+ if (!(event.buttons & 1))
25921
+ return void (drag = undefined);
25922
+ if (Math.hypot(event.clientX - drag.x, event.clientY - drag.y) <= 2)
25923
+ return;
25924
+ panel.dataset.dragging = "";
25925
+ header.setPointerCapture(event.pointerId);
25926
+ }
25666
25927
  x = drag.left + event.clientX - drag.x;
25667
25928
  y = drag.top + event.clientY - drag.y;
25668
25929
  constrain();
25669
25930
  };
25931
+ header.onpointerup = header.onpointercancel = () => {
25932
+ if (drag && !("dragging" in panel.dataset))
25933
+ drag = undefined;
25934
+ };
25670
25935
  header.onlostpointercapture = () => {
25671
25936
  if (!drag)
25672
25937
  return;
25673
25938
  drag = undefined;
25939
+ delete panel.dataset.dragging;
25674
25940
  ({ x, y } = shown);
25941
+ saved = savedPosition(x, y, panel.offsetWidth, win.innerWidth);
25675
25942
  try {
25676
- storage()?.setItem(key, JSON.stringify({ x, y }));
25943
+ storage()?.setItem(key, JSON.stringify(saved));
25677
25944
  } catch {}
25678
25945
  };
25679
25946
  const observer = new ResizeObserver(constrain);
@@ -25688,6 +25955,7 @@ function positionPanel(panel, header, anchor, id, storage) {
25688
25955
  }
25689
25956
  };
25690
25957
  }
25958
+ var leftOf = (saved, width, view) => saved.right ? view - width - saved.x : saved.x;
25691
25959
  var init_position = () => {};
25692
25960
 
25693
25961
  // src/panel/dock.ts
@@ -25831,7 +26099,7 @@ function createPresence(root, options) {
25831
26099
  return;
25832
26100
  const look = presenceLook(spring.x, inline, reduced);
25833
26101
  const r = root.getBoundingClientRect();
25834
- radius ??= parseFloat(win.getComputedStyle(root).getPropertyValue("--kv-radius-panel")) || 12;
26102
+ radius ??= parseFloat(win.getComputedStyle(root).getPropertyValue("--kv-radius-frame")) || 12;
25835
26103
  const s = look.scale;
25836
26104
  const ox = r.left + r.width / 2;
25837
26105
  const oy = inline ? r.top : r.top + r.height / 2;
@@ -25879,6 +26147,20 @@ var init_presence = __esm(() => {
25879
26147
  LOOK = ["opacity", "filter", "scale", "transform-origin"];
25880
26148
  });
25881
26149
 
26150
+ // src/panel/summary.ts
26151
+ function panelFoldSummary(rows, preset) {
26152
+ if (rows.length === 1 && rows[0].value)
26153
+ return rows[0].value;
26154
+ const lead = preset ?? rows.find((r) => r.changed && r.value)?.value ?? rows.find((r) => r.choice && r.value)?.value;
26155
+ const changed = rows.filter((r) => r.changed).length;
26156
+ return [lead, changed ? `${changed} changed` : ""].filter(Boolean).join(" · ");
26157
+ }
26158
+ var baseSignature = (controls) => JSON.stringify([
26159
+ controls.activePreset(),
26160
+ Object.keys(controls.scopes).map((s) => controls.activePreset(s)),
26161
+ Object.keys(controls.groupPresets).map((g) => controls.activeGroupPreset(g))
26162
+ ]);
26163
+
25882
26164
  // src/panel/panel.ts
25883
26165
  function readableTitle(id) {
25884
26166
  if (/\s/.test(id))
@@ -26526,19 +26808,24 @@ function buildPanel(controls, host, options) {
26526
26808
  foldTile(card, next && !filterEl.value.trim(), animate);
26527
26809
  else
26528
26810
  applyVisibility(animate ? "reveal" : "instant");
26529
- if (page) {
26530
- const tile = card.parentElement;
26531
- tile.toggleAttribute("data-folded", next);
26811
+ const marked = page ? card.parentElement : head;
26812
+ if (marked) {
26813
+ marked.toggleAttribute("data-folded", next);
26532
26814
  if (animate) {
26533
- tile.setAttribute("data-fold-motion", "");
26534
- clearTimeout(foldMotions.get(tile));
26535
- foldMotions.set(tile, setTimeout(() => tile.removeAttribute("data-fold-motion"), 400));
26815
+ marked.setAttribute("data-fold-motion", "");
26816
+ clearTimeout(foldMotions.get(marked));
26817
+ foldMotions.set(marked, setTimeout(() => marked.removeAttribute("data-fold-motion"), 400));
26536
26818
  }
26819
+ }
26820
+ if (page) {
26537
26821
  if (remember)
26538
26822
  foldsKept?.set("groups", group, next);
26539
- refreshFolds();
26540
- } else if (sessionKept)
26823
+ } else if (sessionKept) {
26541
26824
  writeSession(controls.id, { groups: { ...readSession(controls.id).groups, [group]: next } });
26825
+ if (remember)
26826
+ panelFolds?.set("groups", group, next);
26827
+ }
26828
+ refreshFolds();
26542
26829
  positioned?.constrain();
26543
26830
  wakeGpu();
26544
26831
  }
@@ -26586,8 +26873,7 @@ function buildPanel(controls, host, options) {
26586
26873
  gRandom.dataset.hint = `${title} random`;
26587
26874
  gRandom.querySelector(".knobs-icon")?.replaceWith(gDie.node);
26588
26875
  gRandom.onclick = (event) => run(() => rerollGroup(groupName, event.altKey));
26589
- const rollable = Object.values(controls.schema).some((f) => f.group === groupName && hasValue(f));
26590
- if (!bound("random") && !bound("mutate") || !rollable)
26876
+ if (!bound("random") && !bound("mutate"))
26591
26877
  gRandom.hidden = true;
26592
26878
  groupDice.set(groupName, { button: gRandom, die: gDie });
26593
26879
  const presets = page ? (controls.groupPresets[groupName] ?? []).slice(0, 9) : [];
@@ -26695,21 +26981,22 @@ function buildPanel(controls, host, options) {
26695
26981
  describeGroup(groupName);
26696
26982
  }
26697
26983
  if (foldAt !== undefined) {
26698
- if (page) {
26699
- summary = el("span");
26700
- summary.className = "knobs-group-summary";
26984
+ summary = el("span");
26985
+ summary.className = "knobs-group-summary";
26986
+ if (page)
26701
26987
  gRight.append(summary);
26702
- groupSummaries.set(groupName, summary);
26703
- }
26988
+ groupSummaries.set(groupName, summary);
26704
26989
  caret = caretSvg();
26705
26990
  gRight.append(caret);
26706
26991
  gHead.dataset.foldable = "";
26707
26992
  gHead.tabIndex = -1;
26708
26993
  gHead.setAttribute("role", "button");
26709
26994
  gHead.setAttribute("aria-label", title);
26710
- const folded = (page ? foldsKept?.get("groups", groupName) : session.groups?.[groupName]) ?? foldAt;
26995
+ const folded = (page ? foldsKept?.get("groups", groupName) : session.groups?.[groupName] ?? panelFolds?.get("groups", groupName)) ?? foldAt;
26711
26996
  folds.set(groupName, folded);
26712
26997
  gHead.setAttribute("aria-expanded", String(!folded));
26998
+ if (!page)
26999
+ gHead.toggleAttribute("data-folded", folded);
26713
27000
  gHead.addEventListener("click", (event) => {
26714
27001
  if (event.detail > 1 || event.target.closest("button"))
26715
27002
  return;
@@ -26721,8 +27008,13 @@ function buildPanel(controls, host, options) {
26721
27008
  gTitle.className = "knobs-group-title";
26722
27009
  gTitle.append(gLabel, gPeek);
26723
27010
  gHead.append(gTitle, gRight);
26724
- } else
26725
- gHead.append(gLabel, gRight);
27011
+ } else {
27012
+ const desc = el("span");
27013
+ desc.className = "knobs-group-desc";
27014
+ groupDescs.set(groupName, desc);
27015
+ describeGroup(groupName);
27016
+ gHead.append(gLabel, desc, ...summary ? [summary] : [], gRight);
27017
+ }
26726
27018
  gpuGroups.push({
26727
27019
  element: gHead,
26728
27020
  labelEl: gLabel,
@@ -26776,6 +27068,7 @@ function buildPanel(controls, host, options) {
26776
27068
  const session = remember ? readSession(controls.id) : {};
26777
27069
  const foldPort = page?.foldStorage !== undefined ? page.foldStorage : controls.persist === "none" ? null : controls.storage;
26778
27070
  const foldsKept = page && foldPort ? foldMemory(controls.id, () => foldPort) : null;
27071
+ const panelFolds = !page && remember && foldPort ? foldMemory(controls.id, () => foldPort) : null;
26779
27072
  const tracks = new Map;
26780
27073
  const toolbars = new Map;
26781
27074
  const keyedActions = new Map;
@@ -26816,7 +27109,8 @@ function buildPanel(controls, host, options) {
26816
27109
  label,
26817
27110
  ...duration.group !== undefined ? { group: duration.group } : {},
26818
27111
  ...duration.section !== undefined ? { section: duration.section } : {},
26819
- ...duration.shortcut !== undefined ? { shortcut: duration.shortcut } : {}
27112
+ ...duration.shortcut !== undefined ? { shortcut: duration.shortcut } : {},
27113
+ ...duration.description !== undefined ? { description: duration.description } : {}
26820
27114
  },
26821
27115
  parts: { duration, bounce },
26822
27116
  group: duration.group,
@@ -27326,6 +27620,41 @@ function buildPanel(controls, host, options) {
27326
27620
  return;
27327
27621
  slot.replaceChildren(...node ? [node] : []);
27328
27622
  }
27623
+ const descriptions = el("div");
27624
+ descriptions.hidden = true;
27625
+ root.append(descriptions);
27626
+ const described = new Map;
27627
+ const descriptionId = descriptionIds(controls.id);
27628
+ function describe(row) {
27629
+ const text = row.field.description?.trim() ?? "";
27630
+ const had = described.get(row);
27631
+ if ((had?.text ?? "") === text)
27632
+ return;
27633
+ const inputs = row.choice ? [row.choice.seg, row.choice.menu] : [row.input];
27634
+ if (had) {
27635
+ had.untip();
27636
+ had.node.remove();
27637
+ described.delete(row);
27638
+ for (const input of inputs)
27639
+ input.removeAttribute("aria-describedby");
27640
+ }
27641
+ if (!text)
27642
+ return;
27643
+ const node = el("span", text);
27644
+ node.id = descriptionId();
27645
+ descriptions.append(node);
27646
+ for (const input of inputs)
27647
+ input.setAttribute("aria-describedby", node.id);
27648
+ described.set(row, { text, node, untip: tooltip(row.row, text) });
27649
+ }
27650
+ const undescribe = (row) => {
27651
+ const had = described.get(row);
27652
+ if (!had)
27653
+ return;
27654
+ had.untip();
27655
+ had.node.remove();
27656
+ described.delete(row);
27657
+ };
27329
27658
  const fieldFlag = (field, name) => {
27330
27659
  const v = field[name];
27331
27660
  return typeof v === "function" ? Boolean(v(controls.get())) : Boolean(v);
@@ -27371,6 +27700,7 @@ function buildPanel(controls, host, options) {
27371
27700
  wakeGpu();
27372
27701
  };
27373
27702
  const dropGpu = (row) => {
27703
+ undescribe(row);
27374
27704
  row.dispose?.();
27375
27705
  row.slider?.dispose();
27376
27706
  row.editor?.dispose();
@@ -27429,11 +27759,13 @@ function buildPanel(controls, host, options) {
27429
27759
  if (existing && existing.shape === shapeOf(spec)) {
27430
27760
  kept.delete(spec.id);
27431
27761
  existing.retune(spec);
27762
+ describe(existing);
27432
27763
  next.push(existing);
27433
27764
  continue;
27434
27765
  }
27435
27766
  const row = buildRow(spec);
27436
27767
  decorate(row);
27768
+ describe(row);
27437
27769
  gpuRows.push(...row.gpu);
27438
27770
  entering.add(row);
27439
27771
  next.push(row);
@@ -27540,6 +27872,8 @@ function buildPanel(controls, host, options) {
27540
27872
  motion.push({ node: r.row, show: true });
27541
27873
  }
27542
27874
  }
27875
+ for (const group of groupDescs.keys())
27876
+ describeGroup(group);
27543
27877
  for (const [id, sh] of subheads)
27544
27878
  if (!usedSubheads.has(id)) {
27545
27879
  sh.el.remove();
@@ -27598,8 +27932,7 @@ function buildPanel(controls, host, options) {
27598
27932
  motion.forEach((m, i) => present(m.node, m.show, i * stagger, m.after));
27599
27933
  }
27600
27934
  applyVisibility(animate ? "reveal" : "instant");
27601
- if (page)
27602
- refreshFolds();
27935
+ refreshFolds();
27603
27936
  if (focused && root.contains(focused) && doc.activeElement !== focused && focused.isConnected)
27604
27937
  focused.focus({ preventScroll: true });
27605
27938
  const gone = focused ? leaving.find((r) => r.row.contains(focused)) : undefined;
@@ -27636,7 +27969,7 @@ function buildPanel(controls, host, options) {
27636
27969
  const caret = caretSvg();
27637
27970
  node.append(label, caret);
27638
27971
  if (!subFolds.has(id))
27639
- subFolds.set(id, session.subsections?.[id] ?? options.subsectionFolded?.[name] ?? false);
27972
+ subFolds.set(id, session.subsections?.[id] ?? panelFolds?.get("subsections", id) ?? options.subsectionFolded?.[name] ?? false);
27640
27973
  node.setAttribute("aria-expanded", String(!subFolds.get(id)));
27641
27974
  node.addEventListener("click", (event) => {
27642
27975
  if (event.detail < 2)
@@ -27670,10 +28003,12 @@ function buildPanel(controls, host, options) {
27670
28003
  if (next && rows.some((r) => r.subhead === id && r.row.contains(doc.activeElement)))
27671
28004
  sh.el.focus({ preventScroll: true, focusVisible: false });
27672
28005
  applyVisibility("reveal");
27673
- if (remember)
28006
+ if (remember) {
27674
28007
  writeSession(controls.id, {
27675
28008
  subsections: { ...readSession(controls.id).subsections, [id]: next }
27676
28009
  });
28010
+ panelFolds?.set("subsections", id, next);
28011
+ }
27677
28012
  }
27678
28013
  const groupTab = (group) => {
27679
28014
  for (const f of Object.values(controls.schema))
@@ -27759,7 +28094,7 @@ function buildPanel(controls, host, options) {
27759
28094
  const searching = () => search?.filtering() ?? false;
27760
28095
  const rowMatches = (r) => {
27761
28096
  const query = search.query();
27762
- if (query && !matchesFilter(query, r.display, r.key, r.group, r.group && groupTitle(r.group), r.field.subsection))
28097
+ 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))
27763
28098
  return false;
27764
28099
  for (const chip of search.active())
27765
28100
  if (!r.keys.some((k) => chipTests.get(chip)?.(k)))
@@ -27881,6 +28216,10 @@ function buildPanel(controls, host, options) {
27881
28216
  };
27882
28217
  const keysOf = (indices) => indices.flatMap((i) => rows[i]?.keys ?? []);
27883
28218
  const groupKeys = (group) => rows.filter((r) => r.group === group).flatMap((r) => r.keys);
28219
+ const mayRoll = (key) => {
28220
+ const field = controls.schema[key];
28221
+ return Boolean(field && hasValue(field) && field.random !== false);
28222
+ };
27884
28223
  const rerollGroup = (group, nudge) => {
27885
28224
  const roller = groupDice.get(group)?.die;
27886
28225
  if (!roller)
@@ -28027,22 +28366,29 @@ function buildPanel(controls, host, options) {
28027
28366
  apply.setAttribute("aria-label", "Import JSON");
28028
28367
  apply.hidden = true;
28029
28368
  const copy = async (value, inPlace = true) => {
28030
- try {
28031
- await doc.defaultView.navigator.clipboard.writeText(value);
28369
+ const done = () => {
28032
28370
  if (inPlace && !copyButton.hidden && headerShown())
28033
28371
  copied();
28034
28372
  else
28035
28373
  say("Copied.");
28374
+ };
28375
+ try {
28376
+ await doc.defaultView.navigator.clipboard.writeText(value);
28377
+ done();
28036
28378
  } catch {
28037
- text.value = value;
28038
- if (page)
28039
- togglePopover(true);
28040
- reveal(text, true);
28041
- apply.hidden = true;
28042
- positioned?.constrain();
28043
- text.focus();
28044
- text.select();
28045
- say("Clipboard unavailable. Copy the selected text.", 6000);
28379
+ if (copyByCommand(doc, value))
28380
+ done();
28381
+ else {
28382
+ text.value = value;
28383
+ if (page)
28384
+ togglePopover(true);
28385
+ reveal(text, true);
28386
+ apply.hidden = true;
28387
+ positioned?.constrain();
28388
+ text.focus();
28389
+ text.select();
28390
+ say("Clipboard unavailable. Copy the selected text.", 6000);
28391
+ }
28046
28392
  }
28047
28393
  wakeGpu();
28048
28394
  };
@@ -28157,11 +28503,6 @@ function buildPanel(controls, host, options) {
28157
28503
  wakeGpu();
28158
28504
  }
28159
28505
  let shownBase = "";
28160
- const baseSignature = () => JSON.stringify([
28161
- controls.activePreset(),
28162
- Object.keys(controls.scopes).map((s) => controls.activePreset(s)),
28163
- Object.keys(controls.groupPresets).map((g) => controls.activeGroupPreset(g))
28164
- ]);
28165
28506
  function update() {
28166
28507
  if (!controls.enabled) {
28167
28508
  close?.();
@@ -28188,6 +28529,7 @@ function buildPanel(controls, host, options) {
28188
28529
  }
28189
28530
  const stagger = initialized ? cascade(changedCount) : 0;
28190
28531
  const moving = initialized ? rows.filter(changed) : rows;
28532
+ const rebasing = baseSignature(controls) !== shownBase;
28191
28533
  updateStrips();
28192
28534
  batchMeasures(() => {
28193
28535
  for (const row of moving) {
@@ -28200,7 +28542,7 @@ function buildPanel(controls, host, options) {
28200
28542
  primeSegPill(chips.seg, chips.buttons);
28201
28543
  showValues(values, moving, stagger, landing);
28202
28544
  });
28203
- if (moving.length || !initialized)
28545
+ if (moving.length || !initialized || rebasing)
28204
28546
  refreshFolds();
28205
28547
  shown = values;
28206
28548
  initialized = true;
@@ -28226,7 +28568,7 @@ function buildPanel(controls, host, options) {
28226
28568
  const fresh = (r) => r.row.dataset.shown === undefined;
28227
28569
  const now = performance.now();
28228
28570
  let turn = 0;
28229
- const based = baseSignature();
28571
+ const based = baseSignature(controls);
28230
28572
  const rebased = based !== shownBase;
28231
28573
  shownBase = based;
28232
28574
  for (const row of moving) {
@@ -28294,10 +28636,17 @@ function buildPanel(controls, host, options) {
28294
28636
  }
28295
28637
  if (randomButton.disabled !== (unlocked === 0))
28296
28638
  randomButton.disabled = unlocked === 0;
28639
+ const rolls = (bound("random") || bound("mutate")) && shows("random");
28640
+ const headerHidden = !rolls || !rows.some((r) => r.keys.some(mayRoll));
28641
+ if (randomButton.hidden !== headerHidden)
28642
+ randomButton.hidden = headerHidden;
28297
28643
  for (const [group, { button }] of groupDice) {
28298
28644
  const off = !options.groupRandom && !groupKeys(group).some((k) => controls.canRoll(k));
28299
28645
  if (button.disabled !== off)
28300
28646
  button.disabled = off;
28647
+ const hidden = !bound("random") && !bound("mutate") || !options.groupRandom && !groupKeys(group).some(mayRoll);
28648
+ if (button.hidden !== hidden)
28649
+ button.hidden = hidden;
28301
28650
  }
28302
28651
  if (root.hasAttribute("data-comparing") !== controls.comparing)
28303
28652
  root.toggleAttribute("data-comparing", controls.comparing);
@@ -28482,7 +28831,7 @@ function buildPanel(controls, host, options) {
28482
28831
  layout.change(() => {
28483
28832
  layout.openFolds(Boolean(query));
28484
28833
  for (const r of rows) {
28485
- const 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));
28834
+ const 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);
28486
28835
  if (r.row.hidden === show)
28487
28836
  r.row.hidden = !show;
28488
28837
  }
@@ -28531,15 +28880,49 @@ function buildPanel(controls, host, options) {
28531
28880
  keys: rows.filter((r) => r.section === section).flatMap((r) => r.keys)
28532
28881
  });
28533
28882
  const changedText = (n) => n && marks ? `${n} changed` : "";
28883
+ const valueText = (r, values) => {
28884
+ const f = r.field;
28885
+ const v = r.value(values);
28886
+ const num = (n) => String(Number(n.toFixed(2)));
28887
+ switch (f.kind) {
28888
+ case "choice":
28889
+ return f.labels?.[v] ?? String(v);
28890
+ case "toggle":
28891
+ return v ? "On" : "Off";
28892
+ case "knob":
28893
+ return `${v.toFixed(f.decimals ?? decimals(f.step))}${f.unit ? ` ${f.unit}` : ""}`;
28894
+ case "spring": {
28895
+ const { duration, bounce } = v;
28896
+ return `${num(duration)} s${bounce ? `, bounce ${num(bounce)}` : ""}`;
28897
+ }
28898
+ case "color":
28899
+ return colorHex(v);
28900
+ case "text":
28901
+ return String(v);
28902
+ default:
28903
+ return "";
28904
+ }
28905
+ };
28906
+ const groupSummary = (group) => {
28907
+ const info = groupInfo(group);
28908
+ if (options.groupSummary)
28909
+ return options.groupSummary(group, info);
28910
+ if (page)
28911
+ return [info.preset, changedText(info.changed)].filter(Boolean).join(" · ") || null;
28912
+ const values = controls.get();
28913
+ const own = rows.filter((r) => r.group === group && !r.leaving && r.keys.length);
28914
+ return panelFoldSummary(own.map((r) => ({
28915
+ value: valueText(r, values),
28916
+ changed: marks && r.keys.some((k) => controls.isModified(k)),
28917
+ choice: r.field.kind === "choice"
28918
+ })), info.preset) || null;
28919
+ };
28534
28920
  function refreshFolds() {
28921
+ for (const [group, node] of groupSummaries)
28922
+ if (folds.get(group))
28923
+ putSummary(node, groupSummary(group));
28535
28924
  if (!layout || !page)
28536
28925
  return;
28537
- for (const [group, node] of groupSummaries) {
28538
- if (!folds.get(group))
28539
- continue;
28540
- const info = groupInfo(group);
28541
- putSummary(node, page.groupSummary ? page.groupSummary(group, info) : [info.preset, changedText(info.changed)].filter(Boolean).join(" · ") || null);
28542
- }
28543
28926
  for (const s of layout.sectionList()) {
28544
28927
  const node = s.label?.querySelector(":scope > .knobs-section-summary");
28545
28928
  if (!node || !s.folded)
@@ -29052,7 +29435,20 @@ function buildPanel(controls, host, options) {
29052
29435
  queueScope();
29053
29436
  });
29054
29437
  };
29438
+ const onPressOutside = (event) => {
29439
+ const target = event.target;
29440
+ if (page || !target || root.contains(target))
29441
+ return;
29442
+ if (target.closest?.(".knobs-root,.knobs-layers"))
29443
+ return;
29444
+ setTimeout(() => {
29445
+ const active = doc.activeElement;
29446
+ if (active && root.contains(active))
29447
+ active.blur();
29448
+ });
29449
+ };
29055
29450
  doc.addEventListener("pointerdown", onPress, true);
29451
+ doc.addEventListener("pointerdown", onPressOutside, true);
29056
29452
  doc.addEventListener("pointerup", onRelease, true);
29057
29453
  doc.addEventListener("pointercancel", onRelease, true);
29058
29454
  root.addEventListener("keydown", () => {
@@ -30057,6 +30453,16 @@ function buildPanel(controls, host, options) {
30057
30453
  attributes: true,
30058
30454
  attributeFilter: ["data-knobs-renderer", "data-theme", "data-appearance", "class", "style"]
30059
30455
  });
30456
+ const onActive = options.onActive;
30457
+ const active = onActive ? watchActive(root, (node) => {
30458
+ if (!node || !root.contains(node))
30459
+ return null;
30460
+ const r = rows.find((r) => !r.leaving && r.row.contains(node));
30461
+ if (r)
30462
+ return r.group === undefined ? null : { group: r.group, key: r.key };
30463
+ const card = node.closest("[data-group]");
30464
+ return card && root.contains(card) ? { group: card.dataset.group } : null;
30465
+ }, onActive) : undefined;
30060
30466
  const panel = {
30061
30467
  element: root,
30062
30468
  presetStrip,
@@ -30144,9 +30550,11 @@ function buildPanel(controls, host, options) {
30144
30550
  lens.dispose();
30145
30551
  win?.cancelAnimationFrame(lensFrame);
30146
30552
  doc.removeEventListener("pointerdown", onPress, true);
30553
+ doc.removeEventListener("pointerdown", onPressOutside, true);
30147
30554
  doc.removeEventListener("pointerup", onRelease, true);
30148
30555
  doc.removeEventListener("pointercancel", onRelease, true);
30149
30556
  crumbs?.dispose();
30557
+ active?.dispose();
30150
30558
  clearTimeout(escTimer);
30151
30559
  win?.removeEventListener("blur", queueScope);
30152
30560
  win?.removeEventListener("focus", queueScope);
@@ -30350,6 +30758,24 @@ function buildPanel(controls, host, options) {
30350
30758
  }
30351
30759
  return pageApi;
30352
30760
  }
30761
+ function copyByCommand(doc, value) {
30762
+ const was = doc.activeElement;
30763
+ const area = doc.createElement("textarea");
30764
+ area.value = value;
30765
+ area.setAttribute("readonly", "");
30766
+ area.style.cssText = "position:fixed;top:0;left:0;opacity:0;pointer-events:none";
30767
+ doc.body.append(area);
30768
+ area.select();
30769
+ let ok = false;
30770
+ try {
30771
+ ok = doc.execCommand("copy");
30772
+ } catch {
30773
+ ok = false;
30774
+ }
30775
+ area.remove();
30776
+ was?.focus({ preventScroll: true });
30777
+ return ok;
30778
+ }
30353
30779
  function createPanel(controls, anchor, close, options) {
30354
30780
  return buildPanel(controls, anchor.ownerDocument.body, {
30355
30781
  ...options,
@@ -30382,6 +30808,7 @@ var init_panel = __esm(() => {
30382
30808
  init_readout();
30383
30809
  init_readout_row();
30384
30810
  init_toast();
30811
+ init_tooltip();
30385
30812
  init_tabs();
30386
30813
  init_tab_transition();
30387
30814
  init_choice();
@@ -30528,7 +30955,7 @@ function attach(controls, element, options = {}) {
30528
30955
  const open = async (restored = false) => {
30529
30956
  if (panel) {
30530
30957
  unpress();
30531
- panel.focus();
30958
+ focusPanel(panel);
30532
30959
  return;
30533
30960
  }
30534
30961
  if (leaving?.reopen()) {
@@ -30539,7 +30966,7 @@ function attach(controls, element, options = {}) {
30539
30966
  writeSession(controls.id, { open: true });
30540
30967
  chip.setAttribute("aria-expanded", "true");
30541
30968
  update();
30542
- panel.focus();
30969
+ focusPanel(panel);
30543
30970
  return;
30544
30971
  }
30545
30972
  leaving = undefined;
@@ -30556,7 +30983,7 @@ function attach(controls, element, options = {}) {
30556
30983
  chip.setAttribute("aria-expanded", "true");
30557
30984
  update();
30558
30985
  if (!restored)
30559
- panel.focus();
30986
+ focusPanel(panel);
30560
30987
  } catch (cause) {
30561
30988
  chip.title = String(cause);
30562
30989
  renderFace("Retry knobs");
@@ -30614,6 +31041,7 @@ function attach(controls, element, options = {}) {
30614
31041
  element.removeAttribute("tabindex");
30615
31042
  };
30616
31043
  }
31044
+ var focusPanel = (panel) => panel.element.focus({ preventScroll: true });
30617
31045
  var MODE_KEY = "knobs:mode";
30618
31046
  var MODE_EVENT = "knobs-mode";
30619
31047
  function knobsMode(win) {