@kitlangton/knobs 0.1.0 → 0.2.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.
- package/README.md +174 -30
- package/dist/dev/{chunk-e13znr7c.js → chunk-4km87zfx.js} +3 -3
- package/dist/dev/{chunk-ktsn42p7.js → chunk-5cr85vv6.js} +7448 -5390
- package/dist/dev/{chunk-kdewv0mt.js → chunk-693jd1gj.js} +171 -70
- package/dist/dev/{chunk-qtt698hp.js → chunk-gezhc24g.js} +21 -7
- package/dist/dev/index.js +377 -106
- package/dist/index.js +392 -108
- package/dist/panel.js +9518 -6749
- package/dist/types/core/random.d.ts +61 -1
- package/dist/types/core/remote.d.ts +3 -1
- package/dist/types/core/schema.d.ts +30 -6
- package/dist/types/core/store.d.ts +68 -6
- package/dist/types/core/variants.d.ts +10 -2
- package/dist/types/index.d.ts +3 -2
- package/dist/types/panel/editors.d.ts +11 -0
- package/dist/types/panel/flash.d.ts +13 -0
- package/dist/types/panel/gpu/renderer.d.ts +78 -6
- package/dist/types/panel/haptics.d.ts +6 -2
- package/dist/types/panel/has.d.ts +0 -1
- package/dist/types/panel/index.d.ts +1 -0
- package/dist/types/panel/inspector.d.ts +32 -2
- package/dist/types/panel/optkeys.d.ts +7 -0
- package/dist/types/panel/presets.d.ts +17 -5
- package/dist/types/panel/ramp-morph.d.ts +117 -0
- package/dist/types/panel/ramps.d.ts +1 -1
- package/dist/types/panel/reel.d.ts +43 -0
- package/dist/types/panel/scope.d.ts +19 -0
- package/dist/types/panel/slider.d.ts +1 -1
- package/dist/types/panel/sound.d.ts +137 -0
- package/dist/types/panel/styles.d.ts +1 -1
- package/dist/types/panel/toolbar-grid.d.ts +6 -0
- package/dist/types/ui/activity-css.d.ts +1 -0
- package/dist/types/ui/activity-model.d.ts +122 -0
- package/dist/types/ui/activity.d.ts +67 -0
- package/dist/types/ui/follow.d.ts +17 -0
- package/dist/types/ui/index.d.ts +5 -1
- package/dist/types/ui/look.d.ts +9 -0
- package/dist/types/ui/picker-css.d.ts +1 -0
- package/dist/types/ui/picker-model.d.ts +53 -0
- package/dist/types/ui/picker.d.ts +67 -0
- package/dist/types/ui/react.d.ts +27 -0
- package/dist/types/ui/solid.d.ts +23 -0
- package/dist/types/ui/split-model.d.ts +54 -0
- package/dist/types/ui/split.d.ts +17 -0
- package/dist/ui-react.js +3239 -567
- package/dist/ui-solid.js +3123 -472
- package/dist/ui.js +2688 -100
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ 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 **
|
|
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
|
|
|
@@ -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
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
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.
|
|
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:
|
|
@@ -311,32 +340,35 @@ Panels and switchers are keyboard-first:
|
|
|
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
341
|
| `←` / `→` 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 |
|
|
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` |
|
|
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). 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
353
|
| `⌥` + 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 |
|
|
354
|
+
| 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 (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 |
|
|
325
355
|
| `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
356
|
| `Tab` / `⇧Tab` | Next/previous control, wrapping inside floating panels |
|
|
327
357
|
| `f` | Focus first row |
|
|
328
|
-
| `r` / `⇧R` | Randomize
|
|
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) |
|
|
358
|
+
| `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 |
|
|
359
|
+
| `⌥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) |
|
|
360
|
+
| `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) |
|
|
331
361
|
| `⇧L` | Lock or unlock the focused row (or the selection) from Random and Mutate |
|
|
332
362
|
| `⇧`-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
363
|
| Right-click / `⇧F10` / `ContextMenu` | Menu for the row or selection: Lock from Random, Randomize, Reset, Copy; `↑↓`, a letter, `Enter`, `Esc` |
|
|
334
364
|
| Hold `\` | Compare: show the values from before the last change while held (the title dims); nothing is recorded |
|
|
335
365
|
| `⇧C` | Copy the current values (or the selected fields) as code |
|
|
336
|
-
| `⇧S` | Save the current values as a new preset chip |
|
|
366
|
+
| `⇧S` | Save the current values as a new preset chip and type its name (`Enter` keeps it, `Esc` keeps "Variant N") |
|
|
367
|
+
| `F2` / `Enter` on the strip, double-click a chip | Rename a saved preset |
|
|
337
368
|
| `u` / `z` | Undo (`⇧u` / `⇧z` redo); slider drags coalesce into a single history entry |
|
|
338
369
|
| `?` | Toggle keyboard shortcuts popover (the title's tooltip mentions it) |
|
|
339
|
-
| `Esc` | Close shortcuts popover
|
|
370
|
+
| `Esc` | Close the shortcuts popover; on a focused floating panel (nothing deeper focused), close it and restore focus to its chip |
|
|
371
|
+
| Click a card's empty area or heading | Focus the card (a soft wash shows it); `↑` / `↓` go to its first or last row, and `r`, `⌥R` and `⇧R` act on its group |
|
|
340
372
|
|
|
341
373
|
### Hint mode across the page
|
|
342
374
|
|
|
@@ -515,7 +547,7 @@ Without Solid: `remote.update(descriptors, values, { land })` and `mountPanel(re
|
|
|
515
547
|
ranges and labels. `onSet` reports `"d"` and `"b"` separately; values stay flat. Locally:
|
|
516
548
|
`defineControls("m", { ...flatSpring({ duration: 0.45, bounce: 0.3 }, { label: "Spring" }) })`.
|
|
517
549
|
- **Actions and settings.** `{ kind: "action" }` (or `action("Replay")` locally) is a button row
|
|
518
|
-
that calls `onAction(key)
|
|
550
|
+
that calls `onAction(key)`; `shortcut` (a combination), `global` and `rebind` work as `action`'s. Choices take `{ id, label }` options. `fromSettings(groups, home)` maps
|
|
519
551
|
a hosted prototype's settings sections (choices, switches, buttons, titled sections becoming
|
|
520
552
|
groups) to descriptors and values, with what the prototype first reported as each default.
|
|
521
553
|
- **Choices** are segmented while their labels fit with room to spare and there are at most four,
|
|
@@ -540,6 +572,33 @@ Without Solid: `remote.update(descriptors, values, { land })` and `mountPanel(re
|
|
|
540
572
|
Android; on iPhone, Safari's switch tick (iOS 17.4–26.4 for every tick; from iOS 26.5, which only
|
|
541
573
|
plays it for a tap, switches alone). `haptics: false` on the mount turns them off, and so does
|
|
542
574
|
reduced motion.
|
|
575
|
+
- **Sound.** `sound: true` on the mount gives the panel quiet, short sound effects, synthesized
|
|
576
|
+
live with Web Audio (no files): a slider's detents pitched by where the value sits (a slow drag
|
|
577
|
+
ratchets evenly, a fast scrub ticks lighter, the default's notch is firmer), a soft bump where a
|
|
578
|
+
pan or an arrow meets an end, a switch's two-part throw (off sits lower), a choice's detent that
|
|
579
|
+
rises going forwards and falls going back, a glass fifth for a preset, a die tumbling for Random
|
|
580
|
+
(two clicks for Mutate), a breath drawn backwards for Reset, two rising glass notes for Copy, air
|
|
581
|
+
sliding back or forward for undo and redo, a latch for a group folding or opening, a ting and
|
|
582
|
+
key ticks for hint mode, a firm click for an action and a key going down and up for a captured
|
|
583
|
+
combination. They're off by default and follow the same moments as haptics. `{ volume, theme }`
|
|
584
|
+
sets the panel's level (0–1) and its material: `"glass"` (the default, with a faint ring) or
|
|
585
|
+
`"mech"` (drier). Nothing is made before the page's first click or key, so browsers don't warn.
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
import { sound } from "knobs/panel"
|
|
589
|
+
|
|
590
|
+
mountPanel(controls, host, { sound: true })
|
|
591
|
+
sound.enable(false) // the page's switch: mutes every panel (and your own cues); on by default
|
|
592
|
+
sound.setVolume(0.6)
|
|
593
|
+
sound.play("copy") // a cue of knobs' palette from your own UI
|
|
594
|
+
sound.subscribe(() => speaker.ariaPressed = String(sound.enabled))
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
With `sound` on, the `?` popover offers **Sounds on/off** for the user, kept in the store's
|
|
598
|
+
`storage`. `sound.output()` hands you the shared context and the bus to connect your own nodes
|
|
599
|
+
to, so volume and mute reach them too (the catalog's lamp hums through it). The approach (struck
|
|
600
|
+
modes, a sine knocked sharp, cadence that lightens repeats, one lazy context) follows
|
|
601
|
+
[Cuelume](https://cuelume.dev) (MIT); the palette is knobs' own.
|
|
543
602
|
- **Reading values.** `useValue(controls, key)` and `useModified(controls, key?)` change only when
|
|
544
603
|
their answer does; a panel update touches only the rows that changed.
|
|
545
604
|
|
|
@@ -604,6 +663,20 @@ mountPanel(station, column, {
|
|
|
604
663
|
This is a change of default: before, `?` had a header button unless `buttons` left it out. List
|
|
605
664
|
`"help"` to keep it (`buttons: ["random", "reset", "copy", "undo", "redo", "help", "close"]`); a
|
|
606
665
|
`buttons` list that already names `"help"`, as the Lab's does, behaves as before.
|
|
666
|
+
The buttons sit in fixed slots, left to right Undo · Redo, Reset, Copy, Random (then `?` and
|
|
667
|
+
the close X): a list picks which show, not their order. A button left out takes no slot; Undo,
|
|
668
|
+
Redo and Reset keep theirs while they're away (`data-shown="false"`, `disabled`, out of the
|
|
669
|
+
tab order and the accessibility tree).
|
|
670
|
+
- **Height-bounded panels.** The header (title and buttons) stays pinned while the rows scroll
|
|
671
|
+
under it; the preset strip, tabs and search scroll with the rows. Once rows are under it, and
|
|
672
|
+
only then, it frosts and softens its foot with a short fade, scroll-driven with no script, in
|
|
673
|
+
both renderers. It pins inside the nearest scroller: an inline panel is one itself
|
|
674
|
+
(`overflow: auto`), so bound the panel (`height`, `max-height`, or `flex: 1; min-height: 0` in a
|
|
675
|
+
flex column) and it scrolls under its header. To scroll an ancestor instead (a card that holds
|
|
676
|
+
more than the panel), add the class `knobs-pin-ancestor` to the panel (`panel.element`; it makes
|
|
677
|
+
the panel `overflow: visible`) and give the ancestor `overflow: auto`; the header then pins to the
|
|
678
|
+
ancestor's top while the panel passes through it. A panel that nothing scrolls under looks as it
|
|
679
|
+
always did. Where scroll-driven animations aren't supported, the header doesn't pin.
|
|
607
680
|
- **Folding groups.** A field's `folded: true` (or `groupFolded` on the mount) makes its group
|
|
608
681
|
foldable, starting folded. Its header folds and opens it with a click, Enter or Space; it's a ↑↓
|
|
609
682
|
stop of its own, ← folds and → opens. `panel.fold(group, folded?)` does it from code. On a
|
|
@@ -704,13 +777,14 @@ Theme with `--k-*` tokens on the panel or any ancestor, or ship a set as a prese
|
|
|
704
777
|
| Token | Default | What it does |
|
|
705
778
|
| --- | --- | --- |
|
|
706
779
|
| `--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` | `
|
|
780
|
+
| `--k-hover-in` / `--k-hover-out` | `0` / `120` | Hover washes in and out (ms): at once under the pointer, fading as it leaves |
|
|
708
781
|
| `--k-chrome` | `1` | `0` drops an inline panel's card: background, border and rim |
|
|
709
782
|
| `--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
783
|
| `--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
784
|
| `--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
785
|
| `--k-motion-blur` | `0` | Up to `1`: moving fill edges and selection pills smear by their speed |
|
|
713
786
|
| `--k-range-blur` | `.5` | Up to `1`: a range band's gliding ends each smear by their own speed (`0` is off) |
|
|
787
|
+
| `--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
788
|
| `--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
789
|
| `--k-readout-mark` | `0` | `1`: a modified value takes `--k-modified` behind a small dot |
|
|
716
790
|
| `--k-group-transform`, `--k-group-size`, `--k-group-color`, `--k-group-tracking`, `--k-group-inset` | uppercase, text-cap, dim, `.04em`, inset | Group headings |
|
|
@@ -770,7 +844,7 @@ turns off.
|
|
|
770
844
|
|
|
771
845
|
## Full page
|
|
772
846
|
|
|
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 **
|
|
847
|
+
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
848
|
|
|
775
849
|
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
850
|
|
|
@@ -920,12 +994,12 @@ On a page, the keyboard spans every card:
|
|
|
920
994
|
| `↑` / `↓` or `j` / `k` | Every row and custom control, down each column and on to the next |
|
|
921
995
|
| `Tab` / `⇧Tab` | Next / previous group (the whole card glides into view where it fits); from the header, the first highlighted card |
|
|
922
996
|
| `⌥↑` / `⌥↓` | Glide to the previous / next section, opening it if it's folded; focus goes with it |
|
|
923
|
-
| `1`–`9` |
|
|
997
|
+
| `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
998
|
| `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
999
|
| `/` | 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
1000
|
| `s` | Hints for what's on screen, custom cards included |
|
|
927
1001
|
|
|
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
|
|
1002
|
+
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
1003
|
|
|
930
1004
|
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
1005
|
|
|
@@ -968,10 +1042,12 @@ const outline = tree(main.pane("outliner").body, {
|
|
|
968
1042
|
})
|
|
969
1043
|
```
|
|
970
1044
|
|
|
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()`.
|
|
1045
|
+
**`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
1046
|
|
|
973
1047
|
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
1048
|
|
|
1049
|
+
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.
|
|
1050
|
+
|
|
975
1051
|
**`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
1052
|
|
|
977
1053
|
- `icon` and `detail` for a glyph and dimmed text, or `render` for your own row content
|
|
@@ -1008,7 +1084,7 @@ import { Split, Tree } from "knobs/ui/react" // or "knobs/ui/solid"
|
|
|
1008
1084
|
</Split>
|
|
1009
1085
|
```
|
|
1010
1086
|
|
|
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.
|
|
1087
|
+
`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
1088
|
|
|
1013
1089
|
### Overlays: popovers, tooltips, the command palette, confirms and toasts
|
|
1014
1090
|
|
|
@@ -1062,6 +1138,66 @@ const clock = timeline(dock, {
|
|
|
1062
1138
|
|
|
1063
1139
|
**`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
1140
|
|
|
1141
|
+
### Activity
|
|
1142
|
+
|
|
1143
|
+
```ts
|
|
1144
|
+
import { activity } from "knobs/ui"
|
|
1145
|
+
|
|
1146
|
+
const rail = activity(container, {
|
|
1147
|
+
...look,
|
|
1148
|
+
events: [
|
|
1149
|
+
{ id: "asked", kind: "you", actor: "You", title: "Make this a knobs component", time: "48m" },
|
|
1150
|
+
{
|
|
1151
|
+
id: "work",
|
|
1152
|
+
kind: "agent",
|
|
1153
|
+
actor: "Agent", // a group: "Agent · 3 updates", opening in place
|
|
1154
|
+
children: [{ id: "read", title: "Read the task" }, { id: "sketch", title: "Sketched the API" }],
|
|
1155
|
+
},
|
|
1156
|
+
{ id: "review", kind: "review", actor: "Review", title: "Walkthrough", duration: 32, current: true },
|
|
1157
|
+
],
|
|
1158
|
+
selected: "review",
|
|
1159
|
+
onSelect: (id, event, source) => stage.show(id), // keys and clicks
|
|
1160
|
+
onActivate: (id) => open(id), // Enter
|
|
1161
|
+
})
|
|
1162
|
+
rail.add({ id: "pr", kind: "pr", actor: "GitHub", title: "#412 Activity rail", time: "now" })
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
**`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.
|
|
1166
|
+
|
|
1167
|
+
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.
|
|
1168
|
+
|
|
1169
|
+
`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.
|
|
1170
|
+
|
|
1171
|
+
| Key | Action |
|
|
1172
|
+
| --- | --- |
|
|
1173
|
+
| `↑` / `↓`, `Home` / `End` | Move the selection (`onSelect(id, event, "key")`) |
|
|
1174
|
+
| `→` / `←` | 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 |
|
|
1175
|
+
| `Enter` | `onActivate(id, event)`; on a group with no `onActivate`, open or close it |
|
|
1176
|
+
|
|
1177
|
+
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`.
|
|
1178
|
+
|
|
1179
|
+
### Option picker
|
|
1180
|
+
|
|
1181
|
+
```tsx
|
|
1182
|
+
import { OptionPicker } from "knobs/ui/solid"
|
|
1183
|
+
import { KnobsPanel } from "knobs/solid"
|
|
1184
|
+
|
|
1185
|
+
<OptionPicker
|
|
1186
|
+
options={variations().map((v) => ({ id: v.id, label: v.name, suggested: v.id === suggested() }))}
|
|
1187
|
+
selected={variation()}
|
|
1188
|
+
chosen={chosen()}
|
|
1189
|
+
onSelect={(id) => setVariation(id)}
|
|
1190
|
+
onChoose={(id) => choose(id)}
|
|
1191
|
+
digits="page"
|
|
1192
|
+
>
|
|
1193
|
+
{(option) => <KnobsPanel controls={paramsOf(option.id)} header={false} variants={false} />}
|
|
1194
|
+
</OptionPicker>
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
**`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.
|
|
1198
|
+
|
|
1199
|
+
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.
|
|
1200
|
+
|
|
1065
1201
|
### Ideas grid (bento)
|
|
1066
1202
|
|
|
1067
1203
|
```ts
|
|
@@ -1123,6 +1259,7 @@ Override CSS variables on `:root` or `.knobs-panel`:
|
|
|
1123
1259
|
--k-blur: 1; /* motion strength: readout blur and pill stretch */
|
|
1124
1260
|
--k-motion-blur: 0; /* up to 1: moving fill edges and pills smear by their speed */
|
|
1125
1261
|
--k-range-blur: .5; /* up to 1: a range band's gliding ends smear by their speed */
|
|
1262
|
+
--k-curve-blur: .5; /* up to 1: morphing curves, easings and gradients smear by their speed */
|
|
1126
1263
|
--k-step-resist: 0.25; /* a stepped slider's pull off its stop (0 snaps, 1 follows) */
|
|
1127
1264
|
--k-glow: 0.5;
|
|
1128
1265
|
--k-roll: 420; /* readout roll (ms) */
|
|
@@ -1158,6 +1295,8 @@ const die = defineControls("die", schema, {
|
|
|
1158
1295
|
})
|
|
1159
1296
|
```
|
|
1160
1297
|
|
|
1298
|
+
A reset that went back to the active preset rather than the defaults carries `base: true` in the third argument.
|
|
1299
|
+
|
|
1161
1300
|
`patch` holds only the fields that changed. The die page (`examples/die.ts`) rolls its demo dice this way.
|
|
1162
1301
|
|
|
1163
1302
|
## Explore in development. Ship constants.
|
|
@@ -1177,7 +1316,7 @@ The package has two core exports selected through the standard `development` con
|
|
|
1177
1316
|
| --- | --- |
|
|
1178
1317
|
| `knob(value, { min, max, step?, unit?, shortcut?, label?, group?, groupLabel?, section?, sectionLabel? })` | Numeric slider control with rolling readout and inline editor. |
|
|
1179
1318
|
| `choice(value, ["a", "b"], { label?, group?, shortcut? }?)` | Nonempty string choices with literal union inference. |
|
|
1180
|
-
| `toggle(value, { label?, group?, shortcut? }?)` | Boolean switch control. |
|
|
1319
|
+
| `toggle(value, { label?, group?, shortcut?, random? }?)` | Boolean switch control. Out of Random and Mutate unless `random: true`. |
|
|
1181
1320
|
| `spring({ duration?, bounce? }?, meta?)` | SwiftUI-style spring: two sliders beside its live curve. |
|
|
1182
1321
|
| `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
1322
|
| `point({ x, y }, { min, max, step?, ...meta })` | 2D pad; bounds and step take a number or `{ x, y }`. |
|
|
@@ -1196,10 +1335,11 @@ The package has two core exports selected through the standard `development` con
|
|
|
1196
1335
|
| `fromDescriptors(descriptors)` | Converts runtime `ControlDescriptor[]` into a `Schema`. |
|
|
1197
1336
|
| `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
1337
|
| `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. |
|
|
1338
|
+
| `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 }`. |
|
|
1339
|
+
| 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
1340
|
| 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()`. |
|
|
1341
|
+
| `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`, `sound`. The handle adds `panel.fold(group, folded?)`, `panel.tab(id?)`, `panel.search(query?, chips?)` (returns `{ query, chips }`) and `panel.updateAccessories()`. |
|
|
1342
|
+
| `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
1343
|
| `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
1344
|
| `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
1345
|
| `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,20 +1358,22 @@ The package has two core exports selected through the standard `development` con
|
|
|
1218
1358
|
- `controls.setMixed(keys, on?)` / `controls.isMixed(key)`
|
|
1219
1359
|
- `controls.hold(key, on?)` / `controls.isHeld(key)` / `controls.landing`
|
|
1220
1360
|
- `controls.action(key, item?)` — press an action row, or a toolbar's item
|
|
1361
|
+
- `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
1362
|
- `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()`
|
|
1363
|
+
- `controls.reset(keys?)` / `controls.resetKey(key)` / `controls.resetGroup(group)`: back to the active preset, else the defaults
|
|
1364
|
+
- `controls.isModified(key?)` (off the active preset, else the defaults) / `controls.canReset(keys?)` / `controls.modified()` (off the defaults)
|
|
1224
1365
|
- `controls.undo()` / `controls.redo()` / `controls.canUndo` / `controls.canRedo`
|
|
1225
1366
|
- `controls.randomize(keys?)` / `controls.mutate(keys?)`: skip locked fields; one undo step each
|
|
1226
1367
|
- `controls.lock(keys, locked = true)` / `controls.isLocked(key)`: locks are saved with the values (remote controls keep them in memory)
|
|
1227
1368
|
- `controls.compare(hold)` / `controls.comparing`: while held, `get()` returns the previous undo entry; no history, no storage
|
|
1228
|
-
- `controls.preset(id)` / `controls.matchingPreset()`
|
|
1369
|
+
- `controls.preset(id, { coalesce? })` / `controls.matchingPreset()`
|
|
1370
|
+
- `controls.activePreset()` / `controls.activeGroupPreset(group)` / `controls.baseValues()`: the base Reset returns to and Random explores around
|
|
1229
1371
|
- `controls.groupPresets` / `controls.groupPreset(group, index, { coalesce? })` / `controls.matchingGroupPreset(group)` / `controls.groupPresetDisplay`
|
|
1230
1372
|
- `controls.groupLabel(id)` / `controls.sectionLabel(id)` / `controls.presetLabel(id)`: display labels (`groupLabels`, `sectionLabels`, `presetLabels`, `defaultLabel`, or the fields' `groupLabel` / `sectionLabel`)
|
|
1231
1373
|
- `controls.groupDescription(group): string | undefined` / `controls.sectionDescription(section): string | undefined`: `groupDescriptions` / `sectionDescriptions`, else the fields' `groupDescription` / `sectionDescription`
|
|
1232
1374
|
- `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
1375
|
- `controls.code("ts" | "json" | "jsx", values?)`
|
|
1234
|
-
- `controls.variants.list()` / `.save(name?)` / `.load(id)` / `.remove(id)` / `.export()` / `.import(json)`
|
|
1376
|
+
- `controls.variants.list()` / `.save(name?)` / `.load(id, { coalesce? })` / `.rename(id, name)` / `.remove(id)` / `.export()` / `.import(json)`
|
|
1235
1377
|
- `controls.attach(element, chip?)` — floating chip + anchored panel; `chip` (`{ label?, reveal?, shortcut? }`) overrides the store's `chip` option
|
|
1236
1378
|
- `controls.mount(container, options?)` — inline panel
|
|
1237
1379
|
|
|
@@ -1249,6 +1391,8 @@ The package has two core exports selected through the standard `development` con
|
|
|
1249
1391
|
| `toast(message, { tone?, linger?, action?, within? })` | A status pill at the bottom centre. |
|
|
1250
1392
|
| `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
1393
|
| `timeline(container, { duration, time?, fps?, units?, markers?, ranges?, snap?, onSeek?, … })` | A zoomable ruler and playhead with markers, loop ranges and playback. |
|
|
1394
|
+
| `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 }`. |
|
|
1395
|
+
| `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
1396
|
| `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
1397
|
| `planBento(aspects, { width, height, gap?, tolerance?, rowHeight?, groups?, hero? })` | The bento's layout on its own: rects, mode, coverage and crop. |
|
|
1254
1398
|
| `overlayLook({ preset?, theme? })` | The look every overlay takes. |
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
__preload
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-gezhc24g.js";
|
|
4
4
|
import {
|
|
5
5
|
iconSvg,
|
|
6
6
|
readSession,
|
|
7
7
|
writeSession,
|
|
8
8
|
injectStyles
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-693jd1gj.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 (__preload("
|
|
85
|
+
let { createPanel } = await (__preload("5cr85vv6"), import("./chunk-5cr85vv6.js"));
|
|
86
86
|
if (disposed || !controls.enabled || panel)
|
|
87
87
|
return;
|
|
88
88
|
if (panel = createPanel(controls, element, close, { restored, chip }), remember)
|