@kitlangton/knobs 0.2.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.
- package/README.md +135 -17
- package/dist/dev/{chunk-gezhc24g.js → chunk-7xgkerd2.js} +14 -5
- package/dist/dev/{chunk-5cr85vv6.js → chunk-jx1ktap3.js} +7843 -7933
- package/dist/dev/{chunk-693jd1gj.js → chunk-pmkvb9ep.js} +1544 -160
- package/dist/dev/{chunk-4km87zfx.js → chunk-v8ssbcbk.js} +18 -7
- package/dist/dev/index.js +187 -31
- package/dist/index.js +197 -33
- package/dist/panel.js +20260 -18462
- package/dist/types/core/keys.d.ts +2 -0
- package/dist/types/core/remote.d.ts +2 -0
- package/dist/types/core/schema.d.ts +5 -0
- package/dist/types/core/store.d.ts +29 -2
- package/dist/types/core/unlock.d.ts +77 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/panel/attach.d.ts +2 -0
- package/dist/types/panel/crumbs.d.ts +19 -0
- package/dist/types/panel/editors.d.ts +9 -0
- package/dist/types/panel/feel.d.ts +103 -0
- package/dist/types/panel/give.d.ts +34 -1
- package/dist/types/panel/gpu/color.d.ts +4 -4
- package/dist/types/panel/gpu/renderer.d.ts +32 -0
- package/dist/types/panel/gpu/shader.wgsl.d.ts +1 -1
- package/dist/types/panel/index.d.ts +2 -1
- package/dist/types/panel/inspector.d.ts +1 -1
- package/dist/types/panel/lens.d.ts +113 -0
- package/dist/types/panel/motion.d.ts +2 -0
- package/dist/types/panel/page-hints.d.ts +2 -1
- package/dist/types/panel/prefs.d.ts +2 -2
- package/dist/types/panel/presence.d.ts +20 -0
- package/dist/types/panel/scope.d.ts +29 -0
- package/dist/types/panel/slider.d.ts +21 -4
- package/dist/types/panel/tab-transition.d.ts +15 -0
- package/dist/types/panel/tabs.d.ts +1 -1
- package/dist/types/panel/toggle.d.ts +27 -0
- package/dist/types/ui/look.d.ts +1 -1
- package/dist/types/ui/scrollbar.d.ts +12 -0
- package/dist/types/ui/split-model.d.ts +4 -3
- package/dist/types/ui/split.d.ts +8 -6
- package/dist/ui-react.js +232 -132
- package/dist/ui-solid.js +232 -132
- package/dist/ui.js +285 -174
- 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 **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 (
|
|
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
|
|
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
|
|
|
@@ -338,9 +338,9 @@ Panels and switchers are keyboard-first:
|
|
|
338
338
|
| Key | Action |
|
|
339
339
|
| --- | --- |
|
|
340
340
|
| `↑` / `↓` or `j` / `k` | Move between presets, rows and custom actions (`←`/`→` move along the custom actions); a spring's two sliders are two stops |
|
|
341
|
-
| `←` / `→`
|
|
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 |
|
|
342
342
|
| `Enter` / `Space` | Open a colour, point, easing, gradient or curve row's editor; `↓` then walks into it (strips, chips, handles, pad) |
|
|
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 |
|
|
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 |
|
|
344
344
|
| Arrows on a pad or curve handle | Move the puck or handle (`⇧` ×10, `⌥` finest); `j` / `k` or `Tab` move on |
|
|
345
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) |
|
|
346
346
|
| `[` / `]` | Cycle presets and saved variants from anywhere in the panel; with no preset strip, cycle tabs |
|
|
@@ -350,16 +350,17 @@ Panels and switchers are keyboard-first:
|
|
|
350
350
|
| `Home` / `End` | Jump slider to `min` or `max` |
|
|
351
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 |
|
|
352
352
|
| `Backspace` / `Delete` | Reset focused row (or the selection) to its default (or double-click row/label) |
|
|
353
|
-
|
|
|
354
|
-
|
|
|
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 |
|
|
355
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)) |
|
|
356
357
|
| `Tab` / `⇧Tab` | Next/previous control, wrapping inside floating panels |
|
|
357
358
|
| `f` | Focus first row |
|
|
358
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 |
|
|
359
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) |
|
|
360
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) |
|
|
361
|
-
|
|
|
362
|
-
| `⇧`-click / `⇧↑` `⇧↓` | Select a range of rows (`⌘`-click adds or removes one); `r`, `⌥R`, `⌫`,
|
|
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 |
|
|
363
364
|
| Right-click / `⇧F10` / `ContextMenu` | Menu for the row or selection: Lock from Random, Randomize, Reset, Copy; `↑↓`, a letter, `Enter`, `Esc` |
|
|
364
365
|
| Hold `\` | Compare: show the values from before the last change while held (the title dims); nothing is recorded |
|
|
365
366
|
| `⇧C` | Copy the current values (or the selected fields) as code |
|
|
@@ -367,8 +368,49 @@ Panels and switchers are keyboard-first:
|
|
|
367
368
|
| `F2` / `Enter` on the strip, double-click a chip | Rename a saved preset |
|
|
368
369
|
| `u` / `z` | Undo (`⇧u` / `⇧z` redo); slider drags coalesce into a single history entry |
|
|
369
370
|
| `?` | Toggle keyboard shortcuts popover (the title's tooltip mentions it) |
|
|
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)) |
|
|
370
372
|
| `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 (
|
|
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 |
|
|
372
414
|
|
|
373
415
|
### Hint mode across the page
|
|
374
416
|
|
|
@@ -443,7 +485,7 @@ defineControls("tune", schema, {
|
|
|
443
485
|
knob(44, { min: 28, max: 72, group: "drawer.spacing", groupLabel: "Spacing", section: "drawer", sectionLabel: "Session List" })
|
|
444
486
|
```
|
|
445
487
|
|
|
446
|
-
`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.
|
|
447
489
|
|
|
448
490
|
### Remote / Socket-Driven Controls (`createRemoteControls`)
|
|
449
491
|
|
|
@@ -561,12 +603,12 @@ Without Solid: `remote.update(descriptors, values, { land })` and `mountPanel(re
|
|
|
561
603
|
are claimed (`preventDefault`) and stopped there, and everything else reaches the page. Esc hands
|
|
562
604
|
focus back. `keys` remaps (`{ hint: "f" }`) or turns off (`{ random: false }`) any command; a
|
|
563
605
|
command that's off loses its header button too, and the `?` popover and row menu list only what's
|
|
564
|
-
on (see **Header** for picking buttons). `vim: false` drops j/k
|
|
606
|
+
on (see **Header** for picking buttons). `vim: false` drops j/k, `edit: false` leaves digits
|
|
565
607
|
and Enter to the page, `bigStep` sets ⇧'s multiplier and `wrap: false` stops ↑/↓ at the ends.
|
|
566
608
|
- **Marks.** `marks: false` (on the store or the mount) hides modified dots, readout marks, group
|
|
567
609
|
resets and Reset, for values that belong to someone else. A command that's off takes its marks
|
|
568
610
|
too: with `reset: false` rows show nothing to reset, and group resets follow `resetAll`.
|
|
569
|
-
- **Haptics.** On phones, a stepped slider crossing a stop,
|
|
611
|
+
- **Haptics.** On phones, a stepped slider crossing a stop, a detent catching a drag, a
|
|
570
612
|
choice, preset or seed changing mid-pan, a pan meeting a limit and a switch flipping each give a
|
|
571
613
|
light tick: `navigator.vibrate` on
|
|
572
614
|
Android; on iPhone, Safari's switch tick (iOS 17.4–26.4 for every tick; from iOS 26.5, which only
|
|
@@ -751,7 +793,8 @@ defineControls("settings", schema, { ship: () => import("knobs/panel") })
|
|
|
751
793
|
|
|
752
794
|
A shipped store stays live in production (`enabled` defaults to true) and mounts from the module you
|
|
753
795
|
name, which your bundler splits into its own chunk. The core never imports the panel itself.
|
|
754
|
-
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).
|
|
755
798
|
|
|
756
799
|
In production its chips stay hidden until someone presses **⌥⇧D**, which turns knobs mode on and off
|
|
757
800
|
for the tab (remembered in `sessionStorage`, with a brief "knobs on" / "knobs off"); then they show on
|
|
@@ -810,6 +853,7 @@ A panel inside an app should look like the app's own controls. Four things get i
|
|
|
810
853
|
| `--k-radius`, `--k-panel-radius` | `8px` by density, radius + `4px` | Corners. `0` squares everything, the pill shapes (switches, handles, dots) included |
|
|
811
854
|
| `--k-thumb` | `#fff` | Switch thumbs and strip handles |
|
|
812
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) |
|
|
813
857
|
| `--k-frame` | `card` | `flat`: no card inside the host's own card (see below) |
|
|
814
858
|
| `--k-disabled-opacity` | `.32` | How far a disabled row dims |
|
|
815
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 |
|
|
@@ -999,7 +1043,7 @@ On a page, the keyboard spans every card:
|
|
|
999
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 |
|
|
1000
1044
|
| `s` | Hints for what's on screen, custom cards included |
|
|
1001
1045
|
|
|
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,
|
|
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.
|
|
1003
1047
|
|
|
1004
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.
|
|
1005
1049
|
|
|
@@ -1299,6 +1343,78 @@ A reset that went back to the active preset rather than the defaults carries `ba
|
|
|
1299
1343
|
|
|
1300
1344
|
`patch` holds only the fields that changed. The die page (`examples/die.ts`) rolls its demo dice this way.
|
|
1301
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
|
+
|
|
1302
1418
|
## Explore in development. Ship constants.
|
|
1303
1419
|
|
|
1304
1420
|
```ts
|
|
@@ -1306,7 +1422,7 @@ splat.locked // frozen schema defaults
|
|
|
1306
1422
|
splat.get() // live while enabled; the exact locked object otherwise
|
|
1307
1423
|
```
|
|
1308
1424
|
|
|
1309
|
-
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.
|
|
1310
1426
|
|
|
1311
1427
|
## API Summary
|
|
1312
1428
|
|
|
@@ -1314,7 +1430,7 @@ The package has two core exports selected through the standard `development` con
|
|
|
1314
1430
|
|
|
1315
1431
|
| Function | Behavior |
|
|
1316
1432
|
| --- | --- |
|
|
1317
|
-
| `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. |
|
|
1318
1434
|
| `choice(value, ["a", "b"], { label?, group?, shortcut? }?)` | Nonempty string choices with literal union inference. |
|
|
1319
1435
|
| `toggle(value, { label?, group?, shortcut?, random? }?)` | Boolean switch control. Out of Random and Mutate unless `random: true`. |
|
|
1320
1436
|
| `spring({ duration?, bounce? }?, meta?)` | SwiftUI-style spring: two sliders beside its live curve. |
|
|
@@ -1335,10 +1451,11 @@ The package has two core exports selected through the standard `development` con
|
|
|
1335
1451
|
| `fromDescriptors(descriptors)` | Converts runtime `ControlDescriptor[]` into a `Schema`. |
|
|
1336
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. |
|
|
1337
1453
|
| `fromSettings(groups, home?)` | A hosted prototype's settings sections as descriptors and values. |
|
|
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). |
|
|
1338
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 }`. |
|
|
1339
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. |
|
|
1340
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. |
|
|
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()`. |
|
|
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()`. |
|
|
1342
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. |
|
|
1343
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. |
|
|
1344
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[] }`. |
|
|
@@ -1376,6 +1493,7 @@ The package has two core exports selected through the standard `development` con
|
|
|
1376
1493
|
- `controls.variants.list()` / `.save(name?)` / `.load(id, { coalesce? })` / `.rename(id, name)` / `.remove(id)` / `.export()` / `.import(json)`
|
|
1377
1494
|
- `controls.attach(element, chip?)` — floating chip + anchored panel; `chip` (`{ label?, reveal?, shortcut? }`) overrides the store's `chip` option
|
|
1378
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
|
|
1379
1497
|
|
|
1380
1498
|
### Workspace pieces (`knobs/ui`)
|
|
1381
1499
|
|
|
@@ -248,6 +248,17 @@ function comboParts2(combo, apple = isApple()) {
|
|
|
248
248
|
function comboLabel2(combo, apple = isApple()) {
|
|
249
249
|
return comboParts2(combo, apple).join(apple ? "" : "+");
|
|
250
250
|
}
|
|
251
|
+
function isTypingTarget(target) {
|
|
252
|
+
let el = target;
|
|
253
|
+
if (!el || typeof el.tagName !== "string")
|
|
254
|
+
return !1;
|
|
255
|
+
let tag = el.tagName;
|
|
256
|
+
if (tag === "SELECT" || el.isContentEditable)
|
|
257
|
+
return !0;
|
|
258
|
+
if (tag === "TEXTAREA")
|
|
259
|
+
return !el.readOnly;
|
|
260
|
+
return tag === "INPUT" && !el.readOnly && !["range", "checkbox", "radio", "button", "submit", "reset", "color"].includes(el.type);
|
|
261
|
+
}
|
|
251
262
|
|
|
252
263
|
// src/core/ramp.ts
|
|
253
264
|
var MAX_STOPS = 32, MAX_POINTS = 64, GRADIENT_SAMPLES = 64, CURVE_SAMPLES = 129, clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v)), finite = (v) => typeof v === "number" && Number.isFinite(v), round4 = (v) => Math.round(v * 1e4) / 1e4 || 0;
|
|
@@ -531,11 +542,9 @@ function withMeta(base, meta) {
|
|
|
531
542
|
return checkShortcut(meta?.shortcut), Object.freeze({ ...base, ...pickMeta(meta) });
|
|
532
543
|
}
|
|
533
544
|
function knob2(value, options) {
|
|
534
|
-
|
|
535
|
-
if (![value, options.min, options.max, step].every(Number.isFinite) || options.max < options.min || step <= 0)
|
|
545
|
+
if (!Number.isFinite(value))
|
|
536
546
|
throw Error("A knob needs finite bounds and a positive step.");
|
|
537
|
-
|
|
538
|
-
throw Error("A knob's decimals must be a whole number.");
|
|
547
|
+
let step = bounds(options, "A knob");
|
|
539
548
|
checkShortcut(options.shortcut);
|
|
540
549
|
let field = { ...options, kind: "knob", value, step };
|
|
541
550
|
return Object.freeze({ ...field, value: normalizeField(field, value) });
|
|
@@ -1129,4 +1138,4 @@ function urlFlag2(name) {
|
|
|
1129
1138
|
}
|
|
1130
1139
|
var storageKey = (id, part) => `knobs:${encodeURIComponent(id)}:${part}:v1`;
|
|
1131
1140
|
|
|
1132
|
-
export { __preload, __chunks, maxChroma, oklchToRgb, parseColor2, fitColor, formatColor, colorHex2, EASINGS2, EASING_Y, easingName, bezierPoint2, bezierCss2, normalizeCombo2, letterOf2, comboFromEvent2, heldMods, matchesCombo2, comboParts2, comboLabel2, GRADIENT_SAMPLES, sampleGradient2, gradientSamples, gradientCss2, stopColorAt, sampleCurve2, curveSamples, CURVE_SHAPES2, inRange, sameRamp, randomStops, mutateStops, randomPoints, mutatePoints, pickMeta, isReadout2, hasValue2, SPRING2, valueKeys, knob2, choice2, action2, flatSpring2, toggle2, spring2, color2, point2, easing2, vec22, vec32, range2, angle2, seed2, keybind2, gradient2, curve2, toolbarShortcut2, actionShortcut2, toolbar2, watch2, sparkline2, graph2, meter2, table2, log2, wrapAngle2, normalizeField, normalize, isRecord, same, equal, Spring, springMotion2, springResponse, springEasing, springCss2, RESHAPE, RESET_KEYS, REFRESHES, readoutState, pushReadout, READOUT, memoryStorage2, browserStorage, urlFlag2, storageKey };
|
|
1141
|
+
export { __preload, __chunks, maxChroma, oklchToRgb, parseColor2, fitColor, formatColor, colorHex2, EASINGS2, EASING_Y, easingName, bezierPoint2, bezierCss2, normalizeCombo2, letterOf2, comboFromEvent2, heldMods, matchesCombo2, comboParts2, comboLabel2, isTypingTarget, GRADIENT_SAMPLES, sampleGradient2, gradientSamples, gradientCss2, stopColorAt, sampleCurve2, curveSamples, CURVE_SHAPES2, inRange, sameRamp, randomStops, mutateStops, randomPoints, mutatePoints, pickMeta, isReadout2, hasValue2, SPRING2, valueKeys, knob2, choice2, action2, flatSpring2, toggle2, spring2, color2, point2, easing2, vec22, vec32, range2, angle2, seed2, keybind2, gradient2, curve2, toolbarShortcut2, actionShortcut2, toolbar2, watch2, sparkline2, graph2, meter2, table2, log2, wrapAngle2, normalizeField, normalize, isRecord, same, equal, Spring, springMotion2, springResponse, springEasing, springCss2, RESHAPE, RESET_KEYS, REFRESHES, readoutState, pushReadout, READOUT, memoryStorage2, browserStorage, urlFlag2, storageKey };
|