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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -777,7 +777,19 @@ mountPanel(station, column, {
777
777
  foldable, starting folded. Its header folds and opens it with a click, Enter or Space; it's a ↑↓
778
778
  stop of its own, ← folds and → opens. `panel.fold(group, folded?)` does it from code. On a
779
779
  [full page](#full-page) the group's tile folds to its header, the cards below follow, and the
780
- filter opens it while it runs.
780
+ filter opens it while it runs. Folded, the header sums the card up on its right, quietly and
781
+ live: a one-row card's value ("CSS orb"), otherwise its matching group preset (or "Custom"),
782
+ else its first changed value, else its first choice, then "· 2 changed". The title keeps its
783
+ room; the summary truncates first. `groupSummary(group, info)` on the mount replaces it, as on a
784
+ page (`info` is `{ preset, changed, keys }`; `null` shows nothing).
785
+ - **Card descriptions.** A group's `groupDescription` (on any of its fields; the first wins) or the
786
+ store's `groupDescriptions` puts one muted line under its card's title, wrapping under the whole
787
+ header. Search matches it.
788
+ - **What's active.** `onActive: (at) => …` hears which card or row the pointer or keyboard focus is
789
+ on: `{ group }` for a card's header or body, `{ group, key }` for a row, `null` for nothing
790
+ (pinned cards and group preset strips count). Whichever moved last wins; it's called at most once
791
+ a frame, never twice with the same value, and once more with `null` when the panel goes. Use it
792
+ to outline the part of your app a card changes. `mountPage` takes it too.
781
793
  - **Group Random by the host.** `groupRandom: (group, mode) => …` runs first for a group's die, `g`
782
794
  and `⌥G` (`mode` is `"random"` or `"mutate"`). Return `true` when the host rolled the group (its
783
795
  own seeded "Randomize <group>"): the die tumbles and knobs leaves the values alone. Return
@@ -838,8 +850,13 @@ panel.tab("emitter") // shows a tab; panel.tab() answers the one showing
838
850
  - **Keys presets leave alone, in place.** `inPresets: false` on a field, or `inPresets: { Motion:
839
851
  false }` on the store for a whole group, keeps presets, scoped presets and saved variants off
840
852
  those keys exactly as they stay off pinned ones (and a preset's chip stays lit when they move),
841
- but the rows stay where they are. A pinned key is one of these, moved to the top.
842
- `controls.inPresets(key)` says whether presets move a key.
853
+ but the rows stay where they are. A pinned key is one of these, moved to the top. The rule, for
854
+ `inPresets: false` and `pinned` groups alike: presets and variants from *other* scopes (the
855
+ panel-wide presets, tab scopes, other groups') leave the group alone, but a scope keyed by the
856
+ group's own name still moves it. So `inPresets: { Motion: false }` with
857
+ `scopedPresets: { Motion: { Snappy } }` gives Motion its own strip that state presets never
858
+ touch; its chips apply, save, match and mark only Motion. A field's own `inPresets: false` stays
859
+ out of every scope. `controls.inPresets(key)` says whether presets from other scopes move a key.
843
860
  - **Presets of a tab's own.** `scopedPresets` gives a tab (or a group) presets that belong to it:
844
861
  `{ Sidebars: { snappy, smooth, calm } }`. Its strip shows only on that tab, and a tab without any
845
862
  shows none. A scoped preset moves only its tab's (or group's) keys, never pinned ones or another
@@ -854,7 +871,8 @@ panel.tab("emitter") // shows a tab; panel.tab() answers the one showing
854
871
  group keeps Shipped, Snappy and Calm beside its knobs. Every strip's default chip reads `defaultLabel`; by scope,
855
872
  `defaultLabel: { "": "Empty", Motion: "Shipped" }` names each on its own. In code, `controls.preset(id, { scope })`, `activePreset(scope)` and
856
873
  `matchingPreset(scope)` take the tab or group, and `controls.scopeOf(key)` says whose presets
857
- move a key (`""` for plain `presets`, undefined when pinned). An active preset saved while the
874
+ move a key (`""` for plain `presets`, the group's id for a pinned or `inPresets: false`
875
+ group with a scope of its own, else undefined when pinned). An active preset saved while the
858
876
  presets were panel-wide comes back on the scope that now has one by that id, and variants saved
859
877
  then start out in each scope they move.
860
878
 
@@ -1093,7 +1111,7 @@ A page with many sections reads better folded. `sectionFolded` starts sections f
1093
1111
 
1094
1112
  Folded things say what's in them. A folded group card trades its presets, reset and die for "Compact · 7 changed" (the matching group preset, or "Custom", then its changed rows; each part only when it applies). A folded section's heading reads "2 changed · 3 cards". `groupSummary(group, info)` and `sectionSummary(section, info)` on the mount replace either (`info` is a `FoldSummaryInfo`: `{ preset, changed, keys }`; return `null` for nothing). Group cards fold as in a panel, by a field's or descriptor's `folded`, or `groupFolded` on the mount, `defineControls` or `createRemoteControls` (both take `sectionFolded` too).
1095
1113
 
1096
- Descriptions add one muted line under a title: `groupDescriptions` and `sectionDescriptions` by id (on the store, remote options or the mount), or `groupDescription` / `sectionDescription` on any field or descriptor (the first that gives one wins). `page.card({ description })` does the same for a custom card. The filter matches descriptions too.
1114
+ Descriptions add one muted line under a title: `groupDescriptions` and `sectionDescriptions` by id (on the store, remote options or the mount), or `groupDescription` / `sectionDescription` on any field or descriptor (the first that gives one wins). `page.card({ description })` does the same for a custom card. The filter matches descriptions too. Group descriptions show in panels as well, wrapping under the card's header.
1097
1115
 
1098
1116
  What the user folds and opens (sections and group cards) is remembered under `knobs:<id>:folds:v1` (`storageKey(id, "folds")`) as `{ sections: Record<string, boolean>, groups: Record<string, boolean> }`, and wins over `sectionFolded` and `groupFolded`. Only the user's folds and those made with `remember` are written; folds for context (`remember: false`, a highlight's `expand`) and the filter's never are. By default they go to the store's own storage, or nowhere with `persist: "none"`. `foldStorage` on the mount always wins and keeps folds only, never values, locks or variants, so a remote store on `storage: null` (the phone's values always win) can still remember them; `null` keeps none.
1099
1117
 
@@ -2033,11 +2051,12 @@ The package has two core exports selected through the standard `development` con
2033
2051
  | `unlock({ panel, sequence?, query?, corner?, persist? })` / `KONAMI` | Lets a production deploy be unlocked by a typed secret, `?knobs` or three corner taps; returns `{ unlocked, unlock(), lock(), dispose() }`. See [Show, hide and unlock](#show-hide-and-unlock). |
2034
2052
  | `action(label, { run, shortcut?, global?, rebind? })` / `flatSpring({ duration, bounce }, meta)` | A button row, with a shortcut its key cap can rebind; two flat number fields drawn as one spring card. `actionShortcut(field)` reads an action's shortcut as `{ key, scope }`. |
2035
2053
  | Field meta `random?`, `disabled?`, `readOnly?`, `folded?`, `tab?`, `subsection?` | On any field or descriptor: `random: false` stays out of Random and Mutate (switches, seeds and key combinations start out; `random: true` brings them in); `disabled` / `readOnly` (boolean or `(values) => boolean`) gate the row; `folded` makes its group foldable, starting folded; `tab` puts the row on a tab; `subsection` puts it under a sub-heading in its group. |
2036
- | Field meta `groupDescription?: string`, `sectionDescription?: string`, `sectionFolded?: boolean` | On any field or descriptor, for pages (the first field of a group or section that gives one wins): a muted line under the card's title or the section's heading; `sectionFolded` makes the section foldable, starting folded. |
2037
- | `mountPanel(controls, container, options?)` (`knobs/panel`) | An inline panel. Beside theme, density, renderer and keys: `preset`, `header` (`false` or `{ title?, buttons? }`), `crumbs` (`true` adds the scope line), `session`, `groupFolded`, `subsectionFolded`, `tabs`, `tab`, `onTab`, `search`, `groupRandom(group, mode)`, `rowAccessory(key)`, `marks`, `haptics`, `detents`, `sound`. The handle adds `panel.fold(group, folded?)`, `panel.tab(id?)`, `panel.search(query?, chips?)` (returns `{ query, chips }`) and `panel.updateAccessories()`. |
2054
+ | Field meta `groupDescription?: string`, `sectionDescription?: string`, `sectionFolded?: boolean` | On any field or descriptor (the first field of a group or section that gives one wins): a muted line under the card's title (in a panel or a page) or a page section's heading; `sectionFolded` makes a page section foldable, starting folded. |
2055
+ | `mountPanel(controls, container, options?)` (`knobs/panel`) | An inline panel. Beside theme, density, renderer and keys: `preset`, `header` (`false` or `{ title?, buttons? }`), `crumbs` (`true` adds the scope line), `session`, `groupFolded`, `groupSummary`, `onActive`, `subsectionFolded`, `tabs`, `tab`, `onTab`, `search`, `groupRandom(group, mode)`, `rowAccessory(key)`, `marks`, `haptics`, `detents`, `sound`. The handle adds `panel.fold(group, folded?)`, `panel.tab(id?)`, `panel.search(query?, chips?)` (returns `{ query, chips }`) and `panel.updateAccessories()`. |
2038
2056
  | `sound` (`knobs/panel`) | The page's sound effects (see **Sound**): `enabled`, `enable(on?)`, `volume`, `setVolume(v)`, `theme`, `setTheme("glass" \| "mech")`, `play(cue \| layers, detail?)`, `output()`, `subscribe(listener)`. Panels play only with `sound` on their mount. |
2039
2057
  | `mountPage(controls, container, options?)` (`knobs/panel`) | A full page of group cards in sections under a sticky header; `page.card({ title?, section?, element?, collapsed?, summary?, description? })` adds custom cards (`card.collapse(collapsed?)`, `card.collapsed`, `card.summary(content \| null)`), and `page.highlight({ groups?, sections? } \| null, { reveal?, expand? })` marks what's on screen. Options take `groupLabels`, `sectionLabels`, `groupDescriptions`, `sectionDescriptions`, `presetLabels`, `defaultLabel`, `groupPresetDisplay` and `groupFolded` over the store's. |
2040
- | `MountPageOptions` folds | `sectionFolded?: SectionFolded` (`boolean \| Record<string, boolean>`), `foldStorage?: StoragePort \| null`, `nav?: boolean`, `groupSummary?(group, info: FoldSummaryInfo): string \| Node \| null`, `sectionSummary?(section, info: FoldSummaryInfo): string \| Node \| null`; `FoldSummaryInfo` is `{ preset: string \| null, changed: number, keys: readonly string[] }`. |
2058
+ | `MountPageOptions` folds | `sectionFolded?: SectionFolded` (`boolean \| Record<string, boolean>`), `foldStorage?: StoragePort \| null`, `nav?: boolean`, `groupSummary?(group, info: FoldSummaryInfo): string \| Node \| null` (a panel's option too), `sectionSummary?(section, info: FoldSummaryInfo): string \| Node \| null`; `FoldSummaryInfo` is `{ preset: string \| null, changed: number, keys: readonly string[] }`. |
2059
+ | `MountPanelOptions.onActive?: (at: PanelActive \| null) => void` | The card (`{ group }`) or row (`{ group, key }`) under the pointer or focus, `null` for nothing; on `mountPanel`, `controls.mount` and `mountPage`. At most once a frame, never the same value twice, and a last `null` when the panel goes. `PanelActive` is `{ group: string, key?: string }`. |
2041
2060
  | `page.fold(group: string, folded?: boolean, options?: PageFoldOptions): void` / `page.foldSection(section: string, folded?: boolean, options?: PageFoldOptions): void` / `page.sectionFolded(section: string): boolean` | Fold a group's card or a section or open it (toggles without `folded`), and ask which a section is. `PageFoldOptions` is `{ remember?: boolean, reveal?: boolean }`; `remember` defaults to `true`. |
2042
2061
  | `page.reveal(target: { section: string } \| { group: string }, options?: { remember?: boolean }): void` | Open a section (or a group's card and its section) and glide it into view; `remember` defaults to `false`. |
2043
2062
  | `page.sections(): readonly PageSectionInfo[]` / `page.onSections(listener: (sections: readonly PageSectionInfo[]) => void): () => void` | `PageSectionInfo` is `{ id, label, description?, folded, changed, groups: PageGroupInfo[] }`; `PageGroupInfo` is `{ id, label, folded, changed, preset: string \| null }`. |
@@ -2632,6 +2632,13 @@ ${GRAD_PROPS}
2632
2632
  .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)}
2633
2633
  .knobs-group-head .knobs-group-die[disabled]{opacity:var(--k-disabled-opacity);cursor:default}
2634
2634
  .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}
2635
+ /* A panel card's header: its title, a folded card's summary and its buttons on one line, and its
2636
+ description on the next. The title takes its room first; the summary gets what's left. */
2637
+ .knobs-panel:not(.knobs-page) .knobs-group-head{flex-wrap:wrap;row-gap:0}
2638
+ .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}
2639
+ .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}
2640
+ .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}
2641
+ .knobs-group-head[data-folded]>.knobs-group-summary:not(:empty){display:block}
2635
2642
 
2636
2643
  /* Grouped cards: rows share one surface, split by hairlines that stay put whatever a row is doing */
2637
2644
  .knobs-card{position:relative;margin-bottom:var(--k-section-gap);border-radius:var(--_k-radius);background:var(--_k-fill);overflow:hidden}
@@ -3338,7 +3345,7 @@ ${POINTER_ROW} .knobs-slider.knobs-modified:not(:hover,.knobs-active) .knobs-val
3338
3345
  .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}
