@kitlangton/knobs 0.4.0-next.15 → 0.4.0-next.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -14
- package/dist/dev/{chunk-v2g91mrm.js → chunk-2g0znytq.js} +1 -0
- package/dist/dev/{chunk-bejz4qcg.js → chunk-3vag4n3q.js} +47 -10
- package/dist/dev/{chunk-49hkdggk.js → chunk-5cv3g7ca.js} +395 -104
- package/dist/dev/{chunk-902v1jgd.js → chunk-xzm0zawh.js} +6 -6
- package/dist/dev/index.js +9 -8
- package/dist/index.js +7 -5
- package/dist/panel.js +549 -121
- package/dist/review-react.js +1341 -880
- package/dist/review-solid.js +1341 -880
- package/dist/review.js +1711 -1248
- package/dist/themes.js +8 -0
- package/dist/types/core/schema.d.ts +11 -2
- package/dist/types/core/store.d.ts +29 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/panel/active.d.ts +53 -0
- package/dist/types/panel/description-ids.d.ts +6 -0
- package/dist/types/panel/folds.d.ts +7 -4
- package/dist/types/panel/gpu/color.d.ts +2 -0
- package/dist/types/panel/gpu/renderer.d.ts +8 -2
- package/dist/types/panel/index.d.ts +1 -0
- package/dist/types/panel/page-hints.d.ts +3 -2
- package/dist/types/panel/page.d.ts +2 -15
- package/dist/types/panel/position.d.ts +23 -0
- package/dist/types/panel/summary.d.ts +22 -0
- package/dist/types/tokens/schema.d.ts +9 -0
- package/dist/types/ui/compare-css.d.ts +1 -1
- package/dist/types/ui/compare-frames.d.ts +64 -2
- package/dist/types/ui/compare-model.d.ts +23 -0
- package/dist/types/ui/compare.d.ts +6 -4
- package/dist/types/ui/player-css.d.ts +1 -1
- package/dist/types/ui/player.d.ts +6 -0
- package/dist/types/ui/react.d.ts +1 -1
- package/dist/types/ui/solid.d.ts +1 -1
- package/dist/types/ui/tooltip-model.d.ts +35 -0
- package/dist/types/ui/tooltip.d.ts +1 -1
- package/dist/types/ui/walkthrough-model.d.ts +4 -2
- package/dist/ui-react.js +2015 -1507
- package/dist/ui-solid.js +2014 -1506
- package/dist/ui.js +2013 -1505
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -458,7 +458,7 @@ Tune them with tokens on the panel or any ancestor:
|
|
|
458
458
|
|
|
459
459
|
With more than one thing on the page to label (two panels, a panel beside a `knobs/ui` tree,
|
|
460
460
|
timeline or split, or the host's own `data-hint` buttons), `s` reaches all of them: pressed in a
|
|
461
|
-
panel or
|
|
461
|
+
panel or a piece, every visible control in every panel (rows, tabs, toolbar
|
|
462
462
|
buttons, header buttons), each tree row on screen, a timeline's Play, markers and playhead, the
|
|
463
463
|
splits' header actions, collapse buttons and rails, and the host's elements gets a code. Typing one
|
|
464
464
|
focuses its control (a row) or presses it (a button, a marker, a tree row); `Backspace` takes a
|
|
@@ -470,7 +470,9 @@ then the pieces, then the host's; down each column, left to right; home row firs
|
|
|
470
470
|
a second letter, home row first, so `a` then `s` is the first panel's second control. A group of
|
|
471
471
|
one is just its letter; spare letters split a big group so its second letters stay on the home row.
|
|
472
472
|
With only one panel on the page, nothing changes: `s` works while focus is in it, with codes from
|
|
473
|
-
its labels.
|
|
473
|
+
its labels. On the page itself (focus on its body, a game's canvas, your content in a split's
|
|
474
|
+
pane) `s` stays the page's, so a game's WASD keeps its S. It starts hint mode there only when the
|
|
475
|
+
host asks: it registers targets of its own (`hints.register`) or marks elements `data-hint`.
|
|
474
476
|
|
|
475
477
|
```ts
|
|
476
478
|
import { hints } from "knobs/panel" // or "knobs/ui"
|
|
@@ -641,9 +643,13 @@ Without Solid: `remote.update(descriptors, values, { land })` and `mountPanel(re
|
|
|
641
643
|
the row. They switch both ways as the width changes; `display: "menu" | "segmented"` fixes one.
|
|
642
644
|
On a menu row, ←/→ step through the options, and Enter or Space opens the menu (↑↓, a letter,
|
|
643
645
|
Enter, Esc).
|
|
644
|
-
- **Keys stay scoped.**
|
|
645
|
-
are claimed (`preventDefault`) and stopped there, and everything else reaches the page.
|
|
646
|
-
|
|
646
|
+
- **Keys stay scoped.** A panel, inline or floating, hears keys only while focus is inside it. Keys
|
|
647
|
+
it handles are claimed (`preventDefault`) and stopped there, and everything else reaches the page.
|
|
648
|
+
A press on the page outside it hands focus back to the page and leaves the panel open, even where
|
|
649
|
+
the page cancels its presses so focus can't move (a game's canvas): play with the panel open, and
|
|
650
|
+
click back into it to tune. A floating panel opens with focus at its own level, so one Esc closes
|
|
651
|
+
it and ↓ goes in to the rows. Esc hands focus back (a floating panel's Esc at its own level closes
|
|
652
|
+
it), and the page's toggle key (`` ` ``) shows and hides it from anywhere. `keys` remaps (`{ hint: "f" }`) or turns off (`{ random: false }`) any command; a
|
|
647
653
|
command that's off loses its header button too, and the `?` popover and row menu list only what's
|
|
648
654
|
on (see **Header** for picking buttons). `vim: false` drops j/k, `edit: false` leaves digits
|
|
649
655
|
and Enter to the page, `bigStep` sets ⇧'s multiplier and `wrap: false` stops ↑/↓ at the ends.
|
|
@@ -748,7 +754,10 @@ mountPanel(station, column, {
|
|
|
748
754
|
`"help"` to keep it (`buttons: ["random", "reset", "copy", "undo", "redo", "help", "close"]`); a
|
|
749
755
|
`buttons` list that already names `"help"`, as the Lab's does, behaves as before.
|
|
750
756
|
The buttons sit in fixed slots, left to right Undo · Redo, Reset, Copy, Random (then `?` and
|
|
751
|
-
the close X): a list picks which show, not their order. A
|
|
757
|
+
the close X): a list picks which show, not their order. A die (the header's, or a group's) shows
|
|
758
|
+
only where Random could change something; a store or group of switches, actions and
|
|
759
|
+
`random: false` fields has none, and locks only disable it. Copy falls back to the browser's copy
|
|
760
|
+
command where the clipboard API isn't there (a page served over plain http). A button left out takes no slot; Undo,
|
|
752
761
|
Redo and Reset keep theirs while they're away (`data-shown="false"`, `disabled`, out of the
|
|
753
762
|
tab order and the accessibility tree).
|
|
754
763
|
- **On a surface.** An inline panel in a split's pane (or inside anything with the class
|
|
@@ -775,9 +784,27 @@ mountPanel(station, column, {
|
|
|
775
784
|
always did. Where scroll-driven animations aren't supported, the header doesn't pin.
|
|
776
785
|
- **Folding groups.** A field's `folded: true` (or `groupFolded` on the mount) makes its group
|
|
777
786
|
foldable, starting folded. Its header folds and opens it with a click, Enter or Space; it's a ↑↓
|
|
778
|
-
stop of its own, ← folds and → opens. `panel.fold(group, folded?)` does it from code.
|
|
787
|
+
stop of its own, ← folds and → opens. `panel.fold(group, folded?)` does it from code. Group
|
|
788
|
+
and sub-heading folds the user chose come back on the next visit, in the store's storage (with
|
|
789
|
+
`session: false` or `persist: "none"`, they don't), over `folded` and `groupFolded`. On a
|
|
779
790
|
[full page](#full-page) the group's tile folds to its header, the cards below follow, and the
|
|
780
|
-
filter opens it while it runs.
|
|
791
|
+
filter opens it while it runs. Folded, the header sums the card up on its right, quietly and
|
|
792
|
+
live: a one-row card's value ("CSS orb"), otherwise its matching group preset (or "Custom"),
|
|
793
|
+
else its first changed value, else its first choice, then "· 2 changed". The title keeps its
|
|
794
|
+
room; the summary truncates first. `groupSummary(group, info)` on the mount replaces it, as on a
|
|
795
|
+
page (`info` is `{ preset, changed, keys }`; `null` shows nothing).
|
|
796
|
+
- **Card descriptions.** A group's `groupDescription` (on any of its fields; the first wins) or the
|
|
797
|
+
store's `groupDescriptions` puts one muted line under its card's title, wrapping under the whole
|
|
798
|
+
header. Search matches it.
|
|
799
|
+
- **Row descriptions.** A field's `description` (`knob(260, { description: "How fast the square
|
|
800
|
+
moves" })`, or on a descriptor) is a tooltip over its row: it shows after the tooltips' usual
|
|
801
|
+
pause over the row, or at once on keyboard focus, and goes with a press. Screen readers hear it
|
|
802
|
+
as the control's description (`aria-describedby`), and search matches it.
|
|
803
|
+
- **What's active.** `onActive: (at) => …` hears which card or row the pointer or keyboard focus is
|
|
804
|
+
on: `{ group }` for a card's header or body, `{ group, key }` for a row, `null` for nothing
|
|
805
|
+
(pinned cards and group preset strips count). Whichever moved last wins; it's called at most once
|
|
806
|
+
a frame, never twice with the same value, and once more with `null` when the panel goes. Use it
|
|
807
|
+
to outline the part of your app a card changes. `mountPage` takes it too.
|
|
781
808
|
- **Group Random by the host.** `groupRandom: (group, mode) => …` runs first for a group's die, `g`
|
|
782
809
|
and `⌥G` (`mode` is `"random"` or `"mutate"`). Return `true` when the host rolled the group (its
|
|
783
810
|
own seeded "Randomize <group>"): the die tumbles and knobs leaves the values alone. Return
|
|
@@ -967,6 +994,7 @@ A panel inside an app should look like the app's own controls. Four things get i
|
|
|
967
994
|
| `--k-font`, `--k-mono`, `--k-font-size` | system, mono, `12px` by density | Faces and size; `--k-value-font: var(--k-mono)` puts readouts in mono |
|
|
968
995
|
| `--k-font-weight`, `--k-label-weight`, `--k-title-weight` | `400`, `500`, `600` | Body, row labels, and titles, group headings and the chosen chip |
|
|
969
996
|
| `--k-radius`, `--k-panel-radius` | `8px` by density, radius + `4px` | Corners. `0` squares everything, the pill shapes (switches, handles, dots) included |
|
|
997
|
+
| `--k-radius-frame` | radius + padding + border | A panel's own outer corners, concentric with the cards, preset strip and lens inside it |
|
|
970
998
|
| `--k-thumb` | `#fff` | Switch thumbs and strip handles |
|
|
971
999
|
| `--k-lift` | `1` | `0` drops the small shadows under pills, thumbs and handles |
|
|
972
1000
|
| `--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) |
|
|
@@ -1099,7 +1127,7 @@ A page with many sections reads better folded. `sectionFolded` starts sections f
|
|
|
1099
1127
|
|
|
1100
1128
|
Folded things say what's in them. A folded group card trades its presets, reset and die for "Compact · 7 changed" (the matching group preset, or "Custom", then its changed rows; each part only when it applies). A folded section's heading reads "2 changed · 3 cards". `groupSummary(group, info)` and `sectionSummary(section, info)` on the mount replace either (`info` is a `FoldSummaryInfo`: `{ preset, changed, keys }`; return `null` for nothing). Group cards fold as in a panel, by a field's or descriptor's `folded`, or `groupFolded` on the mount, `defineControls` or `createRemoteControls` (both take `sectionFolded` too).
|
|
1101
1129
|
|
|
1102
|
-
Descriptions add one muted line under a title: `groupDescriptions` and `sectionDescriptions` by id (on the store, remote options or the mount), or `groupDescription` / `sectionDescription` on any field or descriptor (the first that gives one wins). `page.card({ description })` does the same for a custom card. The filter matches descriptions too.
|
|
1130
|
+
Descriptions add one muted line under a title: `groupDescriptions` and `sectionDescriptions` by id (on the store, remote options or the mount), or `groupDescription` / `sectionDescription` on any field or descriptor (the first that gives one wins). `page.card({ description })` does the same for a custom card. The filter matches descriptions too. Group descriptions show in panels as well, wrapping under the card's header.
|
|
1103
1131
|
|
|
1104
1132
|
What the user folds and opens (sections and group cards) is remembered under `knobs:<id>:folds:v1` (`storageKey(id, "folds")`) as `{ sections: Record<string, boolean>, groups: Record<string, boolean> }`, and wins over `sectionFolded` and `groupFolded`. Only the user's folds and those made with `remember` are written; folds for context (`remember: false`, a highlight's `expand`) and the filter's never are. By default they go to the store's own storage, or nowhere with `persist: "none"`. `foldStorage` on the mount always wins and keeps folds only, never values, locks or variants, so a remote store on `storage: null` (the phone's values always win) can still remember them; `null` keeps none.
|
|
1105
1133
|
|
|
@@ -1329,7 +1357,7 @@ compare(viewer, { before, after, timeline: clock })
|
|
|
1329
1357
|
compareStrip(strip, { before, after, timeline: clock })
|
|
1330
1358
|
```
|
|
1331
1359
|
|
|
1332
|
-
**`compare(container, options)`** is a before/after viewer for videos and images (by extension, or `kind`). It offers two modes, side by side (the default `mode`: a split screen down the middle, each side filling its half) and a wipe; `modes: ["side", "wipe", "difference"]` adds a difference (changes above `threshold` in one accent, `--k-compare-diff`), and one mode hides the bar. The modes sit in a segmented control above the stage, the selection pill sliding to the one picked. It draws in one WebGPU pass that copies a frame in only when one arrives, and draws only when something changed; where there's no WebGPU, two `<video>`s with a clip-path wipe stand in. Pinch, `⌘`/ctrl-wheel or double-click zooms into either side and both zoom to the same part (the wheel and a drag pan it, both 1:1); a small pill on a dark scrim in the corner shows how far, with a reset (`0`, a double-click, `Escape`), which glides back. Side ↔ Wipe is Kit Motion's Comparison fuse: the two sides glide together on one spring (.3 s) and fuse at one side's size in the middle, so the stage never changes size; Before is cut only where they overlap, with a feather that swells mid-transition and a blurred strip over the seam, the wipe's handle fades in with it, and the labels fold to their names and ride either side of the seam (Wipe → Side runs it back, and either way it can be turned around midway). Overlaid, the sides are letterboxed, never cropped. Both videos run from one clock: paused, scrubbed or stepped, each seeks to the frame's midpoint, coalesced while a seek is in flight; playing, After leads, Before's rate leans by up to 25% to stay on it (it seeks past 90 ms), and a bound timeline follows After's time, so only one clock ever moves it (`Space` resumes from the frame showing). Scrubbing
|
|
1360
|
+
**`compare(container, options)`** is a before/after viewer for videos and images (by extension, or `kind`). It offers two modes, side by side (the default `mode`: a split screen down the middle, each side filling its half) and a wipe; `modes: ["side", "wipe", "difference"]` adds a difference (changes above `threshold` in one accent, `--k-compare-diff`), and one mode hides the bar. The modes sit in a segmented control above the stage, the selection pill sliding to the one picked. It draws in one WebGPU pass that copies a frame in only when one arrives, and draws only when something changed; where there's no WebGPU, two `<video>`s with a clip-path wipe stand in. Pinch, `⌘`/ctrl-wheel or double-click zooms into either side and both zoom to the same part (the wheel and a drag pan it, both 1:1); a small pill on a dark scrim in the corner shows how far, with a reset (`0`, a double-click, `Escape`), which glides back. Side ↔ Wipe is Kit Motion's Comparison fuse: the two sides glide together on one spring (.3 s) and fuse at one side's size in the middle, so the stage never changes size; Before is cut only where they overlap, with a feather that swells mid-transition and a blurred strip over the seam, the wipe's handle fades in with it, and the labels fold to their names and ride either side of the seam (Wipe → Side runs it back, and either way it can be turned around midway). Overlaid, the sides are letterboxed, never cropped. Both videos run from one clock: paused, scrubbed or stepped, each seeks to the frame's midpoint, coalesced while a seek is in flight; playing, After leads, Before's rate leans by up to 25% to stay on it (it seeks past 90 ms), and a bound timeline follows After's time, so only one clock ever moves it (`Space` resumes from the frame showing). Scrubbing shows the frame under the hand even on raw screen recordings with a keyframe every 8–18 s: frames decoded in the background (WebCodecs where the file is a plain MP4, else a hidden `<video>`) sit in a cache shared by every viewer of the same source: a grid of small frames first, then every frame as a small JPEG proxy (960 px, at most 48 MB a source, every 2nd or 4th frame past that), and mid-drag the proxy of the frame under the pointer decodes in a few ms, with the one the hand is heading for; the videos wait and seek exactly when it lets go. `player()` does the same over its video once the pointer is over it. Encoding with a keyframe every 15 frames makes every seek exact and near instant: `COMPARE_FFMPEG` holds the command (`ffmpeg -i in.mov -vf fps=30 -fps_mode cfr -c:v libx264 -crf 18 -g 15 … out.mp4`). Sources load only near the view and are let go 3 s after leaving it (`lazy: false` keeps them), every viewer shares one GPU device, and in a container with no height of its own it takes the height its sides need, half its width in their shape, so the split has no margins (`aspect` before they load); with a height of its own (a card going full-window, say), the pair fits as a whole and refits as it resizes, keeping the zoom. On a card (`.knobs-surface`, a split's pane) or marked `data-nested`, its stage drops its fill, so one surface shows. Bind an existing `timeline()` (`timeline: clock`), or pass `timeline: true` for one of its own: a player's thin progress line under the stage (`variant: "line"`, the same as `player()`'s), its markers (a walkthrough's chapters) segments with a small gap between; there's no clock or frame readout anywhere, only the chapter and time under the pointer over the line. Its row starts with a minimal play/pause button; a click on the pictures (not a wipe, pan or pinch) and `Space` play and pause too, with no glyph flashing over them. Two stills get no button and no line. `seek(t, { glide: true })` is a chapter's jump: the videos land at once and the playhead catches up. Loop (`L`, the loop icon) sets a loop range around the playhead on the timeline, whose ends drag, and playback stays inside it. The wipe's line carries a 16×28 grip on the same scrim in its middle that follows the pointer's height only while dragged and settles back when let go; dragging is 1:1, the cursor hides and the line and the grip's marks brighten, and a press elsewhere on the stage brings the line there on a quick catch-up (.15 s). Before and After ride either side of the seam while wiping, each fading as its side runs out of room. The wipe is a slider too (`←`/`→`, `⇧` 10%); a mode change glides. Keys when focused: `Space` plays, `←`/`→` seek half a second (`⇧` two), the playhead gliding there while the videos land at once, `,`/`.` step a frame, `1`–`3` pick the offered modes in order and `S` `W` (`D`, when offered) by name, `Z` held shows the loupe (or `loupe: true`), `L` loops, `=`/`-`/`0` zoom. It reports `onMode`, `onWipe` and `onView({ zoom, loop })`; the handle has `mode`, `modes`, `wipe`, `zoom`, `loop`, `time`, `playing`, `duration`, `renderer`, `timeline`, `update`, `seek`, `seekBy`, `play`, `pause`, `toggle`, `step`, `setZoom`, `setLoop`, `focus` and `dispose`. **`compareStrip(container, { before, after, timeline })`** is a frame strip on its own: it draws Before over After along the timeline's view from the same frame cache, decoded only while it's on screen, with a bar for how much each column differs; a press scrubs. `Compare` and `CompareStrip` are in `knobs/ui/react` and `knobs/ui/solid`.
|
|
1333
1361
|
|
|
1334
1362
|
**`player(container, { src, chapters? })`** is a plain video in the same look: the picture in the compare's frame, then a row under it: a minimal play/pause button (the filled pair, a 28px target, a quiet fill on hover) and one thin progress line (2px, the tertiary text colour filled in text colour, a segment per chapter `{ label, start }`, thicker under the pointer). The button, a click on the picture, or `Space` once it's focused (a click focuses it) plays and pauses, with nothing flashing over the picture; click or drag the line to seek, 1:1, and a click away from where it is glides there while the video lands at once. The chapter and time under the pointer float over the line; `←`/`→` seek 5 s (`⇧` 10 s), and a pinch, `⌘`/ctrl-wheel or `=`/`-` zoom the line as a lane's. The picture comes in with its first frame (a 4px blur and a fade). It takes `poster`, `captions` (WebVTT), `loop` (true), `muted` (true), `aspect` and `fps`, reports `onPlay(playing, time)` and `onSeek(time)` (a seek that landed, paused), and the handle has `video`, `timeline`, `time`, `playing`, `duration`, `play`, `pause`, `toggle`, `seek(t, { glide })`, `seekBy`, `focus` and `dispose`. Under it is `timeline(…, { variant: "line" })`, which you can bind to anything else with a clock.
|
|
1335
1363
|
|
|
@@ -1500,7 +1528,7 @@ A section's meta line leads with its **kicker** in text colour: `"PR 2 of 5"` by
|
|
|
1500
1528
|
|
|
1501
1529
|
By default the page only shows: no progress, notes or Copy feedback, no key hints, no statuses (a section's or an update's) and no counts, and a step's `options` sit side by side under their labels. `review: true` brings Kit's review tools back: the progress in the rail and its state dots, statuses, Copy feedback, picking between options (`1`–`9`) and the note bar. With review on, notes come from a launcher in the column's corner (`noteBar: "collapsed"`, the default) that opens into the bar on a click, `n`, a recording or ink, and folds back when it's empty; `"docked"` keeps the bar there, `"hidden"` drops it (`w.send()` still works). Typed, spoken or inked, each note carries what was on show (a video's time and chapter, a compare's loop and zoom, a diff's file) and a short log of what led up to it; `onFeedback`, `w.feedback` and `w.markdown()` hand it back. The page keeps the bar's height clear at its end and as its scroller's `scroll-padding-bottom`, so `j` / `k`, `go()` and the last step never land under it, and full window stops above it. `j` / `k` move between steps (`Home` / `End` to the first and last), `←` / `→` seek, `f` goes full window, `n` writes a note, `1`–`9` pick.
|
|
1502
1530
|
|
|
1503
|
-
Media is a bare URL (a video or picture), or `compare`, `video`, `image`, `diff`, `code`, `terminal`, `metric`, `callout`, `text`, `facts`, `decision`, `history`, `stack`, `table`, or `mount` (your own, `(el) => cleanup`; the one that isn't JSON). A step's media sit one under another (a group's room apart); `columns: 2` sets them side by side where there's room. Every picture, video
|
|
1531
|
+
Media is a bare URL (a video or picture), or `compare`, `video`, `image`, `diff`, `code`, `terminal`, `metric`, `callout`, `text`, `facts`, `decision`, `history`, `stack`, `table`, or `mount` (your own, `(el) => cleanup`; the one that isn't JSON). A step's media sit one under another (a group's room apart); `columns: 2` sets them side by side where there's room. Every picture, video, compare stage, code block and terminal (and a slot still loading) takes the same frame: the panel radius and a quiet fill, no ring. A diff sits in one plain well, drawn by its hairline alone (no fill), the panel radius and room inside: a box is a fill or a ring, never both. Nothing in a walkthrough is glass. A metric, a callout, facts, a decision, a table, a stack and a history sit on the page. Any slot's `label` (a metric's name, facts' `title`) is one line above it, the same for every kind: as written (sentence case), small and faint, with no rule between it and the media; a history's `label` is only its accessible name.
|
|
1504
1532
|
|
|
1505
1533
|
**Keep it scannable.** A step's title is its claim, in about ten words; its caption is one sentence; everything else goes in `more` (Markdown, blank lines between paragraphs), folded under the caption behind a small **More** toggle whose height springs open on the state spring while the words come into focus (a fade with a small blur, from the top). Sections take `more` under their summary, and the page under its own. Media that mounts after the first paint (near the view, or a picture still loading) comes in the same way; nothing moves on load.
|
|
1506
1534
|
|
|
@@ -1632,6 +1660,8 @@ Changing a token glides: colours, lengths, weights, glass and glow move on the p
|
|
|
1632
1660
|
|
|
1633
1661
|
A floating panel opens out of its chip like liquid glass. The press squeezes the cog (to 0.94, easing in over 60 ms); then each edge of the glass rides its own spring from the chip to the panel. The edges facing the way it travels lead on a stiff spring (as stiff as their axis carries the travel) and the others trail on a softer one, so the glass stretches toward the panel, overshoots a little and settles in under 300 ms, its corners going from the cog's circle to the panel's radius. The content follows a beat behind: blurred 8 px, faded and at 97%, revealed top first, landing exactly crisp. Closing runs it backwards and faster: the content blurs out first, then the glass pulls into the cog with its far edges trailing (about 200 ms). Esc, the close button or the chip itself folds it back; toggling mid-way reverses from wherever the shape is, keeping its velocity. The WebGPU overlay blurs, fades and scales the content in its shader, so both renderers match. A reload brings an open panel back in place, with no morph.
|
|
1634
1662
|
|
|
1663
|
+
**Moving it.** Drag a floating panel by its header (the title or the empty space beside the buttons): it follows the pointer 1:1, shows the closed hand only while it's carried, and stays where it's let go. The spot is kept with the store's values (`persist: "all"`), by the nearer side: a panel left on the right half keeps its distance from the right edge as the window changes width. It always shows inside the window, and on a phone (420px wide or less) it takes the width less an 8px gutter and scrolls inside the height. An inline panel (`mount`, `mountPanel`) belongs to its host's layout and doesn't move: lifting it out would leave its container's clipping, stacking and token overrides behind. For a panel you can move, open a floating one: `controls.attach(el)` puts a chip on `el`, and `createPanel(controls, anchor, close)` from `knobs/panel` opens one beside `anchor` from your own button (a debug bar's, say), with the same drag and kept spot.
|
|
1664
|
+
|
|
1635
1665
|
Reduced motion (`prefers-reduced-motion`) turns off token glides, the cascade, the pop, edge blur, haptics, the spring dot, the editors' height reveals and the chip's squeeze, and panels simply fade in and out, with no stretch or blur.
|
|
1636
1666
|
|
|
1637
1667
|
### Reacting to Random, Reset and presets
|
|
@@ -1682,6 +1712,7 @@ The page roles (**Space**: `--k-space-inside` 4 · `sibling` 8 · `title` 12 ·
|
|
|
1682
1712
|
|---|---|---|---|---|
|
|
1683
1713
|
| `--k-radius` | input | `8px` | `8px` | Control corner radius. 0 squares everything, including capsules. Scales with density. |
|
|
1684
1714
|
| `--k-radius-panel` | semantic | `calc(var(--k-radius) + var(--k-unit))` | `calc(var(--k-radius) + var(--k-unit))` | Panel and card radius, concentric with controls. Derived: `calc(var(--k-radius) + var(--k-unit))`. |
|
|
1715
|
+
| `--k-radius-frame` | semantic | `calc(var(--k-radius) + min(var(--k-radius) * 999, var(--_k-pad) + 1px))` | `calc(var(--k-radius) + min(var(--k-radius) * 999, var(--_k-pad) + 1px))` | A tuning panel's outer corners: its cards' radius plus its padding and border, so the cards, the preset strip and the focus lens inside run concentric. Squares when --k-radius is 0. Derived: `calc(var(--k-radius) + min(var(--k-radius) * 999, var(--_k-pad) + 1px))`. |
|
|
1685
1716
|
| `--k-round` | semantic | `calc(var(--k-radius) * 999)` | `calc(var(--k-radius) * 999)` | Capsule radius that squares when --k-radius is 0. Use instead of 999px. Derived: `calc(var(--k-radius) * 999)`. |
|
|
1686
1717
|
|
|
1687
1718
|
### Size
|
|
@@ -2039,11 +2070,12 @@ The package has two core exports selected through the standard `development` con
|
|
|
2039
2070
|
| `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). |
|
|
2040
2071
|
| `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 }`. |
|
|
2041
2072
|
| 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. |
|
|
2042
|
-
| Field meta `groupDescription?: string`, `sectionDescription?: string`, `sectionFolded?: boolean` | On any field or descriptor
|
|
2043
|
-
| `mountPanel(controls, container, options?)` (`knobs/panel`) | An inline panel. Beside theme, density, renderer and keys: `preset`, `header` (`false` or `{ title?, buttons? }`), `crumbs` (`true` adds the scope line), `session`, `groupFolded`, `subsectionFolded`, `tabs`, `tab`, `onTab`, `search`, `groupRandom(group, mode)`, `rowAccessory(key)`, `marks`, `haptics`, `detents`, `sound`. The handle adds `panel.fold(group, folded?)`, `panel.tab(id?)`, `panel.search(query?, chips?)` (returns `{ query, chips }`) and `panel.updateAccessories()`. |
|
|
2073
|
+
| Field meta `groupDescription?: string`, `sectionDescription?: string`, `sectionFolded?: boolean` | On any field or descriptor (the first field of a group or section that gives one wins): a muted line under the card's title (in a panel or a page) or a page section's heading; `sectionFolded` makes a page section foldable, starting folded. |
|
|
2074
|
+
| `mountPanel(controls, container, options?)` (`knobs/panel`) | An inline panel. Beside theme, density, renderer and keys: `preset`, `header` (`false` or `{ title?, buttons? }`), `crumbs` (`true` adds the scope line), `session`, `groupFolded`, `groupSummary`, `onActive`, `subsectionFolded`, `tabs`, `tab`, `onTab`, `search`, `groupRandom(group, mode)`, `rowAccessory(key)`, `marks`, `haptics`, `detents`, `sound`. The handle adds `panel.fold(group, folded?)`, `panel.tab(id?)`, `panel.search(query?, chips?)` (returns `{ query, chips }`) and `panel.updateAccessories()`. |
|
|
2044
2075
|
| `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. |
|
|
2045
2076
|
| `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. |
|
|
2046
|
-
| `MountPageOptions` folds | `sectionFolded?: SectionFolded` (`boolean \| Record<string, boolean>`), `foldStorage?: StoragePort \| null`, `nav?: boolean`, `groupSummary?(group, info: FoldSummaryInfo): string \| Node \| null
|
|
2077
|
+
| `MountPageOptions` folds | `sectionFolded?: SectionFolded` (`boolean \| Record<string, boolean>`), `foldStorage?: StoragePort \| null`, `nav?: boolean`, `groupSummary?(group, info: FoldSummaryInfo): string \| Node \| null` (a panel's option too), `sectionSummary?(section, info: FoldSummaryInfo): string \| Node \| null`; `FoldSummaryInfo` is `{ preset: string \| null, changed: number, keys: readonly string[] }`. |
|
|
2078
|
+
| `MountPanelOptions.onActive?: (at: PanelActive \| null) => void` | The card (`{ group }`) or row (`{ group, key }`) under the pointer or focus, `null` for nothing; on `mountPanel`, `controls.mount` and `mountPage`. At most once a frame, never the same value twice, and a last `null` when the panel goes. `PanelActive` is `{ group: string, key?: string }`. |
|
|
2047
2079
|
| `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`. |
|
|
2048
2080
|
| `page.reveal(target: { section: string } \| { group: string }, options?: { remember?: boolean }): void` | Open a section (or a group's card and its section) and glide it into view; `remember` defaults to `false`. |
|
|
2049
2081
|
| `page.sections(): readonly PageSectionInfo[]` / `page.onSections(listener: (sections: readonly PageSectionInfo[]) => void): () => void` | `PageSectionInfo` is `{ id, label, description?, folded, changed, groups: PageGroupInfo[] }`; `PageGroupInfo` is `{ id, label, folded, changed, preset: string \| null }`. |
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
DEFAULT_LINEAR,
|
|
11
11
|
motionCss,
|
|
12
12
|
storageKey
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-2g0znytq.js";
|
|
14
14
|
|
|
15
15
|
// src/core/theme.ts
|
|
16
16
|
var property = (k) => k.startsWith("--") ? k : `--k-${k}`;
|
|
@@ -1272,6 +1272,14 @@ var theme = { from: "theme" }, css = (s) => ({ css: s }), value = (s) => ({ valu
|
|
|
1272
1272
|
doc: "Panel and card radius, concentric with controls.",
|
|
1273
1273
|
deprecated: ["--k-panel-radius", "--ku-panel-radius"]
|
|
1274
1274
|
},
|
|
1275
|
+
{
|
|
1276
|
+
name: "radius-frame",
|
|
1277
|
+
tier: "semantic",
|
|
1278
|
+
type: "length",
|
|
1279
|
+
group: "Shape",
|
|
1280
|
+
source: css("calc(var(--k-radius) + min(var(--k-radius) * 999, var(--_k-pad) + 1px))"),
|
|
1281
|
+
doc: "A tuning panel's outer corners: its cards' radius plus its padding and border, so the cards, the preset strip and the focus lens inside run concentric. Squares when --k-radius is 0."
|
|
1282
|
+
},
|
|
1275
1283
|
{
|
|
1276
1284
|
name: "round",
|
|
1277
1285
|
tier: "semantic",
|
|
@@ -2294,7 +2302,16 @@ var COLOR_TOKENS = [
|
|
|
2294
2302
|
"rim",
|
|
2295
2303
|
"border-subtle",
|
|
2296
2304
|
"bg-page"
|
|
2297
|
-
], LENGTH_TOKENS = [
|
|
2305
|
+
], LENGTH_TOKENS = [
|
|
2306
|
+
"pad",
|
|
2307
|
+
"inset",
|
|
2308
|
+
"row",
|
|
2309
|
+
"gap",
|
|
2310
|
+
"radius",
|
|
2311
|
+
"radius-panel",
|
|
2312
|
+
"radius-frame",
|
|
2313
|
+
"font-size"
|
|
2314
|
+
], NUMBER_TOKENS = ["weight-label", "blur", "glow", "glass", "glass-tone"], ANIMATED = [...COLOR_TOKENS, ...LENGTH_TOKENS, ...NUMBER_TOKENS, "width"], TOKEN_TRANSITIONS = ANIMATED.map((t) => `--kt-${t} var(--k-token-t)`).join(","), source = (t) => t === "blur" ? "--k-blur" : `--_k-${t}`, register = (names, syntax, initial) => names.map((n) => `@property --kt-${n}{syntax:"${syntax}";inherits:false;initial-value:${initial}}`).join(`
|
|
2298
2315
|
`);
|
|
2299
2316
|
var DRIVERS = `${register(COLOR_TOKENS, "<color>", "transparent")}
|
|
2300
2317
|
${register(LENGTH_TOKENS, "<length>", "0px")}
|
|
@@ -2426,13 +2443,15 @@ ${GRAD_PROPS}
|
|
|
2426
2443
|
catches its top. The light rides on the edge (a shadow raised by its width, so only its top
|
|
2427
2444
|
shows), not inside the card's 1px border, where it read as a second edge. */
|
|
2428
2445
|
.knobs-panel{-webkit-user-select:none;user-select:none;transition:${TOKEN_TRANSITIONS},--k-armed ${ARMED_T};position:fixed;z-index:2147480001;width:var(--_k-width);max-width:calc(100vw - 16px);max-height:calc(100dvh - 16px);overflow:auto;padding:var(--_k-pad);
|
|
2429
|
-
background:color-mix(in srgb,var(--_k-bg) calc(100% - var(--_k-glass) * 20%),transparent);border:1px solid transparent;border-radius:var(--_k-radius-
|
|
2446
|
+
background:color-mix(in srgb,var(--_k-bg) calc(100% - var(--_k-glass) * 20%),transparent);border:1px solid transparent;border-radius:var(--_k-radius-frame);box-shadow:var(--_k-shadow-overlay),0 calc(var(--k-rim-width) * -1) 0 var(--_k-rim),0 0 0 var(--k-rim-width) var(--ka-edge),var(--ka-glow);scrollbar-width:thin;
|
|
2430
2447
|
/* Armed (focus inside: keys come here), the edge takes the accent and a faint glow lifts it. */
|
|
2431
2448
|
--ka-edge:color-mix(in srgb,var(--_k-accent) calc(var(--k-armed) * var(--k-armed-edge) * 100%),var(--_k-border-subtle));
|
|
2432
2449
|
--ka-glow:0 0 calc(var(--k-armed-glow) * 1px) color-mix(in srgb,var(--_k-accent) calc(var(--k-armed) * var(--k-armed-glow-a) * var(--k-chrome,1) * 100%),transparent);
|
|
2433
2450
|
--k-backdrop:blur(calc(var(--_k-glass) * 24px)) saturate(calc(1 + var(--_k-glass) * .8)) brightness(calc(1 + (var(--_k-glass-tone) - 1) * var(--_k-glass)));
|
|
2434
2451
|
-webkit-backdrop-filter:var(--k-backdrop);backdrop-filter:var(--k-backdrop);
|
|
2435
2452
|
animation:knobs-in .16s var(--k-ease);}
|
|
2453
|
+
/* On a phone a floating panel takes the width, less a gutter, and scrolls inside the height. */
|
|
2454
|
+
@media(max-width:420px){.knobs-panel:not(.knobs-inline){width:calc(100vw - 16px)}}
|
|
2436
2455
|
.knobs-panel[data-restored]{animation:none}
|
|
2437
2456
|
/* Opening from its chip, the panel's chrome rides a separate shape (see morph.ts) while the
|
|
2438
2457
|
content, laid out at full size, is clipped to it and revealed top first. */
|
|
@@ -2466,8 +2485,9 @@ ${GRAD_PROPS}
|
|
|
2466
2485
|
:is(.knobs-panel,.knobs-menu) :is(:focus,:focus-visible):where(:not(.knobs-accessory *)){outline:none}
|
|
2467
2486
|
|
|
2468
2487
|
/* Header */
|
|
2469
|
-
|
|
2470
|
-
.knobs-panel
|
|
2488
|
+
/* A floating panel's header carries it; the hand shows only while it's carrying (data-dragging). */
|
|
2489
|
+
.knobs-panel header{position:relative;display:flex;align-items:center;justify-content:space-between;gap:6px;touch-action:none;padding:0 var(--_k-inset) var(--k-section-gap);min-height:var(--k-icon-btn)}
|
|
2490
|
+
.knobs-panel[data-dragging]>header,.knobs-panel[data-dragging]>header *{cursor:grabbing}
|
|
2471
2491
|
/* The title row stays pinned while the rows scroll under it, in the panel (it scrolls once it's
|
|
2472
2492
|
bounded) or, with .knobs-pin-ancestor (which also makes the panel overflow:visible), in the
|
|
2473
2493
|
nearest scroller around it. Once rows are under it, and only then, it frosts (backdrop blur over
|
|
@@ -2482,7 +2502,7 @@ ${GRAD_PROPS}
|
|
|
2482
2502
|
@supports (animation-timeline:scroll()){
|
|
2483
2503
|
.knobs-panel:not(.knobs-page){scroll-timeline:--knobs-self block}
|
|
2484
2504
|
.knobs-panel.knobs-pin-ancestor:not(.knobs-page){overflow:visible;view-timeline:--knobs-view block}
|
|
2485
|
-
.knobs-panel:not(.knobs-page)>header{position:sticky;top:0;z-index:4;border-radius:max(0px,calc(var(--_k-radius-
|
|
2505
|
+
.knobs-panel:not(.knobs-page)>header{position:sticky;top:0;z-index:4;border-radius:max(0px,calc(var(--_k-radius-frame) - 1px)) max(0px,calc(var(--_k-radius-frame) - 1px)) 0 0;
|
|
2486
2506
|
--k-head-on:max(var(--k-head-s),var(--k-head-v));--k-head-bg:color-mix(in srgb,var(--_k-bg) 88%,transparent);
|
|
2487
2507
|
background:color-mix(in srgb,var(--k-head-bg) calc(var(--k-head-on) * 100%),transparent);
|
|
2488
2508
|
animation:knobs-head-s linear both;animation-timeline:--knobs-self;animation-range:0px 24px}
|
|
@@ -2492,7 +2512,6 @@ ${GRAD_PROPS}
|
|
|
2492
2512
|
.knobs-panel[data-gpu-active="true"]:not(.knobs-page)>header{background:transparent!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important}
|
|
2493
2513
|
.knobs-panel[data-gpu-active="true"]:not(.knobs-page)>header::after{display:none}
|
|
2494
2514
|
}
|
|
2495
|
-
.knobs-panel header:active{cursor:grabbing}
|
|
2496
2515
|
.knobs-panel header strong{flex:1;min-width:0;font-weight:var(--_k-weight-title);font-size:var(--_k-text-title);letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:opacity .2s var(--k-ease),color ${ARMED_T};
|
|
2497
2516
|
color:color-mix(in srgb,var(--_k-fg) calc(var(--k-title-rest) * 100%),var(--_k-fg-2))}
|
|
2498
2517
|
/* At rest a panel's title is a little quiet; armed (keys come here) it's at full strength. */
|
|
@@ -2632,6 +2651,13 @@ ${GRAD_PROPS}
|
|
|
2632
2651
|
.knobs-group-head .knobs-group-die{width:20px;padding:0;display:inline-flex;align-items:center;justify-content:center;border-radius:var(--_k-round);color:var(--_k-fg-3)}
|
|
2633
2652
|
.knobs-group-head .knobs-group-die[disabled]{opacity:var(--k-disabled-opacity);cursor:default}
|
|
2634
2653
|
.knobs-panel .knobs-group-head button:where(:not(.knobs-group-reset)){transition:background var(--k-hover-t),color var(--k-hover-t),opacity .2s,transform .12s}
|
|
2654
|
+
/* A panel card's header: its title, a folded card's summary and its buttons on one line, and its
|
|
2655
|
+
description on the next. The title takes its room first; the summary gets what's left. */
|
|
2656
|
+
.knobs-panel:not(.knobs-page) .knobs-group-head{flex-wrap:wrap;row-gap:0}
|
|
2657
|
+
.knobs-panel:not(.knobs-page) .knobs-group-head>.knobs-group-label{flex:1000 1 0;max-width:max-content;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2658
|
+
.knobs-group-head>.knobs-group-desc{order:1;flex:0 0 100%;padding:0;line-height:16px;white-space:normal;overflow:visible;text-wrap:pretty}
|
|
2659
|
+
.knobs-group-head>.knobs-group-summary{flex:1 1 0;max-width:max-content;margin-left:auto;font-size:var(--_k-text-meta);text-transform:none}
|
|
2660
|
+
.knobs-group-head[data-folded]>.knobs-group-summary:not(:empty){display:block}
|
|
2635
2661
|
|
|
2636
2662
|
/* Grouped cards: rows share one surface, split by hairlines that stay put whatever a row is doing */
|
|
2637
2663
|
.knobs-card{position:relative;margin-bottom:var(--k-section-gap);border-radius:var(--_k-radius);background:var(--_k-fill);overflow:hidden}
|
|
@@ -3338,7 +3364,7 @@ ${POINTER_ROW} .knobs-slider.knobs-modified:not(:hover,.knobs-active) .knobs-val
|
|
|
3338
3364
|
.knobs-group-summary{display:none;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--_k-fg-3);font-size:var(--_k-text-control);font-weight:500;letter-spacing:0;font-variant-numeric:tabular-nums}
|
|
3339
3365
|
.knobs-tile[data-folded]>.knobs-group-head .knobs-group-summary:not(:empty){display:block}
|
|
3340
3366
|
.knobs-tile[data-folded]>.knobs-group-head .knobs-group-ctl{display:none}
|
|
3341
|
-
.knobs-tile[data-fold-motion]>.knobs-group-head :is(.knobs-group-summary,.knobs-group-ctl){animation:knobs-fade .2s var(--k-ease) both}
|
|
3367
|
+
.knobs-tile[data-fold-motion]>.knobs-group-head :is(.knobs-group-summary,.knobs-group-ctl),.knobs-group-head[data-fold-motion]>.knobs-group-summary{animation:knobs-fade .2s var(--k-ease) both}
|
|
3342
3368
|
/* A highlight (page.highlight): the rest dims under a veil of the page's colour (--k-page-dim at
|
|
3343
3369
|
full), and what's highlighted on its own gets a dot in the gutter by its title. The veil and
|
|
3344
3370
|
dot are written each frame on the panel spring (see page.ts). */
|
|
@@ -4111,11 +4137,22 @@ function createRegistry(doc) {
|
|
|
4111
4137
|
return;
|
|
4112
4138
|
if (e.metaKey || e.ctrlKey || e.altKey || e.key !== PAGE_HINT_KEY)
|
|
4113
4139
|
return;
|
|
4114
|
-
if (isTyping(e.target) || !wide())
|
|
4140
|
+
if (isTyping(e.target) || !wide(e.target))
|
|
4115
4141
|
return;
|
|
4116
4142
|
if (registry.start())
|
|
4117
4143
|
e.preventDefault();
|
|
4118
|
-
},
|
|
4144
|
+
}, hostHints = () => [...doc.querySelectorAll("[data-hint]")].some((el) => !el.closest(".knobs-panel")), wide = (target) => {
|
|
4145
|
+
if (!listening)
|
|
4146
|
+
return !1;
|
|
4147
|
+
if (sources.some((s) => s.listen && s.rank >= 2) || hostHints())
|
|
4148
|
+
return !0;
|
|
4149
|
+
if (sources.length < 2)
|
|
4150
|
+
return !1;
|
|
4151
|
+
if (target === void 0)
|
|
4152
|
+
return !0;
|
|
4153
|
+
let piece = target?.closest?.(".knobs-ui,.knobs-pane-body");
|
|
4154
|
+
return Boolean(piece?.classList.contains("knobs-ui"));
|
|
4155
|
+
}, relisten = () => {
|
|
4119
4156
|
let next = sources.some((s) => s.listen);
|
|
4120
4157
|
if (next === listening)
|
|
4121
4158
|
return;
|