@kitlangton/knobs 0.1.0 → 0.3.0

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 (65) hide show
  1. package/README.md +303 -41
  2. package/dist/dev/{chunk-qtt698hp.js → chunk-7xgkerd2.js} +34 -11
  3. package/dist/dev/{chunk-ktsn42p7.js → chunk-jx1ktap3.js} +13762 -11794
  4. package/dist/dev/{chunk-kdewv0mt.js → chunk-pmkvb9ep.js} +1692 -207
  5. package/dist/dev/{chunk-e13znr7c.js → chunk-v8ssbcbk.js} +18 -7
  6. package/dist/dev/index.js +557 -130
  7. package/dist/index.js +589 -141
  8. package/dist/panel.js +21615 -17048
  9. package/dist/types/core/keys.d.ts +2 -0
  10. package/dist/types/core/random.d.ts +61 -1
  11. package/dist/types/core/remote.d.ts +5 -1
  12. package/dist/types/core/schema.d.ts +35 -6
  13. package/dist/types/core/store.d.ts +97 -8
  14. package/dist/types/core/unlock.d.ts +77 -0
  15. package/dist/types/core/variants.d.ts +10 -2
  16. package/dist/types/index.d.ts +4 -2
  17. package/dist/types/panel/attach.d.ts +2 -0
  18. package/dist/types/panel/crumbs.d.ts +19 -0
  19. package/dist/types/panel/editors.d.ts +20 -0
  20. package/dist/types/panel/feel.d.ts +103 -0
  21. package/dist/types/panel/flash.d.ts +13 -0
  22. package/dist/types/panel/give.d.ts +34 -1
  23. package/dist/types/panel/gpu/color.d.ts +4 -4
  24. package/dist/types/panel/gpu/renderer.d.ts +110 -6
  25. package/dist/types/panel/gpu/shader.wgsl.d.ts +1 -1
  26. package/dist/types/panel/haptics.d.ts +6 -2
  27. package/dist/types/panel/has.d.ts +0 -1
  28. package/dist/types/panel/index.d.ts +3 -1
  29. package/dist/types/panel/inspector.d.ts +33 -3
  30. package/dist/types/panel/lens.d.ts +113 -0
  31. package/dist/types/panel/motion.d.ts +2 -0
  32. package/dist/types/panel/optkeys.d.ts +7 -0
  33. package/dist/types/panel/page-hints.d.ts +2 -1
  34. package/dist/types/panel/prefs.d.ts +2 -2
  35. package/dist/types/panel/presence.d.ts +20 -0
  36. package/dist/types/panel/presets.d.ts +17 -5
  37. package/dist/types/panel/ramp-morph.d.ts +117 -0
  38. package/dist/types/panel/ramps.d.ts +1 -1
  39. package/dist/types/panel/reel.d.ts +43 -0
  40. package/dist/types/panel/scope.d.ts +48 -0
  41. package/dist/types/panel/slider.d.ts +22 -5
  42. package/dist/types/panel/sound.d.ts +137 -0
  43. package/dist/types/panel/styles.d.ts +1 -1
  44. package/dist/types/panel/tab-transition.d.ts +15 -0
  45. package/dist/types/panel/tabs.d.ts +1 -1
  46. package/dist/types/panel/toggle.d.ts +27 -0
  47. package/dist/types/panel/toolbar-grid.d.ts +6 -0
  48. package/dist/types/ui/activity-css.d.ts +1 -0
  49. package/dist/types/ui/activity-model.d.ts +122 -0
  50. package/dist/types/ui/activity.d.ts +67 -0
  51. package/dist/types/ui/follow.d.ts +17 -0
  52. package/dist/types/ui/index.d.ts +5 -1
  53. package/dist/types/ui/look.d.ts +9 -0
  54. package/dist/types/ui/picker-css.d.ts +1 -0
  55. package/dist/types/ui/picker-model.d.ts +53 -0
  56. package/dist/types/ui/picker.d.ts +67 -0
  57. package/dist/types/ui/react.d.ts +27 -0
  58. package/dist/types/ui/scrollbar.d.ts +12 -0
  59. package/dist/types/ui/solid.d.ts +23 -0
  60. package/dist/types/ui/split-model.d.ts +55 -0
  61. package/dist/types/ui/split.d.ts +22 -3
  62. package/dist/ui-react.js +3462 -690
  63. package/dist/ui-solid.js +3440 -689
  64. package/dist/ui.js +2963 -264
  65. package/package.json +1 -1
package/README.md CHANGED
@@ -4,11 +4,11 @@ Embeddable, keyboard-first tuning controls for React, Solid, and plain DOM.
4
4
 
5
5
  Give a component a few knobs. Hover it to reveal a tiny cog and open a floating panel, or mount an inline panel directly into a sidebar or tuning dashboard. Explore a seed, a spring curve, a spacing, or a layout variant. Save the good variations. Copy constants back into your source and ship zero tuning UI in production.
6
6
 
7
- Each panel's header carries **Random · Reset · Copy · Undo/Redo** as icon buttons, quiet until the pointer or keyboard is in the panel. Undo and redo fold in once there's history and away when there's none; Copy turns into a check for a moment once it has copied, and Reset's ↺ turns once each time it resets (a press mid-turn adds another; with reduced motion it blinks instead). The header has no `?` button: `?` opens the shortcuts, as the title's tooltip says (`header: { buttons: [..., "help"] }` brings the button back). It sits above a preset strip and inset-grouped cards of rows. Random is a die that tumbles when it rolls; `⌥`-click it (or press `⌥R`) to **mutate** instead: small changes around the current values, with a quarter turn of the die. Each group has its own quieter die beside its reset icon. **Lock** a row (`⇧L`, or the row's context menu) to keep it out of Random and Mutate; a small lock sits just left of its value. Hold **`\`** to see the values from before your last change, in the panel and your app alike. Shipped presets and your saved variants live together in that strip: `+` (or `⇧S`) saves the current values as a new chip, `⌫` on the strip deletes a saved one, and the `?` popover exports/imports them as JSON.
7
+ Each panel's header carries **Undo · Redo · Reset · Copy · Random** as icon buttons, quiet until the pointer or keyboard is in the panel. Random holds the corner with Copy beside it; they're always there. Undo shows while there's history, Redo while there's something to redo and Reset while it has somewhere to go (back to the active preset, else the defaults), each in a slot of its own that waits empty meanwhile, so they bloom in and out in place and nothing else in the header ever moves. Copy turns into a check for a moment once it has copied, and Reset's ↺ turns once each time it resets before it leaves (a press mid-turn adds another; with reduced motion it blinks instead). Hover washes come in at once and fade out. A button flashes when its command runs, from a click or its key alike (`u`, `⇧U`, `⇧R`, `⇧C`, `R`, `⌥R`, and a group card's `R` / `⇧R` on that group's die and reset); a click presses a header button without leaving focus on it. The header has no `?` button: `?` opens the shortcuts, as the title's tooltip says (`header: { buttons: [..., "help"] }` brings the button back, past Random with the close X). It sits above a preset strip and inset-grouped cards of rows. Random is a die that tumbles when it rolls; `⌥`-click it (or press `⌥R`) to **mutate** instead: small changes around the current values, with a quarter turn of the die. Each group has its own quieter die in its header's corner, with a reset icon beside it that shows (in its own slot, as the header's does) while the group differs from its preset or defaults. **Lock** a row (`L`, or the row's context menu) to keep it out of Random and Mutate; a small lock sits just left of its value. Hold **`\`** to see the values from before your last change, in the panel and your app alike. Shipped presets and your saved variants live together in that strip: `+` (or `⇧S`) saves the current values as a new chip and lets you type its name, `F2`, `Enter` or a double-click renames a saved one, `⌫` on the strip deletes one, and the `?` popover exports/imports them as JSON. The preset you applied last is the **base**: Reset goes back to it, and Random explores around it (see [Presets](#presets-names-and-the-base)).
8
8
 
9
9
  Beyond sliders, choices and switches there are four richer fields: a SwiftUI-style **spring** (duration and bounce beside its live step-response curve), an **OKLCH colour** (swatch and hex, with hue, chroma and lightness strips that paint their real gradients), a 2D **point** pad, and a cubic-bezier **easing** editor with named curves.
10
10
 
11
- Zero runtime dependencies in core. Pure DOM panels with `@kitlangton/rolling-number` readouts and DialKit-style sliders: a flat accent-tinted fill that stops exactly at its ends, a hairline handle centred on its edge that fades where it would cross the text, a faint notch at the default that pulls drags onto it, stepped sliders (ten steps or fewer) that resist around their stop and snap to the next past halfway, `⌥`-drag for 1/10 speed, and a sideways trackpad pan that moves the value with your fingers like a drag (a `⇧` + wheel notch steps once); the same pan drags the selection pill across the preset strip or a choice, through as many options as you travel, flips a switch from anywhere on its row, scrubs a seed and steps a ramp's presets, and the momentum after you lift is ignored, so it stops where your fingers did. At an end, a pan gives a little like a rubber band instead of stopping dead. Switching presets ripples the changed rows top to bottom; selection pills and switch thumbs stretch toward where they're going and settle; the panel is a frosted glass surface over your work. A reload, Vite's included, keeps floating panels open, editors expanded and focus where it was, per tab. Thin adapters for React (`knobs/react`) and Solid (`knobs/solid`), plus a remote/descriptor mode for driving values over a WebSocket.
11
+ Zero runtime dependencies in core. Pure DOM panels with `@kitlangton/rolling-number` readouts and DialKit-style sliders: a flat accent-tinted fill that stops exactly at its ends, a hairline handle centred on its edge that fades where it would cross the text, detents that hold a drag for a few px at the default (a faint notch rising from the bottom edge), the active preset's value (one hanging from the top) and the middle (and zero), letting go once you push through, stepped sliders (ten steps or fewer) that resist around their stop and snap to the next past halfway, precision by distance (move the pointer up or down off the row while dragging and it slows to ½, ¼ and ⅛ in smooth steps, a fine ruler showing near the handle; come back and it speeds up without the value jumping), `⌥`-drag for 1/10 speed on top of that, a row that stretches like rubber past either end (its far end anchored, a little thinner as it lengthens, its text still) and springs back on release, and a sideways trackpad pan that moves the value with your fingers like a drag (a `⇧` + wheel notch steps once); the same pan drags the selection pill across the preset strip or a choice, through as many options as you travel, flips a switch from anywhere on its row, scrubs a seed and steps a ramp's presets, and the momentum after you lift is ignored, so it stops where your fingers did. At an end, a drag or a pan gives a little like a rubber band instead of stopping dead. Switching presets ripples the changed rows top to bottom; selection pills and switch thumbs stretch toward where they're going and settle; the panel is a frosted glass surface over your work. A reload, Vite's included, keeps floating panels open, editors expanded and focus where it was, per tab. Thin adapters for React (`knobs/react`) and Solid (`knobs/solid`), plus a remote/descriptor mode for driving values over a WebSocket.
12
12
 
13
13
  ## Install
14
14
 
@@ -192,9 +192,12 @@ then move on past the ends, and `[` / `]` cycle through them. `n` adds one halfw
192
192
  and `Delete` removes the selected one. On a gradient, `Enter` edits the stop's colour and `Esc`
193
193
  comes back. `←`/`→` on a closed row or on the chips cycle the presets. A drag is one undo step.
194
194
 
195
- Presets change smoothly: both ramps are sampled at fixed points (64 colours, 129 curve points)
196
- and the samples blend on the panel's spring, so a strip shifts colour and a curve bends into its
197
- new shape while the pins and points glide. The WebGPU renderer draws the same samples. Random
195
+ Presets change smoothly, on one .3 s spring per row that both renderers read. Stops and points
196
+ keep their identity: the ends travel, inner ones pair up in order with the nearest, one left over
197
+ shrinks into its neighbour and a new one buds out of the line in place. The line is resampled
198
+ (129 curve points, 64 colours) and blended between the two, and anything moving fast smears along
199
+ its path by its speed (`--k-curve-blur`, 0 at rest). Changing preset mid-flight keeps the speed.
200
+ An easing row morphs the same way. Random
198
201
  keeps a gradient's stop count and walks its hue along a lightness ramp. On a curve, Random picks
199
202
  a built-in shape and jitters it. Mutate nudges colours as it does a `color` and moves stops and
200
203
  points without letting them pass each other.
@@ -202,9 +205,10 @@ points without letting them pass each other.
202
205
  ### Inspector fields: vectors, ranges, angles, seeds, key combinations and toolbars
203
206
 
204
207
  ```ts