3339
3346
  .knobs-tile[data-folded]>.knobs-group-head .knobs-group-summary:not(:empty){display:block}
3340
3347
  .knobs-tile[data-folded]>.knobs-group-head .knobs-group-ctl{display:none}
3341
- .knobs-tile[data-fold-motion]>.knobs-group-head :is(.knobs-group-summary,.knobs-group-ctl){animation:knobs-fade .2s var(--k-ease) both}
3348
+ .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}
3342
3349
  /* A highlight (page.highlight): the rest dims under a veil of the page's colour (--k-page-dim at
3343
3350
  full), and what's highlighted on its own gets a dot in the gutter by its title. The veil and
3344
3351
  dot are written each frame on the panel spring (see page.ts). */
@@ -119,7 +119,7 @@ import {
119
119
  createHintController,
120
120
  pageHints,
121
121
  toggleLayer
122
- } from "./chunk-bejz4qcg.js";
122
+ } from "./chunk-9y93net8.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 = {
@@ -4734,6 +4734,33 @@ class GpuOverlayManager {
4734
4734
  let clipped = width > 0 && el.scrollWidth > el.clientWidth;
4735
4735
  return this.clipCache.set(el, { key, text, width, clipped }), clipped;
4736
4736
  }
4737
+ lineCache = /* @__PURE__ */ new WeakMap;
4738
+ linesOf(el, width) {
4739
+ let text = el.textContent ?? "", hit = this.lineCache.get(el);
4740
+ if (hit && hit.text === text && hit.width === width)
4741
+ return hit.lines;
4742
+ let lines = [], node = el.firstChild;
4743
+ if (node?.nodeType === Node.TEXT_NODE) {
4744
+ let top = el.getBoundingClientRect().top, range = el.ownerDocument.createRange(), line, flush = () => {
4745
+ if (line)
4746
+ lines.push({
4747
+ text: text.slice(line.from, line.to),
4748
+ y: (line.top + line.bottom) / 2 - top
4749
+ });
4750
+ };
4751
+ for (let word of text.matchAll(/\S+/g)) {
4752
+ let from = word.index, to = from + word[0].length;
4753
+ range.setStart(node, from), range.setEnd(node, to);
4754
+ let r = range.getBoundingClientRect();
4755
+ if (line && Math.abs(r.top - line.top) < 1)
4756
+ line.to = to, line.bottom = Math.max(line.bottom, r.bottom);
4757
+ else
4758
+ flush(), line = { from, to, top: r.top, bottom: r.bottom };
4759
+ }
4760
+ flush();
4761
+ }
4762
+ return this.lineCache.set(el, { text, width, lines }), lines;
4763
+ }
4737
4764
  curReduced = !1;
4738
4765
  curLocks;
4739
4766
  curSelected = 0;
@@ -6146,12 +6173,10 @@ class GpuOverlayManager {
6146
6173
  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
6174
  if (g.peek && peek > 0.005)
6148
6175
  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
- });
6176
+ } else {
6177
+ let label = tokens.groupUpper ? (g.labelEl.textContent ?? "").toUpperCase() : g.labelEl.textContent ?? "", px = Math.max(9, tokens.groupSize);
6178
+ 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 });
6179
+ }
6155
6180
  if (!g.title && tokens.groupDivider[3] > 0 && g.element.previousElementSibling)
6156
6181
  this.pushPrim({
6157
6182
  x: gr.left,
@@ -6162,13 +6187,14 @@ class GpuOverlayManager {
6162
6187
  kind: 1
6163
6188
  });
6164
6189
  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 });
6190
+ 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);
6191
+ for (let line of lines)
6192
+ 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
6193
  }
6168
6194
  let fadeIn = (node) => node.parentElement?.closest("[data-fold-motion]") ? Number(getComputedStyle(node).opacity) || 0 : 1;
6169
6195
  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 });
6196
+ let r = this.rectOf(g.summary), a = fadeIn(g.summary), px = g.title ? tokens.textControl : tokens.textMeta;
6197
+ 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
6198
  }