205
- import { angle, defineControls, keybind, range, seed, toolbar, vec2, vec3 } from "knobs"
208
+ import { action, angle, defineControls, keybind, range, seed, toolbar, vec2, vec3 } from "knobs"
206
209
 
207
210
  const node = defineControls("node", {
211
+ replay: action("Replay", { run: replay, shortcut: "Shift+K", global: true, rebind: true }),
208
212
  tools: toolbar([
209
213
  { id: "play", label: "Play", icon: "play", shortcut: "Space" },
210
214
  { id: "retrigger", label: "Retrigger", icon: "retrigger", shortcut: "R", global: true },
@@ -242,15 +246,26 @@ const node = defineControls("node", {
242
246
  Esc cancels and ⌫ clears. Not rolled by Random or Mutate. Letters follow the user's layout
243
247
  (Dvorak's K records and matches as K), with ⌥ combinations read through the browser's layout
244
248
  map; `comboFromEvent`, `matchesCombo` and `letterOf` do the same for a host's own keys.
245
- - **`toolbar(items)`**: a wrapping row of buttons with an icon (`play`, `pause`, `frame`,
249
+ - **`toolbar(items)`**: a row of buttons with an icon (`play`, `pause`, `frame`,
246
250
  `retrigger`, `clear`, `target`, `camera`, `sparkle`, … or SVG path data on a 256 grid), a label
247
- and a shortcut chip. A press calls the item's `run` and `onAction(id)`; `controls.action(key, id)`
251
+ and a shortcut chip. The buttons share equal columns, as many as fit, balanced across the rows
252
+ (four that don't fit in one row wrap as two and two), each chip at its button's end. A press calls the item's `run` and `onAction(id)`; `controls.action(key, id)`
248
253
  does it from code. Shortcuts work while the panel has focus, and anywhere on the page with
249
254
  `global: true` (not while typing). A panel's shortcuts win over its own single-key commands.
250
255
  `shortcut: { key, scope }` says where one works: `"panel"` (the default), `"page"` (as `global`)
251
256
  or `"none"`, a chip that only shows a key the host binds itself (a canvas's `F`), so the panel's
252
257
  own `f` and `g` still work beside it. The row holds no value; ←/→ move along it and Enter or
253
258
  Space presses.
259
+ - **`action(label, { run, shortcut?, global?, rebind? })`**: a button row, whose `shortcut` (a
260
+ combination such as `"Shift+K"`, or `{ key, scope }`) presses it under the same rules as a
261
+ toolbar button's: while the panel has focus, or anywhere on the page with `global: true` (not
262
+ while typing). It shows as a key cap at the row's end. With `rebind: true` the user can change
263
+ it there: a click or Enter on the cap listens, the next combination is the new shortcut, Esc
264
+ cancels and ⌫ clears; →/← go between the button and the cap. A rebinding isn't a value: it
265
+ stays out of presets, undo and Random, and is saved with the values, as locks are.
266
+ `controls.shortcut(key, item?)` reads the shortcut in force and `controls.rebind(key, combo,
267
+ item?)` changes it from code (`""` for none, `undefined` for the declared one), a toolbar
268
+ button's too (by its id), which has no cap of its own to rebind it from.
254
269
 
255
270
  Remote descriptors use the same names: `kind: "vec2" | "vec3" | "range" | "angle" | "seed" |
256
271
  "keybind"` with `default`, `min`, `max`, `step`, `unit`, `decimals`, `link`, `linked` and `wrap`, and
@@ -258,7 +273,7 @@ Remote descriptors use the same names: `kind: "vec2" | "vec3" | "range" | "angle
258
273
  `axes`. Each kind takes
259
274
  `disabled`, `readOnly` and mixed values like the rest ("—" in place of the value).
260
275
 
261
- Spring, point, easing, gradient and curve values compare by their contents everywhere (`isModified`, presets, undo, variants), and unchanged ones keep their identity across updates so memoized React children skip them. Random values stay in range people use (springs 0.2–0.9 s with bounce up to 0.45, mid-light colours of moderate chroma, any point, a named curve).
276
+ Spring, point, easing, gradient and curve values compare by their contents everywhere (`isModified`, presets, undo, variants), and unchanged ones keep their identity across updates so memoized React children skip them. Random values stay in range people use (springs 0.2–0.9 s with bounce up to 0.45, mid-light colours of moderate chroma, any point, a named curve). Switches sit out of Random and Mutate unless a field says `random: true`: they usually show or hide things (a view, a scene's part), and a roll that hides the page isn't a new take on it.
262
277
 
263
278
  ### Readouts: live values, sparklines, graphs, meters, tables and logs
264
279
 
@@ -302,6 +317,20 @@ live.watch("events", { level: "warn", text: "Gutter drip over budget" })
302
317
 
303
318
  Readouts cost nothing at rest. They draw on one shared clock, only while on screen and only when something arrived, so a paused feed lets the page, and the GPU overlay, sleep. Graphics are canvases redrawn from a list of drawing ops (the GPU overlay draws the same ops). Text changes at most `hz` times a second (4 by default), in boxes that only grow, so nothing reflows as numbers change. Scales snap to round steps (1, 2 or 5 × 10ⁿ) and ease there on a spring, in log space, so a spike rescales smoothly. The store keeps each readout's history, so a panel mounted later starts with it. On their own, outside a panel: `readout(sparkline({ unit: "ms" }), { preset })` from `knobs/ui` returns `{ element, push(value), value, dispose() }`.
304
319
 
320
+ ## Presets: names and the base
321
+
322
+ The strip holds the schema's `presets` (and `default`) beside the variants you save. Saving (`+` or `⇧S`) puts a field where the new chip goes, its name ("Variant 3") selected so typing replaces it; `Enter` keeps what you typed, `Esc` keeps "Variant 3", and the field opens out to the chip's width without moving anything else. `F2` or `Enter` on the strip, or a double-click, renames a saved one the same way (`controls.variants.rename(id, name)`); names are kept with the variants in the store's storage. The schema's presets keep their names.
323
+
324
+ The preset you apply last (a chip, `[` / `]`, a pan, `controls.preset(id)`, `variants.load(id)`, or a page card's preset for its group) becomes the **active preset**, the base for its scope:
325
+
326
+ - **Reset climbs a ladder.** `⇧R`, the header's ↺, a group's ↺ and a row's reset go back to the active preset's values while anything differs from them; from there, the next reset goes to the defaults. Reaching the defaults with everything forgets the preset (so does applying Default); a group's reset forgets the group's own preset. With no active preset, Reset goes to the defaults, as always. Each reset icon shows while its reset has somewhere to go.
327
+ - **Modified means off the base.** Rows' dots, the readout marks, the "Modified" filter and fold summaries compare with the active preset. Its chip stays selected; once the values drift from it the pill softens and a small dot in the chip's corner marks it modified. Clicking the chip again brings its values back.
328
+ - **Random explores around it.** With an active preset, Random (`r`, the dice, `g`) draws near its values instead of anywhere: a number lands in a window 35% of its range wide, centred on the preset's value and slid inward at an end so it keeps that width. That's far enough to be a new take and near enough to stay recognisably that preset (Mutate moves 8–12% from the current values; a free roll covers all of it). Choices (and switches that opt in) keep the preset's pick 70% of the time; colours keep their family (hue within ±20°, lightness ±.06, chroma ±15% of what the hue can hold); springs, points, vectors, ranges, angles, easings, gradients and curves stay as near; seeds roll freely. A group preset is the base for its own keys; the others roll freely. Mutate still nudges the current values. `AROUND` (exported from `knobs`) holds the numbers.
329
+ - **The `?` popover says so:** "Random around Storm / Back to Storm", then "Reset all to defaults" once you're there.
330
+ - **Undo covers it:** applying a preset, each reset and each roll is one undo step, and undoing restores the active preset with the values. The active preset is kept with the values in storage.
331
+
332
+ `controls.activePreset()` returns `{ kind: "preset" | "variant", id }` or null, `controls.activeGroupPreset(group)` the group's index or -1, `controls.baseValues()` what Reset returns to first, and `controls.canReset(keys?)` whether a reset would change anything. A reset back to a preset reaches `onChange` with `source: "reset"` and `base: true`; remote controls send its values with `onSet` (nulls only for a reset to the defaults), or call `onPreset` again for a group whose peer applies its own presets.
333
+
305
334
  ## Keyboard
306
335
 
307
336
  Panels and switchers are keyboard-first:
@@ -309,34 +338,79 @@ Panels and switchers are keyboard-first:
309
338
  | Key | Action |
310
339
  | --- | --- |
311
340
  | `↑` / `↓` or `j` / `k` | Move between presets, rows and custom actions (`←`/`→` move along the custom actions); a spring's two sliders are two stops |
312
- | `←` / `→` or `h` / `l` | Adjust slider, cycle choice, or flip toggle (`⇧` multiplies numeric step by `×10`); on a colour row turn the hue, on a point row move x, on an easing row cycle the named curves |
341
+ | `←` / `→` | Adjust slider, cycle choice, or flip toggle (`⇧` multiplies numeric step by `×10`); on a colour row turn the hue, on a point row move x, on an easing row cycle the named curves |
313
342
  | `Enter` / `Space` | Open a colour, point, easing, gradient or curve row's editor; `↓` then walks into it (strips, chips, handles, pad) |
314
- | `Esc` | Fold an open editor and return to its row (before closing the panel) |
343
+ | `Esc` | Climb one level: fold an open editor back to its row, then from a row to its card, from a card to the panel, then out (a floating panel closes). The focus lens grows out to each level as you go, and the header's `esc ↑ Shape` says where the next press goes (see [Where keys go](#where-keys-go)). Inline edits, the search field and the tree's type-ahead take `Esc` first |
315
344
  | Arrows on a pad or curve handle | Move the puck or handle (`⇧` ×10, `⌥` finest); `j` / `k` or `Tab` move on |
316
345
  | On a gradient strip or curve graph | Arrows move the selected stop or point; `Tab` / `⇧Tab` and `[` / `]` pick another; `n` adds one, `Delete` removes it; `Enter` edits a stop's colour (`Esc` returns) |
317
346
  | `[` / `]` | Cycle presets and saved variants from anywhere in the panel; with no preset strip, cycle tabs |
347
+ | `1`–`9` | Pick the focused row's Nth option: a choice (segmented or menu) or a curve's, easing's or gradient's presets. Tiny digit hints show on the options once focus settles. A row with options keeps the digits; elsewhere they do what they did before (inline number edits, a page's group presets) |
318
348
  | `⌥1`–`⌥9` | Show tab 1–9 |
319
349
  | `/` | Focus the search field (`Esc` clears it, then hands focus back; `↓` or `Enter` go to the first row left) |
320
350
  | `Home` / `End` | Jump slider to `min` or `max` |
321
351
  | `0`–`9`, `.`, `-` or `Enter` | Edit numeric value inline: the readout's own digits, in place, with the unit after them. `↑` `↓` step the text (`⇧` by the big step), `Enter` commits (the readout rolls from what was typed to the nearest step in range), `Esc` cancels |
322
352
  | `Backspace` / `Delete` | Reset focused row (or the selection) to its default (or double-click row/label) |
323
- | `⌥` + drag | Fine-tune a slider at 1/10 speed |
324
- | Pan sideways (two fingers) | Every control that holds a value answers it, in the active state a drag gives it. Sliders, colour strips, pads, easing handles, vector cells, ranges and dials move 1:1 with the fingers; over the preset strip or a choice, the pill slides through the options, and a choice menu steps them. Anywhere on a switch's row, its thumb follows the fingers and the switch flips past the middle (a flick goes its way). A seed scrubs a step every 12 px, its die rolling along, and a flick carries on a few steps. Over a gradient strip or a curve graph it moves the stop or point under the pointer; on a gradient, curve or easing row's head it steps through the presets, and on a colour row's head it turns the hue. At a limit everything gives a little (6 px at most, with one tick) and springs back when you let go. One undo entry per gesture; momentum is otherwise ignored; vertical scrolling passes through. A `⇧` + wheel notch steps once |
353
+ | Drag a slider | Follows the pointer. Off the row it slows in smooth steps (×1 within 24 px, ×½ around 60, ×¼ around 110, ×⅛ beyond 146; ranges, dials, vector cells and colour strips too), a fine ruler fading in near the handle a level per halving; the value never jumps as the rate changes. It sticks for a few px at the default, the active preset's value and the middle (and zero), with a tick (a sound with `sound: true`) and a small settle of the handle, and lets go once pushed through; slowed, the detents weaken so a fine move can sit beside one. `detents: false` on a knob or the mount turns them off. Past `min` or `max` the row stretches like rubber from the end it's anchored at (14 px at most, never past the panel's glass, with one tick) while the value stays at the limit, and springs back on release; range ends, colour strips and pads stretch at their limits too |
354
+ | `⌥` + drag | Fine-tune at 1/10 speed, on top of the slowing off the row |
355
+ | Pan sideways (two fingers) | Every control that holds a value answers it, in the active state a drag gives it. Sliders, colour strips, pads, easing handles, vector cells, ranges and dials move 1:1 with the fingers; over the preset strip or a choice, the pill slides through the options, and a choice menu steps them. Anywhere on a switch's row, its thumb follows the fingers and the switch flips past the middle (a flick goes its way). A seed scrubs a step every 12 px, its die rolling along, and a flick carries on a few steps. Over a gradient strip or a curve graph it moves the stop or point under the pointer; on a gradient, curve or easing row's head it steps through the presets (a mouse dragged sideways on the folded head does too, a step every 24 px; under 4 px it's still a click that opens the row), and on a colour row's head it turns the hue. At a limit everything gives a little, as under a drag (a slider's fill 14 px at most, other controls 6, with one tick), and springs back when you let go; a slider's pan sticks at the same detents as its drag. One undo entry per gesture; momentum is otherwise ignored; vertical scrolling passes through. A `⇧` + wheel notch steps once |
325
356
  | `s` | Jump: badges every row (one code per row, compound ones included), preset and button with its shortest unique prefix; type it to jump (presets apply). With other panels or `knobs/ui` pieces on the page, it labels all of them (see [Hint mode across the page](#hint-mode-across-the-page)) |
326
357
  | `Tab` / `⇧Tab` | Next/previous control, wrapping inside floating panels |
327
358
  | `f` | Focus first row |
328
- | `r` / `⇧R` | Randomize all fields / reset all fields to defaults (locked fields keep their values on Random, not on Reset) |
329
- | `⌥R` | Mutate: nudge every unlocked value a little (numbers and points 8–12% of their range, colours a few degrees of hue and a touch of chroma and lightness, springs and curves slightly; choices and switches change about 15% of the time) |
330
- | `g` / `⌥G` | Randomize / mutate the focused row's group (also the die beside each group's reset icon; `⌥`-click mutates) |
331
- | `⇧L` | Lock or unlock the focused row (or the selection) from Random and Mutate |
332
- | `⇧`-click / `⇧↑` `⇧↓` | Select a range of rows (`⌘`-click adds or removes one); `r`, `⌥R`, `⌫`, `⇧L` and `⇧C` then act on the selection; `Esc` or a plain click clears it |
359
+ | `r` / `⇧R` | Randomize / reset, scoped to focus: the selection, else the focused row, else the focused card (its group), else every field. With an active preset, Random lands near it and Reset goes back to it, then to the defaults (see [Presets](#presets-names-and-the-base)). Locked fields, switches and `random: false` keep their values on Random, not on Reset. The header's die and reset icon always act on everything. A reset icon shows while its reset has somewhere to go (the header's: anything; a group's: that group), and ⇧R on a group's card turns that group's ↺ as a click would |
360
+ | `⌥R` | Mutate, scoped the same way: nudge every unlocked value a little (numbers and points 8–12% of their range, colours a few degrees of hue and a touch of chroma and lightness, springs and curves slightly; choices and switches change about 15% of the time) |
361
+ | `g` / `⌥G` | Randomize / mutate the focused row's whole group from any row in it (also the die beside each group's reset icon; `⌥`-click mutates) |
362
+ | `L` | Lock or unlock the focused row, the selection, or a focused group heading's whole group, from Random and Mutate (right-click a group heading for its Lock, Randomize and Reset) |
363
+ | `⇧`-click / `⇧↑` `⇧↓` | Select a range of rows (`⌘`-click adds or removes one); `r`, `⌥R`, `⌫`, `L` and `⇧C` then act on the selection; `Esc` or a plain click clears it |
333
364
  | Right-click / `⇧F10` / `ContextMenu` | Menu for the row or selection: Lock from Random, Randomize, Reset, Copy; `↑↓`, a letter, `Enter`, `Esc` |
334
365
  | Hold `\` | Compare: show the values from before the last change while held (the title dims); nothing is recorded |
335
366
  | `⇧C` | Copy the current values (or the selected fields) as code |
336
- | `⇧S` | Save the current values as a new preset chip |
367
+ | `⇧S` | Save the current values as a new preset chip and type its name (`Enter` keeps it, `Esc` keeps "Variant N") |
368
+ | `F2` / `Enter` on the strip, double-click a chip | Rename a saved preset |
337
369
  | `u` / `z` | Undo (`⇧u` / `⇧z` redo); slider drags coalesce into a single history entry |
338
370
  | `?` | Toggle keyboard shortcuts popover (the title's tooltip mentions it) |
339
- | `Esc` | Close shortcuts popover or floating panel and restore focus to its chip |
371
+ | `` ` `` | Show or hide every panel and chip on the page, from anywhere outside a text field (see [Show, hide and unlock](#show-hide-and-unlock)) |
372
+ | `Esc` | Close the shortcuts popover; on a focused floating panel (nothing deeper focused), close it and restore focus to its chip |
373
+ | Click a card's empty area or heading | Focus the card (the lens wraps it); `↑` / `↓` go to its first or last row, and `r`, `⌥R` and `⇧R` act on its group |
374
+
375
+ ### Where keys go
376
+
377
+ Keys act on the level focus is at: an open editor's part, a row, its card, or the whole panel.
378
+ Three things show which, in both renderers, on floating and inline panels and on a page:
379
+
380
+ - **The focus lens.** One shape, a soft wash with a hairline edge, marks that level and morphs
381
+ straight from where it is to the next (position, size and corners on a quick spring with no
382
+ bounce), never through another level. At rest it fits its target exactly: a row's own shape in
383
+ its card (square, but for the card's corners it reaches), a card's rounded rect. Going in, it
384
+ shrinks onto the row; `Esc` grows it out to the card, then to the panel; `Esc` or `Tab` out of
385
+ the panel grows it to the panel's edge as it fades (a click elsewhere just fades it). A row to
386
+ the next glides. It replaces the per-row and per-card washes. A row's lens shows while you use
387
+ keys (as the old focus wash did); a card's also after a click on it. A press waits for its click
388
+ to finish, so a click on a heading goes straight to the card. With reduced motion it fades out
389
+ where it was and in where it lands.
390
+ - **The armed panel.** While a panel holds the keys (focus is inside it; hovering isn't enough),
391
+ its hairline edge brightens a touch toward the accent and its title is at full strength. At rest
392
+ the title is a little quieter, so on a page with several panels the one listening is plain.
393
+ Focus leaving (or `Esc` out of the panel, or the window going to the background) disarms it.
394
+ - **The scope line.** Beside the title, a quiet trail says where keys go: `Swarm › Shape ›
395
+ Cohesion`, the deepest crumb bright and its parents dim. Crumbs glide to their places as focus
396
+ moves, and one that changes fades out as the new one fades in. For a moment after a key below
397
+ the panel's level, a faint `esc ↑ Shape` follows it, then fades. Clicking a crumb takes focus to
398
+ its level; clicking the title takes it to the panel. When it doesn't fit, the first crumbs fold
399
+ into `…`. `crumbs: false` on the mount drops the line (the lens and the armed frame stay).
400
+
401
+ Tune them with tokens on the panel or any ancestor:
402
+
403
+ | Token | Default | What it does |
404
+ | --- | --- | --- |
405
+ | `--k-lens-ms` | `180` | The lens's morph (ms), a spring with no bounce |
406
+ | `--k-lens-lead` | `1` | How much quicker its size settles than its position (`1`: together) |
407
+ | `--k-lens-wash`, `--k-lens-line` | `.9`, `.65` | Its wash (a share of `--k-surface-hi`) and hairline edge (of `--k-line-hi`) |
408
+ | `--k-lens-panel-wash` | `.35` | The wash at the panel's level, where it covers everything (a share of the wash) |
409
+ | `--k-lens-inset`, `--k-lens-panel-inset` | `0px`, `0px` | How far inside a row, and inside the panel's edge, it sits |
410
+ | `--k-armed-edge` | `.1` | How much accent an armed panel's hairline edge takes |
411
+ | `--k-armed-glow`, `--k-armed-glow-a` | `0`, `0` | An optional glow around it: blur (px) and the accent's strength |
412
+ | `--k-title-rest` | `.6` | A resting panel's title, between `--k-label` (0) and `--k-text` (1) |
413
+ | `--k-esc-hint-ms` | `1800` | How long `esc ↑ …` stays after the last key |
340
414
 
341
415
  ### Hint mode across the page
342
416
 
@@ -411,7 +485,7 @@ defineControls("tune", schema, {
411
485
  knob(44, { min: 28, max: 72, group: "drawer.spacing", groupLabel: "Spacing", section: "drawer", sectionLabel: "Session List" })
412
486
  ```
413
487
 
414
- `createRemoteControls` takes the same options (and `groupLabel` / `sectionLabel` on descriptors), and `mountPanel` / `mountPage` take them too, over the store's. Everything that shows a name uses the label: card headers in both renderers, aria names, hint codes, the preset strip, its hint badges. The filter matches labels and ids alike. The preset id stays `default` for `[` / `]`, storage and `matchingPreset()`; there's no second "default" chip. Stores answer `groupLabel(id)`, `sectionLabel(id)` and `presetLabel(id)`. Group presets already have names, so `presetLabels` doesn't apply to them. A small reset icon appears beside the header while anything in that group differs from its defaults, next to a die that randomizes just that group (`⌥`-click mutates it). A slider's default is a short notch rising from its bottom edge while the row is hovered, focused or dragged, brighter once the value moves off it; changed choices and switches show a small dot.
488
+ `createRemoteControls` takes the same options (and `groupLabel` / `sectionLabel` on descriptors), and `mountPanel` / `mountPage` take them too, over the store's. Everything that shows a name uses the label: card headers in both renderers, aria names, hint codes, the preset strip, its hint badges. The filter matches labels and ids alike. The preset id stays `default` for `[` / `]`, storage and `matchingPreset()`; there's no second "default" chip. Stores answer `groupLabel(id)`, `sectionLabel(id)` and `presetLabel(id)`. Group presets already have names, so `presetLabels` doesn't apply to them. A small reset icon appears beside the header while anything in that group differs from its defaults, next to a die that randomizes just that group (`⌥`-click mutates it). A slider's default is a short notch rising from its bottom edge while the row is hovered, focused or dragged, brighter once the value moves off it; with an active preset whose value differs, a second notch hangs from the top edge at that value, and either brightens while a drag is held on it; changed choices and switches show a small dot.
415
489
 
416
490
  ### Remote / Socket-Driven Controls (`createRemoteControls`)
417
491
 
@@ -515,7 +589,7 @@ Without Solid: `remote.update(descriptors, values, { land })` and `mountPanel(re
515
589
  ranges and labels. `onSet` reports `"d"` and `"b"` separately; values stay flat. Locally:
516
590
  `defineControls("m", { ...flatSpring({ duration: 0.45, bounce: 0.3 }, { label: "Spring" }) })`.
517
591
  - **Actions and settings.** `{ kind: "action" }` (or `action("Replay")` locally) is a button row
518
- that calls `onAction(key)`. Choices take `{ id, label }` options. `fromSettings(groups, home)` maps
592
+ that calls `onAction(key)`; `shortcut` (a combination), `global` and `rebind` work as `action`'s. Choices take `{ id, label }` options. `fromSettings(groups, home)` maps
519
593
  a hosted prototype's settings sections (choices, switches, buttons, titled sections becoming
520
594
  groups) to descriptors and values, with what the prototype first reported as each default.
521
595
  - **Choices** are segmented while their labels fit with room to spare and there are at most four,
@@ -529,17 +603,44 @@ Without Solid: `remote.update(descriptors, values, { land })` and `mountPanel(re
529
603
  are claimed (`preventDefault`) and stopped there, and everything else reaches the page. Esc hands
530
604
  focus back. `keys` remaps (`{ hint: "f" }`) or turns off (`{ random: false }`) any command; a
531
605
  command that's off loses its header button too, and the `?` popover and row menu list only what's
532
- on (see **Header** for picking buttons). `vim: false` drops j/k/h/l, `edit: false` leaves digits
606
+ on (see **Header** for picking buttons). `vim: false` drops j/k, `edit: false` leaves digits
533
607
  and Enter to the page, `bigStep` sets ⇧'s multiplier and `wrap: false` stops ↑/↓ at the ends.
534
608
  - **Marks.** `marks: false` (on the store or the mount) hides modified dots, readout marks, group
535
609
  resets and Reset, for values that belong to someone else. A command that's off takes its marks
536
610
  too: with `reset: false` rows show nothing to reset, and group resets follow `resetAll`.
537
- - **Haptics.** On phones, a stepped slider crossing a stop, the default's notch catching a drag, a
611
+ - **Haptics.** On phones, a stepped slider crossing a stop, a detent catching a drag, a
538
612
  choice, preset or seed changing mid-pan, a pan meeting a limit and a switch flipping each give a
539
613
  light tick: `navigator.vibrate` on
540
614
  Android; on iPhone, Safari's switch tick (iOS 17.4–26.4 for every tick; from iOS 26.5, which only
541
615
  plays it for a tap, switches alone). `haptics: false` on the mount turns them off, and so does
542
616
  reduced motion.
617
+ - **Sound.** `sound: true` on the mount gives the panel quiet, short sound effects, synthesized
618
+ live with Web Audio (no files): a slider's detents pitched by where the value sits (a slow drag
619
+ ratchets evenly, a fast scrub ticks lighter, the default's notch is firmer), a soft bump where a
620
+ pan or an arrow meets an end, a switch's two-part throw (off sits lower), a choice's detent that
621
+ rises going forwards and falls going back, a glass fifth for a preset, a die tumbling for Random
622
+ (two clicks for Mutate), a breath drawn backwards for Reset, two rising glass notes for Copy, air
623
+ sliding back or forward for undo and redo, a latch for a group folding or opening, a ting and
624
+ key ticks for hint mode, a firm click for an action and a key going down and up for a captured
625
+ combination. They're off by default and follow the same moments as haptics. `{ volume, theme }`
626
+ sets the panel's level (0–1) and its material: `"glass"` (the default, with a faint ring) or
627
+ `"mech"` (drier). Nothing is made before the page's first click or key, so browsers don't warn.
628
+
629
+ ```ts
630
+ import { sound } from "knobs/panel"
631
+
632
+ mountPanel(controls, host, { sound: true })
633
+ sound.enable(false) // the page's switch: mutes every panel (and your own cues); on by default
634
+ sound.setVolume(0.6)
635
+ sound.play("copy") // a cue of knobs' palette from your own UI
636
+ sound.subscribe(() => speaker.ariaPressed = String(sound.enabled))
637
+ ```
638
+
639
+ With `sound` on, the `?` popover offers **Sounds on/off** for the user, kept in the store's
640
+ `storage`. `sound.output()` hands you the shared context and the bus to connect your own nodes
641
+ to, so volume and mute reach them too (the catalog's lamp hums through it). The approach (struck
642
+ modes, a sine knocked sharp, cadence that lightens repeats, one lazy context) follows
643
+ [Cuelume](https://cuelume.dev) (MIT); the palette is knobs' own.
543
644
  - **Reading values.** `useValue(controls, key)` and `useModified(controls, key?)` change only when
544
645
  their answer does; a panel update touches only the rows that changed.
545
646
 
@@ -604,6 +705,20 @@ mountPanel(station, column, {
604
705
  This is a change of default: before, `?` had a header button unless `buttons` left it out. List
605
706
  `"help"` to keep it (`buttons: ["random", "reset", "copy", "undo", "redo", "help", "close"]`); a
606
707
  `buttons` list that already names `"help"`, as the Lab's does, behaves as before.
708
+ The buttons sit in fixed slots, left to right Undo · Redo, Reset, Copy, Random (then `?` and
709
+ the close X): a list picks which show, not their order. A button left out takes no slot; Undo,
710
+ Redo and Reset keep theirs while they're away (`data-shown="false"`, `disabled`, out of the
711
+ tab order and the accessibility tree).
712
+ - **Height-bounded panels.** The header (title and buttons) stays pinned while the rows scroll
713
+ under it; the preset strip, tabs and search scroll with the rows. Once rows are under it, and
714
+ only then, it frosts and softens its foot with a short fade, scroll-driven with no script, in
715
+ both renderers. It pins inside the nearest scroller: an inline panel is one itself
716
+ (`overflow: auto`), so bound the panel (`height`, `max-height`, or `flex: 1; min-height: 0` in a
717
+ flex column) and it scrolls under its header. To scroll an ancestor instead (a card that holds
718
+ more than the panel), add the class `knobs-pin-ancestor` to the panel (`panel.element`; it makes
719
+ the panel `overflow: visible`) and give the ancestor `overflow: auto`; the header then pins to the
720
+ ancestor's top while the panel passes through it. A panel that nothing scrolls under looks as it
721
+ always did. Where scroll-driven animations aren't supported, the header doesn't pin.
607
722
  - **Folding groups.** A field's `folded: true` (or `groupFolded` on the mount) makes its group
608
723
  foldable, starting folded. Its header folds and opens it with a click, Enter or Space; it's a ↑↓
609
724
  stop of its own, ← folds and → opens. `panel.fold(group, folded?)` does it from code. On a
@@ -678,7 +793,8 @@ defineControls("settings", schema, { ship: () => import("knobs/panel") })
678
793
 
679
794
  A shipped store stays live in production (`enabled` defaults to true) and mounts from the module you
680
795
  name, which your bundler splits into its own chunk. The core never imports the panel itself.
681
- Stores without `ship` are unchanged.
796
+ Stores without `ship` are unchanged. To tune dev-only stores on a deployed page without shipping
797
+ them to visitors, see [`unlock`](#show-hide-and-unlock).
682
798
 
683
799
  In production its chips stay hidden until someone presses **⌥⇧D**, which turns knobs mode on and off
684
800
  for the tab (remembered in `sessionStorage`, with a brief "knobs on" / "knobs off"); then they show on
@@ -704,13 +820,14 @@ Theme with `--k-*` tokens on the panel or any ancestor, or ship a set as a prese
704
820
  | Token | Default | What it does |
705
821
  | --- | --- | --- |
706
822
  | `--k-row-gap` | `0px` | Above 0, each row is its own bar, this far apart; a spring stacks its curve over its sliders |
707
- | `--k-hover-in` / `--k-hover-out` | `150` / `150` | Hover washes in and out (ms) |
823
+ | `--k-hover-in` / `--k-hover-out` | `0` / `120` | Hover washes in and out (ms): at once under the pointer, fading as it leaves |
708
824
  | `--k-chrome` | `1` | `0` drops an inline panel's card: background, border and rim |
709
825
  | `--k-fill-base`, `--k-fill-rest-mix`, `--k-fill-hover-mix`, `--k-fill-active-mix` | surface-hi, 20%, rest, 30% | The fill: accent mixed into the base at rest, hovered and dragged |
710
826
  | `--k-handle`, `--k-handle-rest`, `--k-handle-inset` | accent, `0`, `0px` | The handle's colour, its opacity at rest, and how far inside the fill's edge it sits; it fades where it passes under the label or readout |
711
827
  | `--k-step-resist` | `0.25` | How far a stepped slider's fill follows the pointer off its stop while dragged: `0` snaps, `1` follows freely |
712
828
  | `--k-motion-blur` | `0` | Up to `1`: moving fill edges and selection pills smear by their speed |
713
829
  | `--k-range-blur` | `.5` | Up to `1`: a range band's gliding ends each smear by their own speed (`0` is off) |
830
+ | `--k-curve-blur` | `.5` | Up to `1`: a curve's, easing's or gradient's line, points and stops smear by their speed while they morph between presets (`0` is off) |
714
831
  | `--k-value-font`, `--k-unit-gap`, `--k-unit-color` | font, `.28em`, dim | The readout's face, and the unit's spacing and colour (`currentColor` matches the value) |
715
832
  | `--k-readout-mark` | `0` | `1`: a modified value takes `--k-modified` behind a small dot |
716
833
  | `--k-group-transform`, `--k-group-size`, `--k-group-color`, `--k-group-tracking`, `--k-group-inset` | uppercase, text-cap, dim, `.04em`, inset | Group headings |
@@ -736,6 +853,7 @@ A panel inside an app should look like the app's own controls. Four things get i
736
853
  | `--k-radius`, `--k-panel-radius` | `8px` by density, radius + `4px` | Corners. `0` squares everything, the pill shapes (switches, handles, dots) included |
737
854
  | `--k-thumb` | `#fff` | Switch thumbs and strip handles |
738
855
  | `--k-lift` | `1` | `0` drops the small shadows under pills, thumbs and handles |
856
+ | `--k-shadow-strength` | `.48` (light: `.16`) | How dark the WebGPU renderer draws the panel's drop shadow. Set it beside `--k-shadow` when you change that, so both renderers match (a host blending a dark look into a light one moves both together) |
739
857
  | `--k-frame` | `card` | `flat`: no card inside the host's own card (see below) |
740
858
  | `--k-disabled-opacity` | `.32` | How far a disabled row dims |
741
859
  | `--k-warn`, `--k-critical` | `#ff9f0a`, `#ff453a` (light: `#c56a00`, `#e0352b`) | Readouts' warning and critical colours (meters past `warn` / `critical`, log levels), and a destructive confirm |
@@ -770,7 +888,7 @@ turns off.
770
888
 
771
889
  ## Full page
772
890
 
773
- When there are more controls than a panel holds (a tuning page with a dozen groups), `mountPage` lays them out as a page: a sticky glass header with the title, the presets strip and **Random · Reset · Copy · Undo/Redo**, then **sections** of cards: every group is a card, and each section is a column of them under its heading. As many columns fit as `columnWidth` allows. A long section takes two or more adjacent columns (its cards read down one and on to the next, and the extra columns line up under its heading); when the sections outnumber the columns, short ones stack, in order, with their headings. The split keeps the tallest column as short as it can be. Columns are independent stacks: a card that changes height moves only the cards below it in its own column, never another column.
891
+ When there are more controls than a panel holds (a tuning page with a dozen groups), `mountPage` lays them out as a page: a sticky glass header with the title, the presets strip and the same **Undo · Redo · Reset · Copy · Random** tools, then **sections** of cards: every group is a card, and each section is a column of them under its heading. As many columns fit as `columnWidth` allows. A long section takes two or more adjacent columns (its cards read down one and on to the next, and the extra columns line up under its heading); when the sections outnumber the columns, short ones stack, in order, with their headings. The split keeps the tallest column as short as it can be. Columns are independent stacks: a card that changes height moves only the cards below it in its own column, never another column.
774
892
 
775
893
  Everything moves rather than jumps. A card whose content changes (an editor opening, rows the filter hides, rows added or removed, a custom card swapping its content) eases to its new height and the cards below it glide. The columns re-balance only when the window gains or loses a column (after a brief pause while it's being resized), when cards come or go, when a section folds or opens, and when the filter changes, and then every card glides from where it was to its new place, interruptibly. With reduced motion, layout changes snap.
776
894
 
@@ -920,12 +1038,12 @@ On a page, the keyboard spans every card:
920
1038
  | `↑` / `↓` or `j` / `k` | Every row and custom control, down each column and on to the next |
921
1039
  | `Tab` / `⇧Tab` | Next / previous group (the whole card glides into view where it fits); from the header, the first highlighted card |
922
1040
  | `⌥↑` / `⌥↓` | Glide to the previous / next section, opening it if it's folded; focus goes with it |
923
- | `1`–`9` | Apply the focused group's preset, by its number (in groups without them, digits still edit numbers; `Enter` always does) |
1041
+ | `1`–`9` | On a row with options (a choice, a curve's, easing's or gradient's presets), pick its Nth; elsewhere apply the focused group's preset, by its number (in groups without them, digits still edit numbers; `Enter` always does) |
924
1042
  | `Enter` / `Space` on a section heading or a foldable card's header | Fold or open it (on a section heading or group card, `←` folds and `→` opens too) |
925
1043
  | `/` | Filter rows by label, group or section, live; rows fold shut, cards with nothing left fold away and the rest glide into re-balanced columns; `Esc` clears |
926
1044
  | `s` | Hints for what's on screen, custom cards included |
927
1045
 
928
- Focus that lands off screen (or under the header) glides into view, retargeting from where the last glide was headed so held keys don't fall behind. The rest (`[` `]`, `r`, `g`, `⇧L`, selection, the row menu, hold `\`) works as in a panel.
1046
+ Focus that lands off screen (or under the header) glides into view, retargeting from where the last glide was headed so held keys don't fall behind. The rest (`[` `]`, `r` and `⇧R` scoped to focus, `g`, `Esc` climbing from row to card to page, focusing a card by its empty area or heading, `L`, selection, the row menu, hold `\`) works as in a panel.
929
1047
 
930
1048
  Both renderers draw the page. With `?gpu`, the overlay draws the header's contents, section headings, cards and rows, clipped to below the sticky header, and follows every frame of a glide or fold; the page's background, the header's glass (a real `backdrop-filter`) and custom cards stay in the DOM under the transparent canvas, so they look the same in both. Scrolling and layout motion wake the overlay and it sleeps once the page is still. The demo is `examples/tune.html`: the OpenCode iOS tune page on a simulated iPhone that speaks the tune server's protocol.
931
1049
 
@@ -968,10 +1086,12 @@ const outline = tree(main.pane("outliner").body, {
968
1086
  })
969
1087
  ```
970
1088
 
971
- **`split(container, options)`** lays panes along a `"row"` (default) or a `"column"`, divided by hairlines with a wider grip (`--k-ui-hit`, 9px). Panes with a `size` keep it in pixels; the rest share what's left. Dragging a divider resizes, and each pane slows softly into its `min` and `max` rather than stopping dead. Drag a collapsible pane 40px past its min and it collapses. Double-click a divider to reset it. A focused divider takes `←`/`→` (or `↑`/`↓`; `⇧` for bigger steps), `Home`/`End`, and `Enter` or `Space` to collapse. Collapsing springs the pane down to a slim rail (`--k-ui-rail`), or in a column to its header. The content keeps its open size and slides out by the divider, with no reflow, and a click on the rail opens it again. A collapse can be turned back halfway. With an `id`, the layout is saved (localStorage unless you pass `storage`; `null` saves nothing) and restored before the first paint. Panes take `title`, `count`, `actions` (icon buttons in the header), `rail` (the rail's label), `scroll` and `element` (content to move into the body). The handle has `pane(id)` (`body`, `header`, `size`, `collapsed`, `collapse()`, `resize()`, `title()`), `layout()`, `collapse(id, collapsed?)`, `resize(id, px)`, `reset(id?)` and `dispose()`.
1089
+ **`split(container, options)`** lays panes along a `"row"` (default) or a `"column"`, divided by hairlines with a wider grip (`--k-ui-hit`, 9px). Panes with a `size` keep it in pixels; the rest share what's left. Dragging a divider resizes, and each pane slows softly into its `min` and `max` rather than stopping dead. Drag a collapsible pane 40px past its min and it collapses. Double-click a divider to reset it. A focused divider takes `←`/`→` (or `↑`/`↓`; `⇧` for bigger steps), `Home`/`End`, and `Enter` or `Space` to collapse. Collapsing springs the pane down to a slim rail (`--k-ui-rail`), or in a column to its header. The card folds as one shape: the content keeps its open size and slides out by the divider, with no reflow, fading early, and a click on the rail opens it again. A collapse can be turned back halfway. `fold` sets how: `chevron: "flip"` (default) closes the collapse chevron to a flat line and opens it the other way in about 0.2 s, or `"turn"` spins it with the fold; `title: "travel"` (default) turns the header's title and slides it into the rail's label, or `"fade"` crossfades them; `duration` is the fold's spring in seconds (0.3, no bounce). With an `id`, the layout is saved (localStorage unless you pass `storage`; `null` saves nothing) and restored before the first paint. Panes take `title`, `count`, `actions` (icon buttons in the header), `rail` (the rail's label), `scroll` and `element` (content to move into the body). The handle has `pane(id)` (`body`, `header`, `size`, `collapsed`, `collapse()`, `resize()`, `title()`), `layout()`, `collapse(id, collapsed?)`, `resize(id, px)`, `reset(id?)` and `dispose()`.
972
1090
 
973
1091
  Every change of layout, drag frames and springs included, calls `onLayout(layout)`, fires `knobs-pane-resize` on each pane element and fires `knobs-layout` on the document. The WebGPU renderer listens for `knobs-layout`, so panels in panes stay drawn in place through a drag or a collapse. A panel that a collapse clips away entirely isn't drawn at all. Panes don't create stacking contexts, so a panel's search and editors still sit above the overlay.
974
1092
 
1093
+ Scrolling pane bodies, trees and the command palette's list fade their content out over 20px at an edge with more beyond it, coming in over the first 24px of scroll so an end never pops. Put the class `knobs-scroll-fade` on a scroller of your own for the same thing, or `knobs-scroll-fade-x` on a single line (`overflow: hidden; white-space: nowrap`) to fade it at its end instead of an ellipsis, as a tree's labels do. `--knobs-fade` sets the fade, `--knobs-fade-travel` the scroll it comes in over, and `--knobs-fade-inset` starts the top one below a sticky header. The fades are CSS scroll-driven animations, so no script runs; where those aren't supported there's no fade (a cut-off line takes an ellipsis). Around a panel the WebGPU renderer draws, the mask is left off.
1094
+
975
1095
  **`tree(container, options)`** is an outliner. Rows are virtualised, so 10,000 of them scroll at full speed with about 35 in the DOM. When a branch opens, its children slide out from under it as one block with the rows below, like a drawer, and closing folds them back. The tree takes items with `id`, `label` and `children` (or `getId`, `getLabel` and `getChildren` for your own shape; an array, even empty, makes a parent that takes drops). Options:
976
1096
 
977
1097
  - `icon` and `detail` for a glyph and dimmed text, or `render` for your own row content
@@ -1008,7 +1128,7 @@ import { Split, Tree } from "knobs/ui/react" // or "knobs/ui/solid"
1008
1128
  </Split>
1009
1129
  ```
1010
1130
 
1011
- `Split` puts each child in its pane, in order; the React one portals them, so context and state carry through. `Tree` updates in place when `items`, `selection` or `filter` change, and always calls the latest callbacks. The Lab demo (`examples/lab.html`, `?rows=10000` for a big scene) is a full workspace: an outliner, a viewport over a dock with a Live tab of readouts fed by a fake viewport (`?paused` stops the feed, `?sparks=20` adds sparklines), the inspector column, a `⌘K` palette, tooltips, and a confirm before deleting from the outliner.
1131
+ `Split` puts each child in its pane, in order; the React one portals them, so context and state carry through. `Tree` updates in place when `items`, `selection` or `filter` change, and always calls the latest callbacks. Each wrapper hands its props to the piece in one effect: whichever changed go together, the items or options before the selection, so neither a prop that changes in the same update that mounts the wrapper nor the order Solid runs effects in can lose one. The Lab demo (`examples/lab.html`, `?rows=10000` for a big scene) is a full workspace: an outliner, a viewport over a dock with a Live tab of readouts fed by a fake viewport (`?paused` stops the feed, `?sparks=20` adds sparklines), the inspector column, a `⌘K` palette, tooltips, and a confirm before deleting from the outliner.
1012
1132
 
1013
1133
  ### Overlays: popovers, tooltips, the command palette, confirms and toasts
1014
1134
 
@@ -1062,6 +1182,66 @@ const clock = timeline(dock, {
1062
1182
 
1063
1183
  **`timeline(container, options)`** is a scrubber for time-based work. The ruler counts in seconds or frames (`units`) with ticks that thin out as it zooms (`⌘` wheel, a pinch, `+`/`-`, `0` fits) and pans with the wheel or a trackpad. The playhead follows the hand 1:1, pulls softly toward markers and lands on frames (`snap`). Markers can be added (`M`), dragged, renamed (`Enter`) and removed, and `[`/`]` jump between them; `I` and `O` set a loop range. `Space` plays on one rAF clock that sleeps when nothing moves, and `←`/`→` step a frame (`⇧` ten). A right-click on the ruler, a marker or a range opens a context menu. It reports `onSeek(time, source)` (`"scrub" | "key" | "marker" | "play" | "code"`), `onPlay`, `onMarkers`, `onRanges` and `onCross(marker)`. The handle has `time`, `playing`, `markers`, `ranges`, `view`, `seek`, `play`, `pause`, `toggle`, `update`, `fit`, `zoom`, `focus` and `dispose`. `Timeline` in `knobs/ui/react` and `knobs/ui/solid` follows its props. The Lab's scene clock and Boom retrigger run from one under the viewport.
1064
1184
 
1185
+ ### Activity
1186
+
1187
+ ```ts
1188
+ import { activity } from "knobs/ui"
1189
+
1190
+ const rail = activity(container, {
1191
+ ...look,
1192
+ events: [
1193
+ { id: "asked", kind: "you", actor: "You", title: "Make this a knobs component", time: "48m" },
1194
+ {
1195
+ id: "work",
1196
+ kind: "agent",
1197
+ actor: "Agent", // a group: "Agent · 3 updates", opening in place
1198
+ children: [{ id: "read", title: "Read the task" }, { id: "sketch", title: "Sketched the API" }],
1199
+ },
1200
+ { id: "review", kind: "review", actor: "Review", title: "Walkthrough", duration: 32, current: true },
1201
+ ],
1202
+ selected: "review",
1203
+ onSelect: (id, event, source) => stage.show(id), // keys and clicks
1204
+ onActivate: (id) => open(id), // Enter
1205
+ })
1206
+ rail.add({ id: "pr", kind: "pr", actor: "GitHub", title: "#412 Activity rail", time: "now" })
1207
+ ```
1208
+
1209
+ **`activity(container, options)`** is a vertical rail of what happened: one row per event, each with its kind's icon on one continuous line, who did it, a one-line title that fades at its end when cut off (`knobs-scroll-fade-x`), and optional trailing thumbnails (`thumbs`, four at most), a duration chip (`duration`, seconds or text, "▶ 0:32") and a `time`. The built-in kinds are `you`, `agent`, `proposal`, `review`, `pr`, `build`, `blocked` and `shipped`; any other `kind` takes `icons` and `tints` from the options, and an event can bring its own `icon` (SVG markup, a glyph or a node) and `tint`. `current` puts a quiet ring on the node (the live or latest step), `dimmed` quiets a row, and the selected row's node takes a soft disc in its tint.
1210
+
1211
+ An event with `children` is a group ("Agent · 3 updates" when it has no `title`) that opens in place. Its children sit on the same line as smaller dots, not indented into it, and slide out from under their group with the rows below like a drawer (.22 s open, .18 s close, no bounce). The line is one path drawn between the nodes, stopping clear of each, from the first node to the last, so nothing breaks it and it ends at the last dot; its ends glide as rows come and go, so it grows with the content.
1212
+
1213
+ `set(events)` takes new events: ids keep their identity, so rows that stay glide to their places (.4 s), new ones bloom in from the newest side (a slide of 8px, scale .97 and a 3px blur, .3 s) and gone ones soften out where they are. `newest: "top"` puts the newest first (`add(event)` puts it there), and `update({ events, newest })` changes both at once, so a rail reversed in place glides there. With `follow` (on by default) the rail stays pinned to its newest end when new events arrive while that end is in view. A group opening or closing at the scroll's end keeps the rows above it still, or lets them glide down with the drawer as far as the content shrinks; the scroll never clamps in one frame. Nothing animates on the first paint, times and durations roll when they change, and under reduced motion rows only fade.
1214
+
1215
+ | Key | Action |
1216
+ | --- | --- |
1217
+ | `↑` / `↓`, `Home` / `End` | Move the selection (`onSelect(id, event, "key")`) |
1218
+ | `→` / `←` | Open a group, then step into it / close it, or step out of a child; when they'd do nothing they're left to the page |
1219
+ | `Enter` | `onActivate(id, event)`; on a group with no `onActivate`, open or close it |
1220
+
1221
+ The selection shows as a tint gliding between rows only once keys move it (by distance, .3 s or .42 s); a click selects with no ring, and the tint fades out over 100 ms where it is. Hover washes come in at once and fade out over 100 ms. Keys come from the rail while it has focus, or with `keys: "page"` from anywhere a focused field or widget doesn't take them. The handle has `set`, `add`, `update`, `select(id)` (opens its group; doesn't call `onSelect`), `selected`, `expand`, `isExpanded`, `focus` and `destroy`; `Activity` in `knobs/ui/react` and `knobs/ui/solid` follows `events`, `selected` and `newest`. `--k-activity-line`, `--k-activity-select`, `--k-activity-row`, `--k-activity-gutter` and one `--k-activity-<kind>` per kind tune it, in dark and light. The pure parts are exported: `flattenActivity`, `navigateActivity`, `railSegments`, `stackRows`, `diffActivity`, `showsSelection`, `formatDuration`.
1222
+
1223
+ ### Option picker
1224
+
1225
+ ```tsx
1226
+ import { OptionPicker } from "knobs/ui/solid"
1227
+ import { KnobsPanel } from "knobs/solid"
1228
+
1229
+ <OptionPicker
1230
+ options={variations().map((v) => ({ id: v.id, label: v.name, suggested: v.id === suggested() }))}
1231
+ selected={variation()}
1232
+ chosen={chosen()}
1233
+ onSelect={(id) => setVariation(id)}
1234
+ onChoose={(id) => choose(id)}
1235
+ digits="page"
1236
+ >
1237
+ {(option) => <KnobsPanel controls={paramsOf(option.id)} header={false} variants={false} />}
1238
+ </OptionPicker>
1239
+ ```
1240
+
1241
+ **`optionPicker(container, options)`** lists a proposal's options (1–n), each with its number, its name, a "Suggested" mark (`suggested`, worded by `suggestedLabel`) and a check on the `chosen` one, which blooms in. An option's `thumb` (an image URL) shows small between the number and the name, cover-fit with a hairline: it loads lazily and fades in once loaded, and nothing is drawn while it loads or if it fails (`--k-picker-thumb` sizes it, 24 px in a 34 px row). Every row is numbered, 10, 11 and on in the same two-digit column, though only `1`–`9` pick. The selected option sits on a card that glides between options in one move (.3 s), aimed at where the option will be once the last one's parameters have closed (the first time an option opens, its parameters mount shut a frame ahead and the change starts once their height has settled, so the card aims at their real height), and its parameters open under it in place while the last option's close (.22 s open, .18 s close, the content fading and drifting with the edge). `↑`/`↓` and `Home`/`End` move between options, `1`–`9` pick one, `Enter` calls `onChoose(id)` and `Tab` goes into the parameters. Digits come from the picker and its parameters while focus is inside (`digits: "page"` takes them from anywhere; `false` for none); a focused row that picks its own options with digits, and text fields, keep theirs.
1242
+
1243
+ The parameters are inset by the picker (`--k-picker-pad`, `0 8px 8px`), whatever a panel's own pad. They are real knobs panels: `body(option, element)` mounts whatever an option needs into `element` the first time it opens and returns a cleanup, and it stays mounted while closed, so a panel keeps its state. The picker is plain DOM chrome in `knobs/ui` and the panels come from `knobs/panel`, so `knobs/ui` stays free of the panel, and the parameters draw with the DOM or the WebGPU renderer as any panel does (nothing around them makes a stacking context at rest). `OptionPicker` in `knobs/ui/solid` and `knobs/ui/react` renders `children(option)` there instead, in the component's context. The handle has `set(options)`, `select(id)`, `selected`, `choose(id)`, `chosen`, `focus()` and `destroy()`. A `select(id)` (or a `selected` or `chosen`) for an option that isn't there yet is kept, and the `set()` that brings it picks it; a later `select` replaces it.
1244
+
1065
1245
  ### Ideas grid (bento)
1066
1246
 
1067
1247
  ```ts
@@ -1123,6 +1303,7 @@ Override CSS variables on `:root` or `.knobs-panel`:
1123
1303
  --k-blur: 1; /* motion strength: readout blur and pill stretch */
1124
1304
  --k-motion-blur: 0; /* up to 1: moving fill edges and pills smear by their speed */
1125
1305
  --k-range-blur: .5; /* up to 1: a range band's gliding ends smear by their speed */
1306
+ --k-curve-blur: .5; /* up to 1: morphing curves, easings and gradients smear by their speed */
1126
1307
  --k-step-resist: 0.25; /* a stepped slider's pull off its stop (0 snaps, 1 follows) */
1127
1308
  --k-glow: 0.5;
1128
1309
  --k-roll: 420; /* readout roll (ms) */
@@ -1158,8 +1339,82 @@ const die = defineControls("die", schema, {
1158
1339
  })
1159
1340
  ```
1160
1341
 
1342
+ A reset that went back to the active preset rather than the defaults carries `base: true` in the third argument.
1343
+
1161
1344
  `patch` holds only the fields that changed. The die page (`examples/die.ts`) rolls its demo dice this way.
1162
1345
 
1346
+ ## Show, hide and unlock
1347
+
1348
+ **Backtick.** `` ` `` shows and hides every panel and chip on the page at once, the way a page's
1349
+ dev tools come and go together. It works anywhere outside a text field, and never with ⌘, ⌃ or ⌥
1350
+ held. One key for the whole page, not just the focused panel: you hide them to see the page, and a
1351
+ panel you can't see can't have focus to aim at. If any panel is showing the key hides them all;
1352
+ otherwise it shows them all. Panels mounted later (scrolled into view) follow the last press.
1353
+
1354
+ ```ts
1355
+ hero.mount(column) // ` hides and shows it with the page's other panels
1356
+ hero.mount(column, { toggleKey: false }) // always shown
1357
+ defineControls("hero", schema, { toggleKey: "F8" }) // its panels and chips listen for F8
1358
+ ```
1359
+
1360
+ - **Remembered per store**, in its `storage`, so a reload puts the page back as you left it, with
1361
+ no animation. Stores with `storage: null` start shown.
1362
+ - **Hidden is hidden.** The panel takes no space and is `hidden` and `inert`: no Tab stop, no hint
1363
+ code, no click, and the WebGPU overlay stops drawing it. Chips hide too, hint mode stops, and
1364
+ focus that was in a panel goes back there when it shows again.
1365
+ - **Motion.** Leaving, a panel fades with a little blur (4 px at most) and scale (.96 at least) on a
1366
+ .25 s spring with no bounce; arriving is the same in reverse on .32 s. An inline panel also folds
1367
+ its space shut, its content gone before the last of the height, so the page below glides up.
1368
+ Pressing again mid-way turns it around from where it is. Both renderers draw the same; reduced
1369
+ motion only fades.
1370
+ - **Off by default** for shipped stores (`ship`, product UI) and full pages (`mountPage`, the whole
1371
+ tool); give them a `toggleKey` to opt in. The `?` popover lists the key.
1372
+ - Your own dev tools can join with ``toggleLayer(document).register({ id, key: "`", storage,
1373
+ root, show })`` from `knobs/panel`, or keep their own handler: knobs doesn't claim the key, so a
1374
+ page's backtick listener still runs and both come and go together.
1375
+
1376
+ **A secret unlock for a live site.** A production build locks every store to its constants and
1377
+ leaves the panel out (see below). To tune a deployed page anyway, let the deploy be unlocked:
1378
+
1379
+ ```ts
1380
+ import { defineControls, knob, unlock } from "knobs"
1381
+
1382
+ const hero = defineControls("hero", { radius: knob(12, { min: 0, max: 40, step: 1 }) })
1383
+ hero.mount(column) // nothing for visitors: hero.get() is the shipped constants
1384
+
1385
+ unlock({ panel: () => import("knobs/panel") })
1386
+ ```
1387
+
1388
+ Visitors download no panel code and see the constants. You unlock it by typing **knobs** anywhere
1389
+ outside a text field, opening the page with **`?knobs`**, or tapping the **top-right corner three
1390
+ times** quickly (on a phone). Then the panel chunk loads, panels and chips appear, values come from
1391
+ this browser's storage, and a brief "knobs unlocked" confirms it. It's remembered in localStorage
1392
+ until you lock it again: the same secret again, `?knobs=off`, or the handle's `lock()`. Locking takes
1393
+ the panels down and puts the constants back, which is also how to check what visitors see.
1394
+
1395
+ ```ts
1396
+ import { KONAMI, unlock } from "knobs"
1397
+
1398
+ const gate = unlock({
1399
+ panel: () => import("knobs/panel"),
1400
+ sequence: KONAMI, // a word ("knobs"), key names (↑↑↓↓←→←→BA), or false
1401
+ query: "tune", // ?tune unlocks, ?tune=off locks; false for none
1402
+ corner: "top-left", // three taps there; false for none
1403
+ persist: false, // this page view only
1404
+ })
1405
+ gate.unlocked // also gate.unlock(), gate.lock(), gate.dispose()
1406
+ ```
1407
+
1408
+ It's a secret, not access control: anyone who finds it can tune the page in their own browser, and
1409
+ nothing they change reaches anyone else. In development `unlock` changes nothing, since stores are
1410
+ live there already. Once unlocked, the backtick hides and shows panels as usual.
1411
+
1412
+ How others do it: Leva and Tweakpane have a `hidden` prop; dat.gui hides every GUI on `H` (lil-gui
1413
+ dropped it); Theatre.js toggles its studio with ⌥\\; Stripe's Workbench opens with `~`; Vercel's
1414
+ toolbar hides with ⌘. and only shows to signed-in team members. A typed word, a query flag kept in
1415
+ localStorage and a corner gesture for phones cover a keyboard, a link and a touch screen without an
1416
+ account.
1417
+
1163
1418
  ## Explore in development. Ship constants.
1164
1419
 
1165
1420
  ```ts
@@ -1167,7 +1422,7 @@ splat.locked // frozen schema defaults
1167
1422
  splat.get() // live while enabled; the exact locked object otherwise
1168
1423
  ```
1169
1424
 
1170
- The package has two core exports selected through the standard `development` condition. Vite selects it automatically in dev. The default export is production-safe: **disabled unconditionally, even if `enabled: true` is passed**. `attach()` and `mount()` are no-ops, and their panel imports are erased from that entry.
1425
+ The package has two core exports selected through the standard `development` condition. Vite selects it automatically in dev. The default export is production-safe: **disabled unconditionally, even if `enabled: true` is passed**. `attach()` and `mount()` are no-ops, and their panel imports are erased from that entry. The one way back is a page that calls [`unlock`](#show-hide-and-unlock) and someone who knows its secret; the panel then loads from the module `unlock` names.
1171
1426
 
1172
1427
  ## API Summary
1173
1428
 
@@ -1175,9 +1430,9 @@ The package has two core exports selected through the standard `development` con
1175
1430
 
1176
1431
  | Function | Behavior |
1177
1432
  | --- | --- |
1178
- | `knob(value, { min, max, step?, unit?, shortcut?, label?, group?, groupLabel?, section?, sectionLabel? })` | Numeric slider control with rolling readout and inline editor. |
1433
+ | `knob(value, { min, max, step?, unit?, decimals?, detents?, shortcut?, label?, group?, groupLabel?, section?, sectionLabel? })` | Numeric slider control with rolling readout and inline editor. `detents: false` lets drags glide through the default, the base and the middle. |
1179
1434
  | `choice(value, ["a", "b"], { label?, group?, shortcut? }?)` | Nonempty string choices with literal union inference. |
1180
- | `toggle(value, { label?, group?, shortcut? }?)` | Boolean switch control. |
1435
+ | `toggle(value, { label?, group?, shortcut?, random? }?)` | Boolean switch control. Out of Random and Mutate unless `random: true`. |
1181
1436
  | `spring({ duration?, bounce? }?, meta?)` | SwiftUI-style spring: two sliders beside its live curve. |
1182
1437
  | `color("#hex" \| "oklch(L C H)" \| { l, c, h }, meta?)` | OKLCH colour kept inside sRGB, as an `oklch()` string; swatch, hex and hue/chroma/lightness strips. |
1183
1438
  | `point({ x, y }, { min, max, step?, ...meta })` | 2D pad; bounds and step take a number or `{ x, y }`. |
@@ -1196,10 +1451,12 @@ The package has two core exports selected through the standard `development` con
1196
1451
  | `fromDescriptors(descriptors)` | Converts runtime `ControlDescriptor[]` into a `Schema`. |
1197
1452
  | `createRemoteControls(id, descriptors, options)` | Creates a remote-synced `Controls` store with `.swift(group?)` codegen and `.update(descriptors, values, { land })`; incoming values skip keys in hand or changed here in the last 600 ms, and the newest lands when the hold ends. |
1198
1453
  | `fromSettings(groups, home?)` | A hosted prototype's settings sections as descriptors and values. |
1199
- | `action(label, { run })` / `flatSpring({ duration, bounce }, meta)` | A button row; two flat number fields drawn as one spring card. |
1200
- | Field meta `random?`, `disabled?`, `readOnly?`, `folded?`, `tab?`, `subsection?` | On any field or descriptor: `random: false` stays out of Random and Mutate; `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. |
1454
+ | `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). |
1455
+ | `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 }`. |
1456
+ | 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. |
1201
1457
  | 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. |
1202
- | `mountPanel(controls, container, options?)` (`knobs/panel`) | An inline panel. Beside theme, density, renderer and keys: `preset`, `header` (`false` or `{ title?, buttons? }`), `session`, `groupFolded`, `subsectionFolded`, `tabs`, `tab`, `onTab`, `search`, `groupRandom(group, mode)`, `rowAccessory(key)`, `marks`, `haptics`. The handle adds `panel.fold(group, folded?)`, `panel.tab(id?)`, `panel.search(query?, chips?)` (returns `{ query, chips }`) and `panel.updateAccessories()`. |
1458
+ | `mountPanel(controls, container, options?)` (`knobs/panel`) | An inline panel. Beside theme, density, renderer and keys: `preset`, `header` (`false` or `{ title?, buttons? }`), `crumbs` (`false` drops 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()`. |
1459
+ | `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. |
1203
1460
  | `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. |
1204
1461
  | `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[] }`. |
1205
1462
  | `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`. |
@@ -1218,22 +1475,25 @@ The package has two core exports selected through the standard `development` con
1218
1475
  - `controls.setMixed(keys, on?)` / `controls.isMixed(key)`
1219
1476
  - `controls.hold(key, on?)` / `controls.isHeld(key)` / `controls.landing`
1220
1477
  - `controls.action(key, item?)` — press an action row, or a toolbar's item
1478
+ - `controls.shortcut(key, item?)` / `controls.rebind(key, combo, item?)` — an action's (or toolbar button's) shortcut in force, and a rebinding saved with the values (`""` none, `undefined` back to the declared one)
1221
1479
  - `controls.watch(key, value)` / `controls.watched(key)` — push to a readout row (`null` clears it); the latest value
1222
- - `controls.reset(keys?)` / `controls.resetKey(key)` / `controls.resetGroup(group)`
1223
- - `controls.isModified(key?)` / `controls.modified()`
1480
+ - `controls.reset(keys?)` / `controls.resetKey(key)` / `controls.resetGroup(group)`: back to the active preset, else the defaults
1481
+ - `controls.isModified(key?)` (off the active preset, else the defaults) / `controls.canReset(keys?)` / `controls.modified()` (off the defaults)
1224
1482
  - `controls.undo()` / `controls.redo()` / `controls.canUndo` / `controls.canRedo`
1225
1483
  - `controls.randomize(keys?)` / `controls.mutate(keys?)`: skip locked fields; one undo step each
1226
1484
  - `controls.lock(keys, locked = true)` / `controls.isLocked(key)`: locks are saved with the values (remote controls keep them in memory)
1227
1485
  - `controls.compare(hold)` / `controls.comparing`: while held, `get()` returns the previous undo entry; no history, no storage
1228
- - `controls.preset(id)` / `controls.matchingPreset()`
1486
+ - `controls.preset(id, { coalesce? })` / `controls.matchingPreset()`
1487
+ - `controls.activePreset()` / `controls.activeGroupPreset(group)` / `controls.baseValues()`: the base Reset returns to and Random explores around
1229
1488
  - `controls.groupPresets` / `controls.groupPreset(group, index, { coalesce? })` / `controls.matchingGroupPreset(group)` / `controls.groupPresetDisplay`
1230
1489
  - `controls.groupLabel(id)` / `controls.sectionLabel(id)` / `controls.presetLabel(id)`: display labels (`groupLabels`, `sectionLabels`, `presetLabels`, `defaultLabel`, or the fields' `groupLabel` / `sectionLabel`)
1231
1490
  - `controls.groupDescription(group): string | undefined` / `controls.sectionDescription(section): string | undefined`: `groupDescriptions` / `sectionDescriptions`, else the fields' `groupDescription` / `sectionDescription`
1232
1491
  - `controls.groupFolded` / `controls.sectionFolded`: how groups and a page's sections start, as given to `defineControls` or `createRemoteControls` (`groupFolded?: Record<string, boolean>`, `sectionFolded?: SectionFolded`); mounts read them under their own
1233
1492
  - `controls.code("ts" | "json" | "jsx", values?)`
1234
- - `controls.variants.list()` / `.save(name?)` / `.load(id)` / `.remove(id)` / `.export()` / `.import(json)`
1493
+ - `controls.variants.list()` / `.save(name?)` / `.load(id, { coalesce? })` / `.rename(id, name)` / `.remove(id)` / `.export()` / `.import(json)`
1235
1494
  - `controls.attach(element, chip?)` — floating chip + anchored panel; `chip` (`{ label?, reveal?, shortcut? }`) overrides the store's `chip` option
1236
1495
  - `controls.mount(container, options?)` — inline panel
1496
+ - `controls.toggleKey` — the key that shows and hides its panels and chips (`` "`" `` by default, `false` for shipped stores); `toggleKey` on the mount overrides it
1237
1497
 
1238
1498
  ### Workspace pieces (`knobs/ui`)
1239
1499
 
@@ -1249,6 +1509,8 @@ The package has two core exports selected through the standard `development` con
1249
1509
  | `toast(message, { tone?, linger?, action?, within? })` | A status pill at the bottom centre. |
1250
1510
  | `menu(anchor, items, { placement?, label?, onSelect?, onClose? })` / `contextMenu(target, items, options?)` | Menus with icons, shortcut chips, checks, radios and submenus; a right-click or menu-key menu. |
1251
1511
  | `timeline(container, { duration, time?, fps?, units?, markers?, ranges?, snap?, onSeek?, … })` | A zoomable ruler and playhead with markers, loop ranges and playback. |
1512
+ | `activity(container, { events, selected?, onSelect?, onActivate?, onToggle?, onPlay?, newest?, follow?, keys?, icons?, tints? })` | A rail of events on one continuous line, with groups that open in place; `{ set, add, select, expand, focus, destroy }`. |
1513
+ | `optionPicker(container, { options, selected?, chosen?, onSelect?, onChoose?, body?, digits? })` | Options 1–n on a gliding card, the selected one's parameters (a panel) open under it. |
1252
1514
  | `bento(container, { items, gap?, tolerance?, rowHeight?, fit?, blur?, picks?, onPick?, onZoom?, onActive? })` | An ideas grid: a bento of images and live content that zooms an item to fill the view; picks with Space. |
1253
1515
  | `planBento(aspects, { width, height, gap?, tolerance?, rowHeight?, groups?, hero? })` | The bento's layout on its own: rects, mode, coverage and crop. |
1254
1516
  | `overlayLook({ preset?, theme? })` | The look every overlay takes. |