6173
6199
  let ctlShown = !g.controls || g.controls.getClientRects().length > 0, headAlpha = this.curAlpha;
6174
6200
  if (g.controls && ctlShown)
@@ -14439,6 +14465,79 @@ function createLens(o) {
14439
14465
  };
14440
14466
  }
14441
14467
 
14468
+ // src/panel/active.ts
14469
+ var same2 = (a, b) => a === b || a !== null && b !== null && a.group === b.group && a.key === b.key;
14470
+ function activeReporter(resolve, report, schedule) {
14471
+ let hover = null, focus = null, lead = "pointer", last = null, cancel, flush = () => {
14472
+ cancel = void 0;
14473
+ let [first, second] = lead === "pointer" ? [hover, focus] : [focus, hover], at = resolve(first) ?? resolve(second);
14474
+ if (same2(at, last))
14475
+ return;
14476
+ last = at, report(at);
14477
+ }, touch = () => {
14478
+ cancel ??= schedule(flush);
14479
+ };
14480
+ return {
14481
+ pointer(target) {
14482
+ if (hover = target, target !== null)
14483
+ lead = "pointer";
14484
+ touch();
14485
+ },
14486
+ focus(target) {
14487
+ if (focus = target, target !== null)
14488
+ lead = "focus";
14489
+ touch();
14490
+ },
14491
+ dispose() {
14492
+ if (cancel?.(), cancel = void 0, hover = focus = null, last !== null)
14493
+ report(last = null);
14494
+ }
14495
+ };
14496
+ }
14497
+ function activeEvents(reporter, where) {
14498
+ let inMenu = !1;
14499
+ return {
14500
+ over(e) {
14501
+ reporter.pointer(e.target);
14502
+ },
14503
+ move(e) {
14504
+ if (e.movementX || e.movementY)
14505
+ reporter.pointer(e.target);
14506
+ },
14507
+ out(e) {
14508
+ if (where(e.relatedTarget) !== "inside")
14509
+ reporter.pointer(null);
14510
+ },
14511
+ focusIn(e) {
14512
+ let at = where(e.target);
14513
+ if (inMenu = at === "menu", at === "inside")
14514
+ reporter.focus(e.target);
14515
+ else if (at === "outside")
14516
+ reporter.focus(null);
14517
+ },
14518
+ focusOut(e) {
14519
+ if (e.relatedTarget !== null)
14520
+ return;
14521
+ let at = where(e.target);
14522
+ if (at === "inside" || at === "menu" && inMenu)
14523
+ inMenu = !1, reporter.focus(null);
14524
+ }
14525
+ };
14526
+ }
14527
+ function watchActive(root, resolve, report) {
14528
+ let doc = root.ownerDocument, win = doc.defaultView, reporter = activeReporter(resolve, report, (flush) => {
14529
+ let id = win.requestAnimationFrame(flush);
14530
+ return () => win.cancelAnimationFrame(id);
14531
+ }), 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) });
14532
+ 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))
14533
+ reporter.focus(doc.activeElement);
14534
+ return {
14535
+ dispose() {
14536
+ root.removeEventListener("pointerover", over), root.removeEventListener("pointermove", move), root.removeEventListener("pointerout", out), doc.removeEventListener("focusin", focusIn), doc.removeEventListener("focusout", focusOut), reporter.dispose();
14537
+ }
14538
+ };
14539
+ }
14540
+
14442
14541
  // src/panel/has.ts
14443
14542
  function trackHas(root) {
14444
14543
  let doc = root.ownerDocument, win = doc.defaultView, marked = { fv: [], active: [] }, mark = (list, name, nodes, value = "") => {
@@ -14915,6 +15014,19 @@ function createPresence(root, options) {
14915
15014
  };
14916
15015
  }
14917
15016
 
15017
+ // src/panel/summary.ts
15018
+ function panelFoldSummary(rows, preset) {
15019
+ if (rows.length === 1 && rows[0].value)
15020
+ return rows[0].value;
15021
+ 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;
15022
+ return [lead, changed ? `${changed} changed` : ""].filter(Boolean).join(" · ");
15023
+ }
15024
+ var baseSignature = (controls) => JSON.stringify([
15025
+ controls.activePreset(),
15026
+ Object.keys(controls.scopes).map((s) => controls.activePreset(s)),
15027
+ Object.keys(controls.groupPresets).map((g) => controls.activeGroupPreset(g))
15028
+ ]);
15029
+
14918
15030
  // src/panel/panel.ts
14919
15031
  var layer = 2147480001, COPIED_MS = 1000, lastTab = /* @__PURE__ */ new Map, REVEAL_QUIET_MS = 2000, NUDGE_BURST_MS = 300;
14920
15032
  function readableTitle(id) {
@@ -15339,16 +15451,17 @@ function buildPanel(controls, host, options) {
15339
15451
  foldTile(card, next && !filterEl.value.trim(), animate);
15340
15452
  else
15341
15453
  applyVisibility(animate ? "reveal" : "instant");
15454
+ let marked = page ? card.parentElement : head;
15455
+ if (marked) {
15456
+ if (marked.toggleAttribute("data-folded", next), animate)
15457
+ marked.setAttribute("data-fold-motion", ""), clearTimeout(foldMotions.get(marked)), foldMotions.set(marked, setTimeout(() => marked.removeAttribute("data-fold-motion"), 400));
15458
+ }
15342
15459
  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
15460
  if (remember)
15347
15461
  foldsKept?.set("groups", group, next);
15348
- refreshFolds();
15349
15462
  } else if (sessionKept)
15350
15463
  writeSession(controls.id, { groups: { ...readSession(controls.id).groups, [group]: next } });
15351
- positioned?.constrain(), wakeGpu();
15464
+ refreshFolds(), positioned?.constrain(), wakeGpu();
15352
15465
  }
15353
15466
  function foldTile(card, shut, animate) {
15354
15467
  if ((!card.hidden && card.dataset.leaving !== "true") !== shut)
@@ -15444,11 +15557,13 @@ function buildPanel(controls, host, options) {
15444
15557
  desc.className = "knobs-group-desc", groupDescs.set(groupName, desc), describeGroup(groupName);
15445
15558
  }
15446
15559
  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);
15560
+ if (summary = el("span"), summary.className = "knobs-group-summary", page)
15561
+ gRight.append(summary);
15562
+ groupSummaries.set(groupName, summary), caret = caretSvg(), gRight.append(caret), gHead.dataset.foldable = "", gHead.tabIndex = -1, gHead.setAttribute("role", "button"), gHead.setAttribute("aria-label", title);
15450
15563
  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) => {
15564
+ if (folds.set(groupName, folded), gHead.setAttribute("aria-expanded", String(!folded)), !page)
15565
+ gHead.toggleAttribute("data-folded", folded);
15566
+ gHead.addEventListener("click", (event) => {
15452
15567
  if (event.detail > 1 || event.target.closest("button"))
15453
15568
  return;
15454
15569
  byUser(() => fold(groupName));
@@ -15457,8 +15572,10 @@ function buildPanel(controls, host, options) {
15457
15572
  if (page) {
15458
15573
  let gTitle = el("span");
15459
15574
  gTitle.className = "knobs-group-title", gTitle.append(gLabel, gPeek), gHead.append(gTitle, gRight);
15460
- } else
15461
- gHead.append(gLabel, gRight);
15575
+ } else {
15576
+ let desc = el("span");
15577
+ desc.className = "knobs-group-desc", groupDescs.set(groupName, desc), describeGroup(groupName), gHead.append(gLabel, desc, ...summary ? [summary] : [], gRight);
15578
+ }
15462
15579
  return gpuGroups.push({
15463
15580
  element: gHead,
15464
15581
  labelEl: gLabel,
@@ -16107,7 +16224,7 @@ function buildPanel(controls, host, options) {
16107
16224
  }
16108
16225
  under = name, nodes.push(r.row);
16109
16226
  }
16110
- let strip = run.group !== void 0 && !run.pinned && !stripped.has(run.group) ? groupStripFor(run.group) : void 0;
16227
+ let strip = run.group !== void 0 && !stripped.has(run.group) ? groupStripFor(run.group) : void 0;
16111
16228
  if (strip)
16112
16229
  stripped.add(run.group);
16113
16230
  if (arrange(block.card, strip ? [strip.element, ...nodes] : nodes), animate && at >= 0) {
@@ -16116,6 +16233,8 @@ function buildPanel(controls, host, options) {
16116
16233
  motion.push({ node: r.row, show: !0 });
16117
16234
  }
16118
16235
  }
16236
+ for (let group of groupDescs.keys())
16237
+ describeGroup(group);
16119
16238
  for (let [id, sh] of subheads)
16120
16239
  if (!usedSubheads.has(id)) {
16121
16240
  sh.el.remove();
@@ -16166,9 +16285,7 @@ function buildPanel(controls, host, options) {
16166
16285
  let stagger = cascade(motion.length);
16167
16286
  motion.forEach((m, i) => present(m.node, m.show, i * stagger, m.after));
16168
16287
  }
16169
- if (applyVisibility(animate ? "reveal" : "instant"), page)
16170
- refreshFolds();
16171
- if (focused && root.contains(focused) && doc.activeElement !== focused && focused.isConnected)
16288
+ if (applyVisibility(animate ? "reveal" : "instant"), refreshFolds(), focused && root.contains(focused) && doc.activeElement !== focused && focused.isConnected)
16172
16289
  focused.focus({ preventScroll: !0 });
16173
16290
  let gone = focused ? leaving.find((r) => r.row.contains(focused)) : void 0;
16174
16291
  if (gone) {
@@ -16293,7 +16410,7 @@ function buildPanel(controls, host, options) {
16293
16410
  return selectTab(list[(at + direction + list.length) % list.length].id, !1), !0;
16294
16411
  }, searching = () => search?.filtering() ?? !1, rowMatches = (r) => {
16295
16412
  let query = search.query();
16296
- if (query && !matchesFilter(query, r.display, r.key, r.group, r.group && groupTitle(r.group), r.field.subsection))
16413
+ if (query && !matchesFilter(query, r.display, r.key, r.group, r.group && groupTitle(r.group), r.group && groupDescription(r.group), r.field.subsection))
16297
16414
  return !1;
16298
16415
  for (let chip of search.active())
16299
16416
  if (!r.keys.some((k) => chipTests.get(chip)?.(k)))
@@ -16585,11 +16702,7 @@ function buildPanel(controls, host, options) {
16585
16702
  if (changed)
16586
16703
  wakeGpu();
16587
16704
  }
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
- ]);
16705
+ let shownBase = "";
16593
16706
  function update() {
16594
16707
  if (!controls.enabled) {
16595
16708
  close?.();
@@ -16608,7 +16721,7 @@ function buildPanel(controls, host, options) {
16608
16721
  if (!fresh(r) && changed(r))
16609
16722
  changedCount++;
16610
16723
  }
16611
- let stagger = initialized ? cascade(changedCount) : 0, moving = initialized ? rows.filter(changed) : rows;
16724
+ let stagger = initialized ? cascade(changedCount) : 0, moving = initialized ? rows.filter(changed) : rows, rebasing = baseSignature(controls) !== shownBase;
16612
16725
  if (updateStrips(), batchMeasures(() => {
16613
16726
  for (let row of moving)
16614
16727
  if (row.slider?.prime(), row.editor?.prime?.(), row.choice)
@@ -16616,7 +16729,7 @@ function buildPanel(controls, host, options) {
16616
16729
  for (let chips of groupChips.values())
16617
16730
  primeSegPill(chips.seg, chips.buttons);
16618
16731
  showValues(values, moving, stagger, landing);
16619
- }), moving.length || !initialized)
16732
+ }), moving.length || !initialized || rebasing)
16620
16733
  refreshFolds();
16621
16734
  shown = values, initialized = !0, wakeGpu();
16622
16735
  }
@@ -16636,7 +16749,7 @@ function buildPanel(controls, host, options) {
16636
16749
  }
16637
16750
  }
16638
16751
  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;
16752
+ let baseline = controls.baseValues(), fresh = (r) => r.row.dataset.shown === void 0, now = performance.now(), turn = 0, based = baseSignature(controls), rebased = based !== shownBase;
16640
16753
  shownBase = based;
16641
16754
  for (let row of moving) {
16642
16755
  let instant = !initialized || landing || fresh(row);
@@ -16889,16 +17002,45 @@ function buildPanel(controls, host, options) {
16889
17002
  preset: null,
16890
17003
  changed: changedRows((r) => r.section === section),
16891
17004
  keys: rows.filter((r) => r.section === section).flatMap((r) => r.keys)
16892
- }), changedText = (n) => n && marks ? `${n} changed` : "";
17005
+ }), changedText = (n) => n && marks ? `${n} changed` : "", valueText = (r, values) => {
17006
+ let f = r.field, v = r.value(values), num = (n) => String(Number(n.toFixed(2)));
17007
+ switch (f.kind) {
17008
+ case "choice":
17009
+ return f.labels?.[v] ?? String(v);
17010
+ case "toggle":
17011
+ return v ? "On" : "Off";
17012
+ case "knob":
17013
+ return `${v.toFixed(f.decimals ?? decimals(f.step))}${f.unit ? ` ${f.unit}` : ""}`;
17014
+ case "spring": {
17015
+ let { duration, bounce } = v;
17016
+ return `${num(duration)} s${bounce ? `, bounce ${num(bounce)}` : ""}`;
17017
+ }
17018
+ case "color":
17019
+ return colorHex2(v);
17020
+ case "text":
17021
+ return String(v);
17022
+ default:
17023
+ return "";
17024
+ }
17025
+ }, groupSummary = (group) => {
17026
+ let info = groupInfo(group);
17027
+ if (options.groupSummary)
17028
+ return options.groupSummary(group, info);
17029
+ if (page)
17030
+ return [info.preset, changedText(info.changed)].filter(Boolean).join(" · ") || null;
17031
+ let values = controls.get(), own = rows.filter((r) => r.group === group && !r.leaving && r.keys.length);
17032
+ return panelFoldSummary(own.map((r) => ({
17033
+ value: valueText(r, values),
17034
+ changed: marks && r.keys.some((k) => controls.isModified(k)),
17035
+ choice: r.field.kind === "choice"
17036
+ })), info.preset) || null;
17037
+ };
16893
17038
  function refreshFolds() {
17039
+ for (let [group, node] of groupSummaries)
17040
+ if (folds.get(group))
17041
+ putSummary(node, groupSummary(group));
16894
17042
  if (!layout || !page)
16895
17043
  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
17044
  for (let s of layout.sectionList()) {
16903
17045
  let node = s.label?.querySelector(":scope > .knobs-section-summary");
16904
17046
  if (!node || !s.folded)
@@ -18050,7 +18192,15 @@ function buildPanel(controls, host, options) {
18050
18192
  attributes: !0,
18051
18193
  attributeFilter: ["data-knobs-renderer", "data-theme", "data-appearance", "class", "style"]
18052
18194
  });
18053
- let panel = {
18195
+ let onActive = options.onActive, active = onActive ? watchActive(root, (node) => {
18196
+ if (!node || !root.contains(node))
18197
+ return null;
18198
+ let r = rows.find((r) => !r.leaving && r.row.contains(node));
18199
+ if (r)
18200
+ return r.group === void 0 ? null : { group: r.group, key: r.key };
18201
+ let card = node.closest("[data-group]");
18202
+ return card && root.contains(card) ? { group: card.dataset.group } : null;
18203
+ }, onActive) : void 0, panel = {
18054
18204
  element: root,
18055
18205
  presetStrip,
18056
18206
  focus() {
@@ -18113,7 +18263,7 @@ function buildPanel(controls, host, options) {
18113
18263
  doc.removeEventListener(type, onPointerUp, !0);
18114
18264
  for (let type of ["pointerup", "pointercancel"])
18115
18265
  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);
18266
+ doc.defaultView?.removeEventListener("blur", endScrub), endScrub(), unregisterHints?.(), hintCtrl.stop(), modeObserver.disconnect(), unregisterGpu?.(), lens.dispose(), win?.cancelAnimationFrame(lensFrame), doc.removeEventListener("pointerdown", onPress, !0), doc.removeEventListener("pointerup", onRelease, !0), doc.removeEventListener("pointercancel", onRelease, !0), crumbs?.dispose(), active?.dispose(), clearTimeout(escTimer), win?.removeEventListener("blur", queueScope), win?.removeEventListener("focus", queueScope), unsubscribe(), unlisten?.(), rowsObserver.disconnect(), gpuObserver.disconnect(), doc.defaultView?.cancelAnimationFrame(landTimer);
18117
18267
  for (let k of inHand)
18118
18268
  controls.hold(k, !1);
18119
18269
  inHand.clear(), positioned?.dispose(), die.dispose();
@@ -6,7 +6,7 @@ import {
6
6
  writeSession,
7
7
  injectStyles,
8
8
  toggleLayer
9
- } from "./chunk-bejz4qcg.js";
9
+ } from "./chunk-9y93net8.js";
10
10
 
11
11
  // src/panel/attach.ts
12
12
  function attach(controls, element, options = {}) {
@@ -82,7 +82,7 @@ function attach(controls, element, options = {}) {
82
82
  return;
83
83
  loading = !0;
84
84
  try {
85
- let { createPanel } = await import("./chunk-s7d5a8vb.js");
85
+ let { createPanel } = await import("./chunk-e5hbs8fe.js");
86
86
  if (disposed || !controls.enabled || panel)
87
87
  return;
88
88
  if (panel = createPanel(controls, element, close, { restored, chip }), remember)
package/dist/dev/index.js CHANGED
@@ -625,9 +625,13 @@ function defineControls(id, input, options = {}) {
625
625
  named.add(variants);
626
626
  let of = /* @__PURE__ */ new Map;
627
627
  for (let k of valueKeys(schema)) {
628
- if (presetFree().has(k))
628
+ let f = fields[k];
629
+ if (presetFree().has(k)) {
630
+ if (f.inPresets !== !1 && f.group !== void 0 && named.has(f.group))
631
+ of.set(k, f.group);
629
632
  continue;
630
- let f = fields[k], tab = tabOf(f);
633
+ }
634
+ let tab = tabOf(f);
631
635
  of.set(k, tab !== void 0 && named.has(tab) ? tab : f.group !== void 0 && named.has(f.group) ? f.group : "");
632
636
  }
633
637
  return scopingOf = { schema, of, named: [...named], variants }, scopingOf;
@@ -1522,11 +1526,11 @@ function defineControls(id, input, options = {}) {
1522
1526
  code: (format, values) => formatCode(values ?? get(), format),
1523
1527
  variants,
1524
1528
  attach(element, chip) {
1525
- let load = options.ship ? () => options.ship().then((m) => m.attach) : () => import("./chunk-f0e5tkqg.js").then((m) => m.attach);
1529
+ let load = options.ship ? () => options.ship().then((m) => m.attach) : () => import("./chunk-exaya50m.js").then((m) => m.attach);
1526
1530
  return live(load, (attach) => attach(controls, element, chip), (detach) => detach());
1527
1531
  },
1528
1532
  mount(container, mountOptions) {
1529
- let load = options.ship ? () => options.ship().then((m) => m.mountPanel) : () => import("./chunk-s7d5a8vb.js").then((m) => m.mountPanel);
1533
+ let load = options.ship ? () => options.ship().then((m) => m.mountPanel) : () => import("./chunk-e5hbs8fe.js").then((m) => m.mountPanel);
1530
1534
  return live(load, (mountPanel) => {
1531
1535
  let panel = mountPanel(controls, container, mountOptions);
1532
1536
  return mountOptions?.onReady?.(panel), panel;
package/dist/index.js CHANGED
@@ -1688,9 +1688,13 @@ function defineControls(id, input, options = {}) {
1688
1688
  named.add(variants);
1689
1689
  let of = /* @__PURE__ */ new Map;
1690
1690
  for (let k of valueKeys(schema)) {
1691
- if (presetFree().has(k))
1691
+ let f = fields[k];
1692
+ if (presetFree().has(k)) {
1693
+ if (f.inPresets !== !1 && f.group !== void 0 && named.has(f.group))
1694
+ of.set(k, f.group);
1692
1695
  continue;
1693
- let f = fields[k], tab = tabOf(f);
1696
+ }
1697
+ let tab = tabOf(f);
1694
1698
  of.set(k, tab !== void 0 && named.has(tab) ? tab : f.group !== void 0 && named.has(f.group) ? f.group : "");
1695
1699
  }
1696
1700
  return scopingOf = { schema, of, named: [...named], variants }, scopingOf;