@kitlangton/knobs 0.4.0-next.0 → 0.4.0-next.10
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 +451 -54
- package/dist/dev/{chunk-7xgkerd2.js → chunk-3ygqgy1x.js} +23 -1
- package/dist/dev/{chunk-stbcgz1y.js → chunk-5pb7ymrd.js} +567 -471
- package/dist/dev/{chunk-t68smqkc.js → chunk-a7cjgy8x.js} +13 -11
- package/dist/dev/{chunk-pmkvb9ep.js → chunk-dyq4m24m.js} +2081 -507
- package/dist/dev/index.js +10 -5
- package/dist/index.js +25 -1
- package/dist/panel.js +3187 -1341
- package/dist/react.js +294 -6
- package/dist/review-react.js +18580 -0
- package/dist/review-solid.js +18482 -0
- package/dist/review.js +18488 -0
- package/dist/solid.js +292 -4
- package/dist/themes.js +1767 -7
- package/dist/types/core/adapter.d.ts +6 -2
- package/dist/types/core/appearance.d.ts +44 -0
- package/dist/types/core/glide.d.ts +7 -0
- package/dist/types/core/motion.d.ts +66 -0
- package/dist/types/core/root.d.ts +13 -0
- package/dist/types/core/store.d.ts +18 -5
- package/dist/types/core/theme.d.ts +10 -3
- package/dist/types/index.d.ts +2 -1
- package/dist/types/panel/gpu/color.d.ts +6 -0
- package/dist/types/panel/motion.d.ts +1 -1
- package/dist/types/panel/styles.d.ts +2 -3
- package/dist/types/review/font.d.ts +13 -0
- package/dist/types/review/foundations.d.ts +175 -0
- package/dist/types/review/index.d.ts +13 -0
- package/dist/types/review/react.d.ts +1 -0
- package/dist/types/review/solid.d.ts +1 -0
- package/dist/types/themes/define.d.ts +104 -0
- package/dist/types/themes/index.d.ts +16 -0
- package/dist/types/themes/knobs.d.ts +5 -0
- package/dist/types/themes/review.d.ts +8 -0
- package/dist/types/themes/void.d.ts +8 -0
- package/dist/types/tokens/color.d.ts +42 -0
- package/dist/types/tokens/css.d.ts +13 -0
- package/dist/types/tokens/evaluate.d.ts +8 -0
- package/dist/types/tokens/index.d.ts +4 -0
- package/dist/types/tokens/schema.d.ts +1280 -0
- package/dist/types/ui/bento-css.d.ts +1 -1
- package/dist/types/ui/compare-css.d.ts +1 -1
- package/dist/types/ui/compare-gpu.d.ts +6 -6
- package/dist/types/ui/compare-model.d.ts +85 -2
- package/dist/types/ui/compare.d.ts +18 -4
- package/dist/types/ui/diff.d.ts +4 -5
- package/dist/types/ui/history-css.d.ts +1 -0
- package/dist/types/ui/history-model.d.ts +249 -0
- package/dist/types/ui/history.d.ts +38 -0
- package/dist/types/ui/icons.d.ts +40 -0
- package/dist/types/ui/index.d.ts +9 -5
- package/dist/types/ui/look.d.ts +18 -13
- package/dist/types/ui/overlay-css.d.ts +1 -1
- package/dist/types/ui/player-css.d.ts +1 -0
- package/dist/types/ui/player.d.ts +51 -0
- package/dist/types/ui/readout-css.d.ts +1 -1
- package/dist/types/ui/readout-model.d.ts +5 -1
- package/dist/types/ui/review-look.d.ts +6 -0
- package/dist/types/ui/review-options.d.ts +28 -0
- package/dist/types/ui/rgba.d.ts +11 -0
- package/dist/types/ui/selection-pill-css.d.ts +1 -0
- package/dist/types/ui/selection-pill.d.ts +106 -0
- package/dist/types/ui/timeline-model.d.ts +27 -0
- package/dist/types/ui/timeline.d.ts +16 -1
- package/dist/types/ui/tree.d.ts +3 -2
- package/dist/types/ui/walkthrough-annotations.d.ts +42 -0
- package/dist/types/ui/walkthrough-blocks.d.ts +87 -0
- package/dist/types/ui/walkthrough-media.d.ts +30 -1
- package/dist/types/ui/walkthrough-model.d.ts +469 -5
- package/dist/types/ui/walkthrough.d.ts +27 -5
- package/dist/ui-react.js +8087 -2350
- package/dist/ui-solid.js +8100 -2363
- package/dist/ui.js +9751 -3963
- package/package.json +13 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ const detach = splat.attach(document.querySelector<HTMLElement>("#splat")!)
|
|
|
49
49
|
|
|
50
50
|
// Or mount an inline panel inside a container:
|
|
51
51
|
const unmount = splat.mount(document.querySelector<HTMLElement>("#sidebar")!, {
|
|
52
|
-
|
|
52
|
+
appearance: "system",
|
|
53
53
|
density: "compact",
|
|
54
54
|
})
|
|
55
55
|
|
|
@@ -856,7 +856,7 @@ A panel inside an app should look like the app's own controls. Four things get i
|
|
|
856
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) |
|
|
857
857
|
| `--k-frame` | `card` | `flat`: no card inside the host's own card (see below) |
|
|
858
858
|
| `--k-disabled-opacity` | `.32` | How far a disabled row dims |
|
|
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 |
|
|
859
|
+
| `--k-warn`, `--k-critical` | unset; knobs/ui falls back to `#ff9f0a`, `#ff453a` (light: `#c56a00`, `#e0352b`) | Readouts' warning and critical colours (meters past `warn` / `critical`, log levels), and a destructive confirm. Only knobs/ui pieces read them |
|
|
860
860
|
| `--k-control-h` | row − 2 × unit | Height of the controls inside a row: segments, menus, switches, number cells, key caps, toolbar buttons |
|
|
861
861
|
| `--k-value-size` | font size − `.5px` | Size of every readout (sliders, cells, ranges, dials, seeds, editor summaries) |
|
|
862
862
|
| `--k-button-bg`, `--k-button-line`, `--k-button-hover-bg`, `--k-button-hover-line` | each button's own | Fill and outline of custom actions, Export/Import, toolbar buttons, action rows and preset chips, at rest and hovered. With a fill or outline set, an action row is a sized button in its row and the presets strip a row of buttons (`data-buttons` on the panel) |
|
|
@@ -1049,14 +1049,14 @@ Both renderers draw the page. With `?gpu`, the overlay draws the header's conten
|
|
|
1049
1049
|
|
|
1050
1050
|
## Workspaces (`knobs/ui`)
|
|
1051
1051
|
|
|
1052
|
-
`knobs/ui` has the pieces a tool's workspace is built from: resizable split panes and docks, and an outliner tree. They are plain DOM and framework-neutral, use the same tokens as panels (`--k-*`,
|
|
1052
|
+
`knobs/ui` has the pieces a tool's workspace is built from: resizable split panes and docks, and an outliner tree. They are plain DOM and framework-neutral, use the same tokens as panels (`--k-*`, and a `theme` and `appearance`, as panels take), and work on their own or next to panels. A panel can mount inside a pane.
|
|
1053
1053
|
|
|
1054
1054
|
```ts
|
|
1055
|
-
import {
|
|
1055
|
+
import { themes } from "knobs/themes"
|
|
1056
1056
|
import { applyMove, split, tree } from "knobs/ui"
|
|
1057
1057
|
import { mountPanel } from "knobs/panel"
|
|
1058
1058
|
|
|
1059
|
-
const look = {
|
|
1059
|
+
const look = { theme: themes.void, appearance: "dark" as const }
|
|
1060
1060
|
const main = split(workspace, {
|
|
1061
1061
|
...look,
|
|
1062
1062
|
id: "workspace", // saves sizes and collapsed panes
|
|
@@ -1095,7 +1095,7 @@ Scrolling pane bodies, trees and the command palette's list fade their content o
|
|
|
1095
1095
|
**`tree(container, options)`** is an outliner. Rows are virtualised, so 10,000 of them scroll at full speed with about 35 in the DOM. When a branch opens, its children slide out from under it as one block with the rows below, like a drawer, and closing folds them back. The tree takes items with `id`, `label` and `children` (or `getId`, `getLabel` and `getChildren` for your own shape; an array, even empty, makes a parent that takes drops). Options:
|
|
1096
1096
|
|
|
1097
1097
|
- `icon` and `detail` for a glyph and dimmed text (or nodes of your own), or `render` for your own row content
|
|
1098
|
-
- `glide: true` to show the selected row as one highlight that
|
|
1098
|
+
- `glide: true` to show the selected row as the selection pill (`selectionPill`): one highlight that slides to the next row picked on the state spring (.3 s, no bounce), the row's text bright under it, instead of each row lighting in place; several selected rows light in place
|
|
1099
1099
|
- `toggles` for row accessories such as `visible` and `locked`, which show at rest only in their unusual state
|
|
1100
1100
|
- `filter` for a query or a test; matching rows show with their ancestors, the match marked
|
|
1101
1101
|
- `selection`, `expanded` and `rowHeight` (`--k-row` by default)
|
|
@@ -1133,12 +1133,12 @@ import { Split, Tree } from "knobs/ui/react" // or "knobs/ui/solid"
|
|
|
1133
1133
|
|
|
1134
1134
|
### Overlays: popovers, tooltips, the command palette, confirms and toasts
|
|
1135
1135
|
|
|
1136
|
-
`knobs/ui` also has the floating layers a workspace needs. They sit in one fixed layer above the page and the GPU overlay, come in on a spring (scale and blur from where they open, interruptible both ways; opacity only under reduced motion), and use the same tokens. `overlayLook({
|
|
1136
|
+
`knobs/ui` also has the floating layers a workspace needs. They sit in one fixed layer above the page and the GPU overlay, come in on a spring (scale and blur from where they open, interruptible both ways; opacity only under reduced motion), and use the same tokens. `overlayLook({ theme, appearance })` sets the look they all take; each also takes its own `look`.
|
|
1137
1137
|
|
|
1138
1138
|
```ts
|
|
1139
1139
|
import { commandPalette, commandsFrom, confirm, overlayLook, popover, toast, tooltip, tooltips } from "knobs/ui"
|
|
1140
1140
|
|
|
1141
|
-
overlayLook({
|
|
1141
|
+
overlayLook({ theme: themes.void, appearance: "dark" })
|
|
1142
1142
|
|
|
1143
1143
|
const menu = popover(button, list, { placement: "bottom-end", role: "menu", label: "Games" })
|
|
1144
1144
|
tooltip(pause, { text: "Pause the live feed", shortcut: "Alt+P" })
|
|
@@ -1181,7 +1181,7 @@ const clock = timeline(dock, {
|
|
|
1181
1181
|
})
|
|
1182
1182
|
```
|
|
1183
1183
|
|
|
1184
|
-
**`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; `arrows: { step: 0.5, shift: 2 }` makes them seconds, the playhead gliding there) and `,`/`.` always do
|
|
1184
|
+
**`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; `arrows: { step: 0.5, shift: 2 }` makes them seconds, the playhead gliding there) and `,`/`.` always do. A jump the hand didn't make catches up: a marker's click, a loop's wrap, a press on the track away from the playhead, play from elsewhere and `seek(t, { glide: true })` all carry the playhead from where it is on a quick spring (.15 s, no bounce) while the time lands at once; a drag stays 1:1, and moves during a catch-up follow the hand while the rest settles. There's no clock beside play: the time shows under the pointer on the track, in seconds, while hovering or scrubbing. `variant: "lane"` makes it one slim lane (a compare's own): ticks without labels, markers as faint chapter ticks with the current one (the last at or before the playhead) in text colour, nothing to edit; a pinch, `⌘`/ctrl-wheel or `=`/`-` zoom it around the pointer and a sideways scroll pans it, both 1:1, a double-click or `0` fits it again, and an up-and-down wheel is left to the page. 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`, `duration`, `fps`, `markers`, `ranges`, `view`, `seek`, `play`, `pause`, `toggle`, `update`, `fit`, `zoom`, `focus` and `dispose`. Pieces bound to a timeline hear it with `listen({ onSeek, onPlay, onView, onScrub, onRanges })` (it returns a stop), and `drive(clock)` makes playback follow another clock, such as a video's `currentTime`, so the two never drift. `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.
|
|
1185
1185
|
|
|
1186
1186
|
### Compare
|
|
1187
1187
|
|
|
@@ -1192,8 +1192,8 @@ import { compare, compareStrip, timeline } from "knobs/ui"
|
|
|
1192
1192
|
const review = compare(el, {
|
|
1193
1193
|
before: { src: "/runs/41.mp4" },
|
|
1194
1194
|
after: { src: "/runs/42.mp4" },
|
|
1195
|
-
mode: "side", // the default
|
|
1196
|
-
timeline: true,
|
|
1195
|
+
mode: "side", // the default, or "wipe"
|
|
1196
|
+
timeline: true, // a slim lane of its own; its markers are chapter ticks
|
|
1197
1197
|
onView: ({ zoom, loop }) => log(zoom, loop), // { u, v, w, h }, { start, end } | null
|
|
1198
1198
|
})
|
|
1199
1199
|
review.seekBy(0.5)
|
|
@@ -1202,13 +1202,20 @@ review.setLoop(true) // around the playhead; drag its ends on the timeline
|
|
|
1202
1202
|
// A long page of them: each loads near the view and lets go well off it.
|
|
1203
1203
|
for (const step of steps) compare(step.el, { before: step.before, after: step.after, timeline: true })
|
|
1204
1204
|
|
|
1205
|
+
// The difference, opted in as a third mode; a chapter's click jumps with a catch-up.
|
|
1206
|
+
const diff = compare(el, { before, after, modes: ["side", "wipe", "difference"], timeline: true })
|
|
1207
|
+
diff.timeline?.update({ markers: [{ id: "a", time: 1.5, label: "The card moves" }] })
|
|
1208
|
+
diff.seek(1.5, { glide: true })
|
|
1209
|
+
|
|
1205
1210
|
// Or composed: the app places the pieces and binds them to one timeline.
|
|
1206
1211
|
const clock = timeline(dock, { duration: 6, fps: 30 })
|
|
1207
1212
|
compare(viewer, { before, after, timeline: clock })
|
|
1208
1213
|
compareStrip(strip, { before, after, timeline: clock })
|
|
1209
1214
|
```
|
|
1210
1215
|
|
|
1211
|
-
**`compare(container, options)`** is a before/after viewer for videos and images (by extension, or `kind`).
|
|
1216
|
+
**`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 answers at once 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 small cache shared by every viewer and strip of the same source, and the nearest shows while the exact seek lands (mid-drag, far from any cached frame, `fastSeek` answers first; letting go seeks exactly). 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`, `strip`, `update`, `seek`, `seekBy`, `play`, `pause`, `toggle`, `step`, `setZoom`, `setLoop`, `focus` and `dispose`. A frame strip is off by default (`strip: true` puts one over the timeline); **`compareStrip(container, { before, after, timeline })`** is the same piece 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`. The Lab's View switch shows one on its scene timeline.
|
|
1217
|
+
|
|
1218
|
+
**`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.
|
|
1212
1219
|
|
|
1213
1220
|
### Activity
|
|
1214
1221
|
|
|
@@ -1278,7 +1285,7 @@ import { codeDiff, diffReview } from "knobs/ui"
|
|
|
1278
1285
|
// A patch's files in a tree beside the diff, in a split.
|
|
1279
1286
|
const review = diffReview(container, { ...look, id: "review", patch: gitShowOutput })
|
|
1280
1287
|
|
|
1281
|
-
// Or the diff alone
|
|
1288
|
+
// Or the diff alone: one file or many, from a patch or before and after text.
|
|
1282
1289
|
const diff = codeDiff(el, {
|
|
1283
1290
|
...look,
|
|
1284
1291
|
files: [{ name: "src/spring.ts", before, after }],
|
|
@@ -1291,13 +1298,13 @@ const diff = codeDiff(el, {
|
|
|
1291
1298
|
for (const step of steps) codeDiff(step.el, { ...look, patch: step.patch, height: "auto" })
|
|
1292
1299
|
```
|
|
1293
1300
|
|
|
1294
|
-
**`codeDiff(container, options)`** shows a code diff
|
|
1301
|
+
**`codeDiff(container, options)`** shows a code diff on the void, with no card, fill or ring (a hairline under its header and one under the code, as a walkthrough's code block has, and only changed lines tinted): a quiet header with the file's path, its `+12 −3`, ‹ 2 of 5 › when there are several files (`nav: false` hides it), a wrap toggle and Split | Unified (a segmented control, the selection pill sliding to the one picked), over the diff. Its buttons are Phosphor icons. The diff itself is drawn by [`@pierre/diffs`](https://diffs.com) (Shiki highlighting, word-level changes, hunks that expand when whole files are known), an optional peer dependency: install it to use diffs (`npm i @pierre/diffs`). It loads on first use, so `knobs/ui` doesn't carry it. Its colours come from the pieces' tokens, so dark, light, presets and `restyle()` carry into it. `--k-diff-add`, `--k-diff-del` and `--k-diff-bg` tune it, and `syntax: { dark, light }` picks Shiki themes for the code. It takes `patch` (`git diff` or `git show` output, one file or many) or `files: [{ name, before, after, lang? }]`, plus `file`, `layout`, `wrap`, `onFile`, `onLayout`, `onWrap` and `onLines(lines, file)` (dragging over line numbers selects lines). Changing the file, the layout or the wrap fades the new view in over the old in place (.3 s in, .24 s out, a 3px blur while it fades; a .16 s fade under reduced motion). With `height: "fill"` the card fills its container and each file keeps its scroll. With `height: "auto"` it's a self-contained piece for a long page: it's as tall as the file's diff up to `maxHeight` (640, or 70% of the window when that's less) and scrolls inside past that, springing to each file's height (.4 s, no bounce). It holds the height the diff will take from the first frame, counted from the patch's rows before the renderer has loaded, so nothing below it jumps. It also draws only within a screen of the viewport (of the scroller it sits in) and lets go of its views four screens away, keeping its height; `lazy: false` draws it at once, and `lazy: true` does this with `height: "fill"`. Pass `viewed` (names) or `onViewed(viewed, file)` for a Viewed tick in the header. `stack: true` puts every file down one scroller, as GitHub's Files changed does: each under its own header (status, path, counts and their bar, Viewed) that sticks to the top until the next file's pushes it off: flush (square, no card around each file, a gap between files), opaque in the code's colour, with a hairline under it only while it's stuck. The card's header then sums the change, ‹ › and `show()` glide to a file (.4 s), the file on show follows the scroll, and each file draws only near the view.
|
|
1295
1302
|
|
|
1296
|
-
Its scrollers fade their content out at an edge with more beyond it (a mask: at the top once scrolled, below a stuck header, and at the bottom while there's more below, `--k-diff-fade`, 20px), easing in and out on the .25 s spring. Its scrollbars are thin ones drawn over the code that take no room and stay hidden until the pointer is over the diff, or while you scroll or drag: `overlayScrollbar` up and down, and a bar per code column across, kept at the bottom of what shows of the column however far down a long file you are (the renderer's own, inside its shadow root, are hidden).
|
|
1303
|
+
Its scrollers fade their content out at an edge with more beyond it (a mask: at the top once scrolled, below a stuck header, and at the bottom while there's more below, `--k-diff-fade`, 20px), easing in and out on the .25 s spring. Its scrollbars are thin ones drawn over the code that take no room and stay hidden until the pointer is over the diff, or while you scroll or drag: `overlayScrollbar` up and down, and a bar per code column across, kept at the bottom of what shows of the column however far down a long file you are (the renderer's own, inside its shadow root, are hidden). On its own or on a surface the header and the code line up with the column's content (no inset across); in a split's pane or a `diffReview`, one inset runs all round, `--k-diff-inset` (8px, the renderer's own gap): the header's controls sit it in from the top and right, the path, the separators and the line numbers it in from the left, and the code ends it above the bottom. Inner corners (separators, the header's controls) are the outer corner less that distance, so they're concentric with the card. On another surface the diff takes no inset across, so its header and code line up with the surface's own content (a walkthrough's caption), and its corners are measured from the surface's. The header's hairline shows only once the code has scrolled under it. The handle has `files`, `file`, `layout`, `wrap`, `viewed`, `show(name)`, `step(±1)`, `update({ patch?, files?, file?, layout?, wrap?, viewed?, height?, maxHeight? })`, `ready` (once drawn) and `dispose()`. `update({ height: "fill" | "auto" })` switches in place, keeping the views, their scroll and the sticky headers; the new height isn't sprung, so a container whose size is moving (a walkthrough card going full window) is followed, not fought.
|
|
1297
1304
|
|
|
1298
1305
|
**One surface.** Cards never nest. Whatever draws a card carries `.knobs-surface` (a walkthrough's step; put it on your own cards too), and a split's pane counts as one; a piece inside a surface draws no card of its own, so a diff in a step, a pane or your card shows no second fill or ring, and a `diffReview` inside one parts its panes on a hairline instead of drawing them as cards. The diff's code keeps an opaque fill under its sticky line numbers, the colour showing behind it, so it still matches.
|
|
1299
1306
|
|
|
1300
|
-
**`diffReview(container, options)`** pairs it with a file list
|
|
1307
|
+
**`diffReview(container, options)`** pairs it with a file list, on the void as the diff is: a hairline parts the list from the diff, one runs under both headers and one under it all, and the filter is a line of text with no box. The list is a drawer at the review's left that springs open and shut between 0 and 256px (`listSize`; .4 s, no bounce), from a button at the start of the diff's header or `⌘B` / `Ctrl+B` while you're working in the review; the diff reflows as it moves, the list slides out from under it, and with an `id` it stays as you left it (`listOpen: false` starts it shut). The list is built on `tree()` and laid out as GitHub's, Graphite's and VS Code's are: a filter field on top (every word must be in the path: `ui diff`; `Esc` clears it, `↓` or `Enter` goes to the list), then a compact tree (28px rows, `--k-diff-row`) with folders folded into paths (`src/ui` is one row when `src` holds nothing else). Each file shows its change as a coloured letter (M amber, A green, D red and struck through, R blue; `--k-diff-mod` and `--k-diff-move` tune the two new ones), its `+12 −3` and a five-square bar of the share added and removed, and a tick to mark it viewed (on a folder, everything in it). Viewed files dim, the list's title counts them (`2/5`), and with an `id` they're remembered until their change moves on (each is saved with a fingerprint of its diff). The file on show has the selection pill: one highlight that slides to the next on the state spring (.3 s, no bounce), its row's text sharp and bright under it while other rows stay as they are (the tree's `glide` option). Picking a file shows it. The handle has the pieces, `tree` and `diff`, plus `listOpen`, `toggleList(open?)`, `viewed`, `setViewed(names, on)`, `filter(query)` and `update()`; `viewed` and `onViewed(viewed, file)` seed and report the marks, and `search: false` drops the field. To place the tree and the diff yourself, `diffFiles({ patch })` lists the files with their counts in tree order, `diffTree(files)` makes the tree's items, and `diffCounts(file, doc, { bar })` and `diffStatus(file)` give a row's counts and letter (a tree's `detail` and `icon`). `CodeDiff` and `DiffReview` in `knobs/ui/react` and `knobs/ui/solid` follow `patch`, `files`, `file`, `layout`, `wrap`, `viewed`, `height` and `maxHeight`. The demo is `examples/lab-diff.html`.
|
|
1301
1308
|
|
|
1302
1309
|
### Ideas grid (bento)
|
|
1303
1310
|
|
|
@@ -1328,54 +1335,166 @@ Click or `Enter` zooms a tile to fill the view, aspect-fit and centred: one cont
|
|
|
1328
1335
|
|
|
1329
1336
|
The handle has `update(items)`, `zoom(id | null)`, `zoomed`, `active`, `focus(id?)`, `picks`, `pick(ids)`, `plan`, `moving`, `stats()` (the frame loop's script time) and `dispose()`; `onZoom` and `onActive` report changes. It takes the pieces' tokens and `restyle`, in dark and light; tiles round at `--k-ui-bento-radius` (3px), square under `--k-frame: flat`; `--k-ui-bento-bg` and `--k-ui-scrim` set the page under the tiles and the zoom. `planBento(aspects, { width, height, … })` is the layout on its own, with `bentoNeighbour`, `bentoWindow` and `fitRect`. `Bento` in `knobs/ui/react` and `knobs/ui/solid` follows `items` and `picks`. The demo is `examples/ideas.html` (`?n=500` for a big set, `?groups`, `?hero=3`, `?perf` for a frame-time readout, `?bench` to scroll and zoom on its own and report).
|
|
1330
1337
|
|
|
1338
|
+
### Review pieces (`knobs/review`)
|
|
1339
|
+
|
|
1340
|
+
The walkthrough and what it shows (compare, player, code diff, history, facts, decision, taste test, `mediaSlot`) are their own subsystem: `knobs/review` (and `knobs/review/react`, `knobs/review/solid`). They have their own quiet look, `themes.review` (true black, flat, hairlines held to an APCA target, the platform's own face), one type scale and a short set of rules: [docs/review-design.md](docs/review-design.md), with every piece alone in `examples/review.html`. `knobs/ui` still exports them. `reviewFont(document, { href, sans: "Inter" })` gives the review look in a webfont.
|
|
1341
|
+
|
|
1342
|
+
A walkthrough, `mediaSlot()`, `tasteTest()`, `facts()` and `decision()` take `themes.review` unless you pass a theme or they sit inside a themed knobs element; pass `theme: themes.review` to a standalone `compare()` or `player()` for the same look. Every piece sets its type on the page scale (20, 16, 13, 12 and 11 px, at 400 and 500), fixed rather than following `--k-font-size` or density, so a compare or a diff reads the same in a panel as on a page. Under the hand they all behave alike: the arrow cursor, a hover that's one quiet fill landing at once, buttons 8 px in on their text side and 6 px by an icon. A label over a picture (compare's Before and After, its zoom pill and grip, the video line's time, an annotation's label) sits on a quiet dark scrim with no ring, in either appearance.
|
|
1343
|
+
|
|
1344
|
+
### Walkthrough
|
|
1345
|
+
|
|
1346
|
+
```ts
|
|
1347
|
+
import { walkthrough, type WalkthroughDoc } from "knobs/review"
|
|
1348
|
+
|
|
1349
|
+
const doc: WalkthroughDoc = {
|
|
1350
|
+
id: "2026-10-19-onboarding-prs",
|
|
1351
|
+
title: "Onboarding audit: before / after",
|
|
1352
|
+
sections: [
|
|
1353
|
+
{
|
|
1354
|
+
title: "Refresh models globally",
|
|
1355
|
+
link: "https://github.com/anomalyco/opencode/pull/57148",
|
|
1356
|
+
status: "ready",
|
|
1357
|
+
steps: [{ title: "Desktop model picker", media: { kind: "compare", before: "a.mp4", after: "b.mp4" } }],
|
|
1358
|
+
},
|
|
1359
|
+
],
|
|
1360
|
+
}
|
|
1361
|
+
const w = walkthrough(container, { doc, onFeedback: (fb) => send(fb) })
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
**`walkthrough(container, { doc })`** is the page an agent writes to show its work: a plain document (JSON) of sections and steps, each step a title, a caption, its media and what it asks (a note, or a pick between options). The contents rail sits sticky at the page's left edge (`rail: "right"` mirrors it, `false` drops it; under 960px it hides, or with review on folds to a bar on top with the progress and the export). Its rows share one text edge and one colour, the selection pill marks the step you're on, and a click goes to a row, and the column is centred in the room beside it, up to `--k-walkthrough-width` (960px): media take the column's width, prose stops at the measure. It's laid out on the page tokens: the intro, the updates and each section a section's room apart (`--k-space-section`, 96px, a hairline in the middle), the groups and steps inside a section a group's room apart (`--k-space-group`, 48px), prose at 68ch. Steps aren't boxes: they sit on the page with a hairline between them, and the spacing roles do the grouping; `cards: true` puts each on a card of its own, the one surface its media sit on (nothing inside draws a fill and a ring of its own). Titles take inline Markdown (`` `code` ``, `[links](…)`); the contents read them as plain words.
|
|
1365
|
+
|
|
1366
|
+
A section's meta line leads with its **kicker** in text colour: `"PR 2 of 5"` by default where its `link` is a pull request (counted among the sections that link one). The doc's `kickers` counts with another noun, `{ noun: "Part" }` giving every section "Part 2 of 6", or none with `false`. A section's own `kicker: "Epilogue"` wins over its count and `kicker: false` hides it, without renumbering the rest. The contents rail isn't numbered. Its PR link follows in faint, joined by `·` (its `status` only with review on). One type scale: the page's title 20/28, a section's 16/24, a step's 13/20 at 500, prose 13/20 in the second ink, meta 11/16 in the third. Steps aren't numbered on screen, but a ref still names one by its key, id or number (`"1.2"`): `step` on an update, a history event, a stack item or a table cell, and clicking goes there as `j` / `k` do.
|
|
1367
|
+
|
|
1368
|
+
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.
|
|
1369
|
+
|
|
1370
|
+
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 and compare stage (and a slot still loading) takes the same frame: the panel radius and a quiet fill, no ring. A diff, code and a terminal sit 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. A slot's `label` reads as written (sentence case), small and faint above it.
|
|
1371
|
+
|
|
1372
|
+
**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.
|
|
1373
|
+
|
|
1374
|
+
```json
|
|
1375
|
+
{ "title": "Retired models get a way out", "caption": "After: **Pick another model**, and the prompt back.",
|
|
1376
|
+
"more": "A mock provider answers 410 Gone.\n\n**Pick another model** opens the picker." }
|
|
1377
|
+
```
|
|
1378
|
+
|
|
1379
|
+
**Pull requests and issues** read as capsules: their state's icon (merge, pull request, draft, closed; a circle-dot for an issue) and number in a quiet tint of the state (open green, merged violet, draft and closed faint, unknown neutral), a faint check where its checks pass, the title and state on hover. Every ref becomes one: a section's PR `link` on the meta line ("PR 3 of 9 · ⟨#57158⟩"), and in titles, captions, summaries, `more` and table cells, a link to a pull request or issue whose words are its number, inline code that's just a ref, and a bare `#57148` (`issue:#123` for an issue). The state comes from the doc's `prs` by number (`{ "57148": { state: "merged", title: "…" } }`, `kind: "issue"` for an issue), else a `stack` on the page (an item's title and the first word of its `status`), else the section whose `link` is that PR. A ref needs no `repo` to be a chip; `repo: "owner/name"` only links the ones whose text has no URL. So a caption needn't spell out "#57158 (Draft · Green)".
|
|
1380
|
+
|
|
1381
|
+
**The feed** (`updates` and `qa`, newest first) is a calm list a group's room apart, a hairline between items and no boxes: a meta line (when · the step's PR, and `status` with review on), the question in text colour (or the update), its answer in dim prose, and its `trail` (what Kit did before asking, `"0:04 · Played the compare"`) folded under a small faint **Context** toggle whose height springs open on the state spring (.3 s) while the lines fade in place under a soft bottom edge. `status: "shipped"` reads Shipped, `"changes"` reads Changes queued.
|
|
1382
|
+
|
|
1383
|
+
```json
|
|
1384
|
+
{ "qa": [{ "at": "2026-10-19T12:10:00-07:00", "q": "Why is there no video for #57158?",
|
|
1385
|
+
"a": "It's the installer redirect: nothing to watch.", "step": "4.1", "status": "shipped",
|
|
1386
|
+
"trail": ["0:03 · Viewed TUI /connect", "0:11 · Played the compare at 0:04"] }] }
|
|
1387
|
+
```
|
|
1388
|
+
|
|
1389
|
+
The demo is `examples/walkthrough.html`.
|
|
1390
|
+
|
|
1391
|
+
**`history`**: drawn by **`history(container, { data })`** (also exported on its own): a shared date axis (month ticks, today's line) over lanes, one per channel or track, each carrying spans whose tone says their state, marks for releases and deletions, faint bands for phases; hovering shows a date line and a tooltip, and a click or `←` / `→` opens one detail area under the chart. Write the lanes shape (`lanes: [{ id, label, spans: [{ from, to?, state, label?, detail?, provenance?, evidence?, step? }] }]`, `events: [{ date, lane?, title, type?, tone?, provenance?, step? }]`, `bands`, `states`, `axis`), or the older groups of dated events below, which it lays on one code lane under a band per heading. `provenance: "inferred"` hatches a span and hollows a mark; a `step` gets "Go to step" in the detail; `label` names it, and `marks` picks which event types are marked from the start (releases and deletions, or `"all"`).
|
|
1392
|
+
|
|
1393
|
+
```json
|
|
1394
|
+
{ "kind": "history", "groups": [{ "heading": "v2 dropped it", "summary": "Jun 26 – Jul 30", "events": [
|
|
1395
|
+
{ "date": "Jun 26, 2026", "age": "116d ago", "title": "The v2 TUI rewrite leaves out DialogRetryAction",
|
|
1396
|
+
"detail": "`session/index.tsx` replaced without the listener.", "tone": "bad",
|
|
1397
|
+
"provenance": "verified", "source": "git", "evidence": [{ "label": "e6f660fecf" }] },
|
|
1398
|
+
{ "date": "Oct 20, 2026", "title": "/install still 307s to v1", "tone": "warn",
|
|
1399
|
+
"provenance": "inferred", "source": "edge", "step": "4.8" } ] }] }
|
|
1400
|
+
```
|
|
1401
|
+
|
|
1402
|
+
**`facts`**: a short list of term → value pairs, for what the user sees, what happens and the evidence: terms small and faint in a gutter on the value's baseline, values in prose. `facts` is `{ term: value }` or `[{ term, value, code? }]`; a value can be a list (one under another), `` `code` `` in it reads as a small mono chip, and `code: true` makes each value (a string splits at commas) a chip, for file:line evidence. `groups: [{ title, facts }]` sets several runs under headings with a hairline between. Where it's narrow (under 400 px), each term sits over its value. **`facts(container, { facts })`** draws one on its own.
|
|
1403
|
+
|
|
1404
|
+
```json
|
|
1405
|
+
{ "kind": "facts", "groups": [
|
|
1406
|
+
{ "title": "v2 TUI (origin/dev)", "facts": [
|
|
1407
|
+
{ "term": "What the user sees", "value": "Two rows, `OpenCode Go` first" },
|
|
1408
|
+
{ "term": "Evidence", "value": "dialog-integration.tsx:49-56, opencode.ts:271-288", "code": true } ] } ] }
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
**`decision`**: N ways to go, one recommended. The options sit side by side only in whole rows (four go 4, 2 or 1 across; three go 3 or 1), as many columns as fit at 280 px, with hairlines between columns and rows and no cards. Each is a kicker (`Option 1 · Recommended`, the mark a quiet word in text colour; `noun` changes the word), its title, a `summary` sentence, optional `media`, and what changes as facts (`facts`, then `pros` and `cons` as For and Against). A taste test (`options` on a step, Kit's pick) looks the same, its head the radio and a check at the kicker's end. **`decision(container, { options })`** draws one on its own.
|
|
1412
|
+
|
|
1413
|
+
```json
|
|
1414
|
+
{ "kind": "decision", "options": [
|
|
1415
|
+
{ "title": "One OpenCode row in /connect", "summary": "Signing in already unlocks Go models.",
|
|
1416
|
+
"recommended": true, "facts": { "Size": "~30 lines" } },
|
|
1417
|
+
{ "title": "Keep Go as a shortcut", "summary": "Both rows stay.", "cons": ["Two rows that do one job"] } ] }
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
**`stack`**: PRs in merge order as a branch graph. `branch` is `"head → base"` (or `head` and `base`); an item whose base is another's head hangs under it, joined by a drawn rail, and the rest sit on the trunk (`base`, else the branch most items merge into), named at the top. An item on a branch that's neither shows it. `delta` is `"+164 / -18"` or `{ add, del }`, drawn green and red; its PR's state shows the GitHub way, from the doc's `prs` or the first word of `status` (`merged`, `open`, `draft`, `closed`, any case), never from `tone`: the merge or pull-request glyph and word, open and merged quietly tinted, draft and closed faint, and a small check after it where `status` says the checks pass (`green`, `passing`, `CI`). Any other `status` is a chip in its `tone`. `step` (or `stepId`) makes the whole row go to that step, the row filling under the pointer, with nothing trailing it; the PR number links out (`href`, else the doc's `prs` or `repo`).
|
|
1421
|
+
|
|
1422
|
+
```json
|
|
1423
|
+
{ "kind": "stack", "base": "dev", "items": [
|
|
1424
|
+
{ "order": "01", "pr": "#57146", "href": "https://github.com/anomalyco/opencode/pull/57146", "title": "Typed free-limit error",
|
|
1425
|
+
"branch": "free-limit-card → dev", "delta": "+193 / -8", "status": "Stack base", "tone": "good", "step": "4.1" },
|
|
1426
|
+
{ "pr": "#57466", "title": "Restore the Desktop dialog", "branch": "free-limit-desktop → free-limit-card",
|
|
1427
|
+
"delta": { "add": 446, "del": 47 }, "status": "Stacked on #57146", "tone": "good", "summary": "Modal, notice and prompt recall." } ] }
|
|
1428
|
+
```
|
|
1429
|
+
|
|
1430
|
+
**`table`**: a real `<table>` for audits and matrices. Rows are cells by column key, bare or under `cells`; a cell is text, a number, or `{ text, href?, code?, badge?, tone?, step? }` (a `badge` is a tone chip). A row with a `step` on any cell goes there: the whole row, a wash under the pointer, Enter when it has focus, and nothing trailing it, so every row lines up. Columns take `mono` and `align` (number columns go right by themselves). Figures are tabular; a tall table keeps its head stuck, and its bars show thin on hover. `caption` goes under it. Where the step is narrower than the columns' natural width (long text keeping a measure, code on one line), each row becomes a block: its `primary` cell (the first column by default) as its title with its badges at the end of the line, the rest as term and value under it, the blocks parted by hairlines. `stack: "always"` always stacks; `stack: "never"` scrolls across instead, edges fading where there's more.
|
|
1431
|
+
|
|
1432
|
+
```json
|
|
1433
|
+
{ "kind": "table", "caption": "From a clean HOME, Oct 20", "columns": [
|
|
1434
|
+
{ "key": "channel", "label": "Entry point", "mono": true }, { "key": "days", "label": "Days on v2" },
|
|
1435
|
+
{ "key": "state", "label": "State", "align": "right" } ],
|
|
1436
|
+
"rows": [ { "cells": { "channel": { "text": "npm i -g @opencode-ai/cli", "code": true }, "days": 39,
|
|
1437
|
+
"state": { "text": "Broken", "badge": "bad" } } },
|
|
1438
|
+
{ "channel": "brew install …/opencode-v2", "days": 31, "state": { "text": "Pending", "badge": "info", "step": "4.6" } } ] }
|
|
1439
|
+
```
|
|
1440
|
+
|
|
1441
|
+
**`annotations`** on an `image`, a `video` or a `compare` draw the eye to what matters: `[{ x, y, w?, h?, label?, at?: [start, end], shape?: "box" | "dot" | "arrow" }]`, in 0–1 of the picture (a box's top-left, or the point). A box takes a calm ring, a point a dot (or an arrow pointing at it), each with a small label on a dark scrim; on a video or compare one shows only between its `at` seconds, fading in and out with a small blur. On a compare they're drawn on both sides and stay on their spot through a zoom. They're on by default; the eye button on the picture, or `a`, hides or shows every one on the page.
|
|
1442
|
+
|
|
1443
|
+
A **`video`** plays in **`player()`**, the same look as a compare: the picture, then one thin line under it to seek on, its chapters as segments.
|
|
1444
|
+
|
|
1445
|
+
**`chapters`** on a `video` (or a `compare`): a readout list under it, a row each (number · label · start). A click selects the chapter at once while the media seeks there and plays, and goes in the note's log; the selection pill slides to the chapter playing (the last that started at or before the playhead) and fades out in place outside them, and a note's `media` names it (`chapter`). A chapter ends at `end`, else `start + duration`, else the next one's start. On a compare they're also its timeline's markers. A video's `captions` (or `captionsSrc`) is a WebVTT URL, shown by default.
|
|
1446
|
+
|
|
1447
|
+
```json
|
|
1448
|
+
{ "kind": "video", "src": "journey.mp4", "poster": "journey.png", "captions": "captions.vtt", "chapters": [
|
|
1449
|
+
{ "id": "scorecard", "label": "Scorecard", "start": 0, "duration": 9.57 },
|
|
1450
|
+
{ "label": "Today (broken)", "start": 9.57 }, { "label": "After", "start": 35.38 } ] }
|
|
1451
|
+
```
|
|
1452
|
+
|
|
1331
1453
|
## Theming, Density & WebGPU Renderer
|
|
1332
1454
|
|
|
1333
|
-
Panels
|
|
1455
|
+
Panels take a `theme`, an `appearance`, `density: "compact" | "default" | "comfortable"` and `renderer: "gpu" | "dom" | "auto"` (or `?gpu` in the URL), on `defineControls`, `mountPanel` or `<KnobsPanel />`; every `knobs/ui` piece takes the same `theme` and `appearance`, and `restyle(target, look)` changes them on what's already on screen. The header title defaults to the controls id, written as words (`"synth-voice"` reads "Synth voice"); pass `title` to override it or `title: false` to hide it.
|
|
1456
|
+
|
|
1457
|
+
### Themes and appearance
|
|
1458
|
+
|
|
1459
|
+
Three themes ship in `knobs/themes`: **Knobs** (the default: a near-black glass panel, a white accent, alpha hairlines), **Void** (true black and flat, solid hairlines, text capped at `#ededed`, a black stage behind compared media; for OLED screens and video review) and **Review** (`themes.review`: Void with hairlines you can see, their alphas set by APCA against the page at Lc 9, 12 and 17, fills at 6, 10 and 14%, white in light, and the platform's own face; the review pieces' default). Each has a dark and a light appearance. `--k-before` and `--k-after` name a comparison's two sides, neutral by default (`fg-3` and `fg`).
|
|
1334
1460
|
|
|
1335
|
-
|
|
1461
|
+
```ts
|
|
1462
|
+
import { defineTheme, themes } from "knobs/themes"
|
|
1463
|
+
|
|
1464
|
+
mountPanel(controls, column, { theme: themes.void })
|
|
1465
|
+
split(workspace, { theme: themes.void, appearance: "dark", panes })
|
|
1466
|
+
|
|
1467
|
+
// A theme of your own from a few dials, both appearances generated (checked against APCA).
|
|
1468
|
+
const ocean = defineTheme({
|
|
1469
|
+
name: "ocean",
|
|
1470
|
+
extends: themes.knobs, // optional
|
|
1471
|
+
gray: { hue: 240, chroma: 0.008 },
|
|
1472
|
+
accent: "#4f8cff", // or { dark, light }
|
|
1473
|
+
contrast: 60, // 0–100
|
|
1474
|
+
radius: 6,
|
|
1475
|
+
density: "compact",
|
|
1476
|
+
material: "solid", // "glass" | "solid" | "flat"
|
|
1477
|
+
tokens: { dark: { "bg-page": "#05070a" } }, // any token, per appearance
|
|
1478
|
+
})
|
|
1479
|
+
```
|
|
1336
1480
|
|
|
1337
|
-
|
|
1481
|
+
`appearance` is `"dark"`, `"light"`, `"system"` (the OS setting) or `"inherit"`, the default: the page decides, through a `color-scheme`, a `.dark`/`.light` class, `[data-theme]` or `[data-appearance]` on an ancestor, or an outer knobs element, else the OS. A theme is a value: `theme.dark` and `theme.light` hold every token resolved, `theme.gpu` the colours as floats for WebGPU, and `theme.css()` its stylesheet. The old `theme: "dark" | "light" | "auto"` still works as an appearance, and `preset` (`labPreset`, `problemsPreset`) still lays its tokens over the theme; both are deprecated. `examples/themes.html` shows every piece in both themes and appearances, with a builder for `defineTheme`'s dials that copies the call as code.
|
|
1338
1482
|
|
|
1339
|
-
|
|
1483
|
+
### Overriding tokens
|
|
1484
|
+
|
|
1485
|
+
Set any token on `:root`, `.knobs-panel` or any ancestor; a host's value wins at whatever level it's set. Tokens derived from others (hover and selected fills from the surfaces, the slider handle from the accent, sizes from `--k-density`) are derived on each panel and knobs/ui piece, so an override anywhere above them carries through. The full list, with each theme's values, is under [Tokens](#tokens); the old names (`--k-surface`, `--k-text`, `--ku-*`) still work for now.
|
|
1340
1486
|
|
|
1341
1487
|
```css
|
|
1342
1488
|
:root {
|
|
1343
1489
|
--k-bg: #1c1c1e;
|
|
1344
|
-
--k-
|
|
1345
|
-
--k-
|
|
1346
|
-
--k-
|
|
1347
|
-
--k-text: rgba(255, 255, 255, 0.92);
|
|
1348
|
-
--k-label: rgba(255, 255, 255, 0.64);
|
|
1349
|
-
--k-accent: #fff;
|
|
1350
|
-
--k-modified: #ff9f0a;
|
|
1351
|
-
--k-width: 288px;
|
|
1352
|
-
--k-pad: 10px;
|
|
1353
|
-
--k-inset: 10px;
|
|
1354
|
-
--k-row: 32px;
|
|
1355
|
-
--k-gap: 4px;
|
|
1356
|
-
--k-radius: 8px;
|
|
1357
|
-
--k-panel-radius: 12px;
|
|
1358
|
-
--k-font-size: 12px;
|
|
1359
|
-
--k-label-weight: 500;
|
|
1360
|
-
--k-blur: 1; /* motion strength: readout blur and pill stretch */
|
|
1361
|
-
--k-motion-blur: 0; /* up to 1: moving fill edges and pills smear by their speed */
|
|
1362
|
-
--k-range-blur: .5; /* up to 1: a range band's gliding ends smear by their speed */
|
|
1363
|
-
--k-curve-blur: .5; /* up to 1: morphing curves, easings and gradients smear by their speed */
|
|
1364
|
-
--k-step-resist: 0.25; /* a stepped slider's pull off its stop (0 snaps, 1 follows) */
|
|
1365
|
-
--k-glow: 0.5;
|
|
1366
|
-
--k-roll: 420; /* readout roll (ms) */
|
|
1367
|
-
--k-drag-roll: 0; /* roll while dragging (ms; 0 snaps) */
|
|
1368
|
-
--k-cascade: 12; /* preset ripple between rows (ms; 0 moves them together) */
|
|
1369
|
-
--k-glass: 0.6; /* 0 is opaque; up to 1 thins the panel over a blurred backdrop */
|
|
1370
|
-
--k-glass-tone: 0.5; /* backdrop brightness under full glass (light theme: 1.08) */
|
|
1371
|
-
--k-spring: 0.24 0.16; /* duration and bounce values settle on (GPU); pair with: */
|
|
1372
|
-
--k-fill-ease: linear(...); /* springCss(value).easing, for CSS transitions */
|
|
1373
|
-
--k-fill-ms: 300; /* springCss(value).duration */
|
|
1374
|
-
--k-reveal-ease: cubic-bezier(.2, .8, .2, 1); /* how editors and messages open */
|
|
1375
|
-
--k-rim: rgba(255, 255, 255, 0.09);
|
|
1490
|
+
--k-accent: #4f8cff;
|
|
1491
|
+
--k-radius: 6px;
|
|
1492
|
+
--k-density: 0.875;
|
|
1376
1493
|
}
|
|
1377
1494
|
```
|
|
1378
1495
|
|
|
1496
|
+
The panel's motion dials are tokens too: `--k-blur` (readout blur and pill stretch), `--k-motion-blur`, `--k-range-blur`, `--k-curve-blur`, `--k-step-resist`, `--k-roll` and `--k-drag-roll` (readout roll, ms), `--k-cascade` (preset ripple, ms), `--k-glass-tone`, `--k-reveal-ease`, and the spring values settle on, `--k-spring` (GPU) with `--k-fill-ease` and `--k-fill-ms` (`springCss(value)`, for CSS).
|
|
1497
|
+
|
|
1379
1498
|
Changing a token glides: colours, lengths, weights, glass and glow move on the panel spring (`--k-fill-ease` over `--k-fill-ms`), so a theme switch or a preset of tokens animates in both renderers. While a control is dragged they follow it step for step.
|
|
1380
1499
|
|
|
1381
1500
|
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.
|
|
@@ -1400,6 +1519,281 @@ A reset that went back to the active preset rather than the defaults carries `ba
|
|
|
1400
1519
|
|
|
1401
1520
|
`patch` holds only the fields that changed. The die page (`examples/die.ts`) rolls its demo dice this way.
|
|
1402
1521
|
|
|
1522
|
+
## Tokens
|
|
1523
|
+
|
|
1524
|
+
Generated from the token schema (`bun run scripts/tokens-table.ts`), with the Knobs theme's values. Inputs are the dials a theme turns, semantic tokens are what pieces read, hooks style one component.
|
|
1525
|
+
|
|
1526
|
+
The page roles (**Space**: `--k-space-inside` 4 · `sibling` 8 · `title` 12 · `group` 48 · `section` 96 · `column` 24, `--k-pad-page` 24 (16 under 640px) and `--k-pad-card` 12; **Type**: `--k-type-heading` 20/28 · `title` 16/24 · `body` 13/20 · `label` 12/16 · `small` 11/16, with `--k-leading-*`) are what a page built from knobs/ui (a walkthrough) is laid out on. They don't scale with density: density sizes a panel's controls, and the panel doesn't read them. Two weights (400, 500), mono one size down from what's around it, tabular figures.
|
|
1527
|
+
|
|
1528
|
+
<!-- tokens:start -->
|
|
1529
|
+
|
|
1530
|
+
### Accent
|
|
1531
|
+
|
|
1532
|
+
| Token | Tier | Dark | Light | |
|
|
1533
|
+
|---|---|---|---|---|
|
|
1534
|
+
| `--k-accent` | input | `#ffffff` | `#1c1c1e` | The one accent: primary actions, handles, focus. Hover, soft and selection derive from it in CSS. |
|
|
1535
|
+
| `--k-accent-hover` | semantic | `#e5e5e5` | `#2e2e30` | Hovered accent. Derived: `color-mix(in oklch,var(--k-accent) 90%,var(--k-bg))`. |
|
|
1536
|
+
| `--k-accent-soft` | semantic | `#ffffff26` | `#1c1c1e26` | Accent tint behind accent content. Derived: `color-mix(in oklch,var(--k-accent) 15%,transparent)`. |
|
|
1537
|
+
| `--k-accent-fg` | semantic | `#ffffff` | `#1c1c1e` | Accent as text on bg (≥ Lc 60). |
|
|
1538
|
+
| `--k-on-accent` | semantic | `#111111` | `#ffffff` | Content placed on a solid accent. |
|
|
1539
|
+
|
|
1540
|
+
### Inputs
|
|
1541
|
+
|
|
1542
|
+
| Token | Tier | Dark | Light | |
|
|
1543
|
+
|---|---|---|---|---|
|
|
1544
|
+
| `--k-contrast` | input | `1` | `1` | Contrast multiplier for fills and alpha borders (1 = default; the theme's 0–100 dial maps 50 → 1). |
|
|
1545
|
+
|
|
1546
|
+
### Shape
|
|
1547
|
+
|
|
1548
|
+
| Token | Tier | Dark | Light | |
|
|
1549
|
+
|---|---|---|---|---|
|
|
1550
|
+
| `--k-radius` | input | `8px` | `8px` | Control corner radius. 0 squares everything, including capsules. Scales with density. |
|
|
1551
|
+
| `--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))`. |
|
|
1552
|
+
| `--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)`. |
|
|
1553
|
+
|
|
1554
|
+
### Size
|
|
1555
|
+
|
|
1556
|
+
| Token | Tier | Dark | Light | |
|
|
1557
|
+
|---|---|---|---|---|
|
|
1558
|
+
| `--k-density` | input | `1` | `1` | Size multiplier: compact .875, default 1, comfortable 1.1875. |
|
|
1559
|
+
| `--k-unit` | semantic | `4px` | `4px` | The spacing unit. |
|
|
1560
|
+
| `--k-row` | semantic | `round(calc(32px * var(--k-density)),2px)` | `round(calc(32px * var(--k-density)),2px)` | Row height. Derived: `round(calc(32px * var(--k-density)),2px)`. |
|
|
1561
|
+
| `--k-width` | semantic | `288px` | `288px` | Panel width. |
|
|
1562
|
+
|
|
1563
|
+
### Type
|
|
1564
|
+
|
|
1565
|
+
| Token | Tier | Dark | Light | |
|
|
1566
|
+
|---|---|---|---|---|
|
|
1567
|
+
| `--k-font` | input | `system-ui,-apple-system,"SF Pro Text",sans-serif` | `system-ui,-apple-system,"SF Pro Text",sans-serif` | UI font stack. |
|
|
1568
|
+
| `--k-mono` | input | `"SF Mono",SFMono-Regular,ui-monospace,Menlo,monospace` | `"SF Mono",SFMono-Regular,ui-monospace,Menlo,monospace` | Monospace font stack. |
|
|
1569
|
+
| `--k-type-heading` | semantic | `20px` | `20px` | A page's title, weight 500. |
|
|
1570
|
+
| `--k-leading-heading` | semantic | `28px` | `28px` | Line height for type-heading. |
|
|
1571
|
+
| `--k-type-title` | semantic | `16px` | `16px` | Section titles (under their kicker), weight 500. |
|
|
1572
|
+
| `--k-leading-title` | semantic | `24px` | `24px` | Line height for type-title. |
|
|
1573
|
+
| `--k-type-body` | semantic | `13px` | `13px` | Prose, questions, list rows, step titles; 400, or 500 for emphasis. |
|
|
1574
|
+
| `--k-leading-body` | semantic | `20px` | `20px` | Line height for type-body. |
|
|
1575
|
+
| `--k-type-label` | semantic | `12px` | `12px` | Mono inside body text: code and paths. |
|
|
1576
|
+
| `--k-leading-label` | semantic | `16px` | `16px` | Line height for type-label. |
|
|
1577
|
+
| `--k-type-small` | semantic | `11px` | `11px` | Meta, controls, keys, timestamps; mono inside labels. |
|
|
1578
|
+
| `--k-leading-small` | semantic | `16px` | `16px` | Line height for type-small. |
|
|
1579
|
+
| `--k-font-size` | semantic | `round(calc(12px * (1 + (var(--k-density) - 1) * .5)),1px)` | `round(calc(12px * (1 + (var(--k-density) - 1) * .5)),1px)` | Body size; a 5-step scale derives privately. Derived: `round(calc(12px * (1 + (var(--k-density) - 1) * .5)),1px)`. |
|
|
1580
|
+
| `--k-value-font` | semantic | `var(--k-font)` | `var(--k-font)` | Font for readouts and values (labPreset: mono). Derived: `var(--k-font)`. |
|
|
1581
|
+
|
|
1582
|
+
### Surfaces
|
|
1583
|
+
|
|
1584
|
+
| Token | Tier | Dark | Light | |
|
|
1585
|
+
|---|---|---|---|---|
|
|
1586
|
+
| `--k-bg-page` | semantic | `#0a0a0b` | `#e6e6e8` | The page behind panels; workspace backdrops. |
|
|
1587
|
+
| `--k-bg-inset` | semantic | `#151516` | `#ededef` | Wells sunk into a panel: tracks, inputs, segmented backgrounds. |
|
|
1588
|
+
| `--k-bg` | semantic | `#1c1c1d` | `#f5f5f7` | The panel surface. |
|
|
1589
|
+
| `--k-bg-raised` | semantic | `#232324` | `#ffffff` | Raised within a panel: the active segment, cards on a panel. |
|
|
1590
|
+
| `--k-bg-overlay` | semantic | `#29292a` | `#ffffff` | Floating surfaces: menus, popovers, tooltips. |
|
|
1591
|
+
|
|
1592
|
+
### Fills
|
|
1593
|
+
|
|
1594
|
+
| Token | Tier | Dark | Light | |
|
|
1595
|
+
|---|---|---|---|---|
|
|
1596
|
+
| `--k-fill` | semantic | `#f4f5f90e` | `#1a1a1d0e` | Resting fill for controls. Derived: `color-mix(in oklch,var(--k-fg) calc(5.5% * var(--k-contrast)),transparent)`. |
|
|
1597
|
+
| `--k-fill-hover` | semantic | `#f4f5f914` | `#1a1a1d14` | Hovered fill. Derived: `color-mix(in oklch,var(--k-fg) calc(8% * var(--k-contrast)),transparent)`. |
|
|
1598
|
+
| `--k-fill-selected` | semantic | `#f4f5f91c` | `#1a1a1d1c` | Pressed or selected fill. Derived: `color-mix(in oklch,var(--k-fg) calc(11% * var(--k-contrast)),transparent)`. |
|
|
1599
|
+
|
|
1600
|
+
### Foreground
|
|
1601
|
+
|
|
1602
|
+
| Token | Tier | Dark | Light | |
|
|
1603
|
+
|---|---|---|---|---|
|
|
1604
|
+
| `--k-fg` | semantic | `#f4f5f9` | `#1a1a1d` | Primary text and icons (≥ Lc 90 on bg). |
|
|
1605
|
+
| `--k-fg-2` | semantic | `#d0d0d5` | `#59595d` | Secondary text: labels (≥ Lc 75). |
|
|
1606
|
+
| `--k-fg-3` | semantic | `#9e9ea3` | `#96969b` | Tertiary text: metadata, units, captions (≥ Lc 45). |
|
|
1607
|
+
| `--k-fg-4` | semantic | `#7d7e81` | `#b4b5b9` | Placeholder and disabled text (≥ Lc 30). |
|
|
1608
|
+
|
|
1609
|
+
### Borders
|
|
1610
|
+
|
|
1611
|
+
| Token | Tier | Dark | Light | |
|
|
1612
|
+
|---|---|---|---|---|
|
|
1613
|
+
| `--k-border-subtle` | semantic | `#f4f5f90f` | `#1a1a1d0f` | Hairline edges and dividers (below Lc 15 by design). Derived: `color-mix(in oklch,var(--k-fg) calc(6% * var(--k-contrast)),transparent)`. |
|
|
1614
|
+
| `--k-border` | semantic | `#f4f5f91a` | `#1a1a1d1a` | Default borders. Derived: `color-mix(in oklch,var(--k-fg) calc(10% * var(--k-contrast)),transparent)`. |
|
|
1615
|
+
| `--k-border-strong` | semantic | `#58585c` | `#d3d3d7` | Interactive edges (≥ Lc 15; solid). |
|
|
1616
|
+
|
|
1617
|
+
### Status
|
|
1618
|
+
|
|
1619
|
+
| Token | Tier | Dark | Light | |
|
|
1620
|
+
|---|---|---|---|---|
|
|
1621
|
+
| `--k-ok` | semantic | `#59d38c` | `#0a7e3a` | Success. |
|
|
1622
|
+
| `--k-warn` | semantic | `#f7bc50` | `#b66c00` | Warning. |
|
|
1623
|
+
| `--k-danger` | semantic | `#fa6863` | `#cc2827` | Error, destructive. Put text on danger-soft rather than bare bg. |
|
|
1624
|
+
| `--k-info` | semantic | `#55b8f8` | `#0071b3` | Informational. |
|
|
1625
|
+
| `--k-ok-soft` | semantic | `#59d38c26` | `#0a7e3a26` | Success tint. Derived: `color-mix(in oklch,var(--k-ok) 15%,transparent)`. |
|
|
1626
|
+
| `--k-warn-soft` | semantic | `#f7bc5026` | `#b66c0026` | Warning tint. Derived: `color-mix(in oklch,var(--k-warn) 15%,transparent)`. |
|
|
1627
|
+
| `--k-danger-soft` | semantic | `#fa686326` | `#cc282726` | Danger tint. Derived: `color-mix(in oklch,var(--k-danger) 15%,transparent)`. |
|
|
1628
|
+
| `--k-info-soft` | semantic | `#55b8f826` | `#0071b326` | Info tint. Derived: `color-mix(in oklch,var(--k-info) 15%,transparent)`. |
|
|
1629
|
+
|
|
1630
|
+
### Interaction
|
|
1631
|
+
|
|
1632
|
+
| Token | Tier | Dark | Light | |
|
|
1633
|
+
|---|---|---|---|---|
|
|
1634
|
+
| `--k-focus` | semantic | `#ffffff` | `#1c1c1e` | Focus ring colour (drawn as a 2px bg gap, then 2px of focus). Derived: `var(--k-accent)`. |
|
|
1635
|
+
| `--k-selection` | semantic | `#ffffff3d` | `#1c1c1e3d` | Selected rows and ranges. Derived: `color-mix(in oklch,var(--k-accent) 24%,transparent)`. |
|
|
1636
|
+
| `--k-scrim` | semantic | `#00000099` | `#00000040` | Dimming behind modal layers. |
|
|
1637
|
+
|
|
1638
|
+
### Data
|
|
1639
|
+
|
|
1640
|
+
| Token | Tier | Dark | Light | |
|
|
1641
|
+
|---|---|---|---|---|
|
|
1642
|
+
| `--k-diff-add` | semantic | `#59d38c` | `#0a7e3a` | Added lines; line and word tints derive privately (12% / 38%). Derived: `var(--k-ok)`. |
|
|
1643
|
+
| `--k-diff-del` | semantic | `#fa6863` | `#cc2827` | Deleted lines. Derived: `var(--k-danger)`. |
|
|
1644
|
+
| `--k-diff-mod` | semantic | `#f7bc50` | `#b66c00` | Modified lines. Derived: `var(--k-warn)`. |
|
|
1645
|
+
| `--k-before` | semantic | `#9e9ea3` | `#96969b` | Before in a comparison, and only Before: its label and side of the seam. Neutral unless a theme gives it a colour. Derived: `var(--k-fg-3)`. |
|
|
1646
|
+
| `--k-after` | semantic | `#f4f5f9` | `#1a1a1d` | After in a comparison, and only After. Derived: `var(--k-fg)`. |
|
|
1647
|
+
| `--k-series-1` | semantic | `#6db0f4` | `#2378c2` | Chart series 1. |
|
|
1648
|
+
| `--k-series-2` | semantic | `#66c189` | `#008b4f` | Chart series 2. |
|
|
1649
|
+
| `--k-series-3` | semantic | `#d79f4c` | `#9c6900` | Chart series 3. |
|
|
1650
|
+
| `--k-series-4` | semantic | `#ed8c84` | `#b84d49` | Chart series 4. |
|
|
1651
|
+
| `--k-series-5` | semantic | `#bc97e8` | `#885cb5` | Chart series 5. |
|
|
1652
|
+
| `--k-series-6` | semantic | `#25c2c2` | `#008686` | Chart series 6. |
|
|
1653
|
+
|
|
1654
|
+
### Space
|
|
1655
|
+
|
|
1656
|
+
| Token | Tier | Dark | Light | |
|
|
1657
|
+
|---|---|---|---|---|
|
|
1658
|
+
| `--k-space-inside` | semantic | `var(--k-unit)` | `var(--k-unit)` | Parts of one thing: an icon and its label, a meta line and its title. Derived: `var(--k-unit)`. |
|
|
1659
|
+
| `--k-space-sibling` | semantic | `calc(var(--k-unit) * 2)` | `calc(var(--k-unit) * 2)` | Siblings in a group: buttons in a row, a list item's lines. Derived: `calc(var(--k-unit) * 2)`. |
|
|
1660
|
+
| `--k-space-title` | semantic | `calc(var(--k-unit) * 3)` | `calc(var(--k-unit) * 3)` | A heading and what it heads. Derived: `calc(var(--k-unit) * 3)`. |
|
|
1661
|
+
| `--k-space-group` | semantic | `calc(var(--k-unit) * 12)` | `calc(var(--k-unit) * 12)` | Groups inside a section (prose, video, diff), stacked media, feed items (a hairline in the middle). Derived: `calc(var(--k-unit) * 12)`. |
|
|
1662
|
+
| `--k-space-section` | semantic | `calc(var(--k-unit) * 24)` | `calc(var(--k-unit) * 24)` | Sections of a page (the intro, the updates, each PR), a hairline in the middle. Derived: `calc(var(--k-unit) * 24)`. |
|
|
1663
|
+
| `--k-space-column` | semantic | `calc(var(--k-unit) * 6)` | `calc(var(--k-unit) * 6)` | Columns: a contents rail and the page, media side by side. Derived: `calc(var(--k-unit) * 6)`. |
|
|
1664
|
+
| `--k-pad-page` | semantic | `calc(var(--k-unit) * 6)` | `calc(var(--k-unit) * 6)` | A page's edge; 16px under 640px wide. Derived: `calc(var(--k-unit) * 6)`. |
|
|
1665
|
+
| `--k-pad-card` | semantic | `calc(var(--k-unit) * 3)` | `calc(var(--k-unit) * 3)` | Inside a card or popover. Derived: `calc(var(--k-unit) * 3)`. |
|
|
1666
|
+
|
|
1667
|
+
### Material
|
|
1668
|
+
|
|
1669
|
+
| Token | Tier | Dark | Light | |
|
|
1670
|
+
|---|---|---|---|---|
|
|
1671
|
+
| `--k-shadow-raised` | semantic | `0 1px 2px light-dark(#0000001f,#00000029)` | `0 1px 2px light-dark(#0000001f,#00000029)` | Small lift for raised controls. |
|
|
1672
|
+
| `--k-shadow-overlay` | semantic | `0 12px 36px light-dark(#00000024,#00000085),0 1px 2px light-dark(#00000014,#0000004d)` | `0 12px 36px light-dark(#00000024,#00000085),0 1px 2px light-dark(#00000014,#0000004d)` | Floating panels, menus, popovers. |
|
|
1673
|
+
| `--k-glass` | semantic | `0.6` | `0.6` | Backdrop glass amount (0 = opaque). |
|
|
1674
|
+
| `--k-glow` | semantic | `0.5` | `0.5` | Specular glow amount. |
|
|
1675
|
+
| `--k-rim` | semantic | `#ffffff1a` | `#ffffff99` | Top-edge highlight hairline. |
|
|
1676
|
+
|
|
1677
|
+
### Motion
|
|
1678
|
+
|
|
1679
|
+
| Token | Tier | Dark | Light | |
|
|
1680
|
+
|---|---|---|---|---|
|
|
1681
|
+
| `--k-spring` | semantic | `0.3 0` | `0.3 0` | Default spring as `duration bounce` (the motion module's `default`). |
|
|
1682
|
+
| `--k-hover-in` | semantic | `0` | `0` | Hover wash fade-in, unitless milliseconds. |
|
|
1683
|
+
| `--k-hover-out` | semantic | `120` | `120` | Hover wash fade-out, unitless milliseconds. |
|
|
1684
|
+
| `--k-z` | semantic | `2147480000` | `2147480000` | Base of knobs' stacking ladder; layers add small offsets. |
|
|
1685
|
+
|
|
1686
|
+
### Hooks
|
|
1687
|
+
|
|
1688
|
+
| Token | Tier | Dark | Light | |
|
|
1689
|
+
|---|---|---|---|---|
|
|
1690
|
+
| `--k-slider-track` | hook | `#f4f5f90e` | `#1a1a1d0e` | Slider track. Derived: `var(--k-fill)`. |
|
|
1691
|
+
| `--k-slider-fill` | hook | `#fcfcfd49` | `#1b1b1e49` | Slider value fill. Derived: `color-mix(in oklch,var(--k-accent) 20%,var(--k-fill-selected))`. |
|
|
1692
|
+
| `--k-slider-handle` | hook | `#ffffff` | `#1c1c1e` | Slider handle and range thumbs. Derived: `var(--k-accent)`. |
|
|
1693
|
+
| `--k-toggle-thumb-off` | hook | `#ffffff` | `#ffffff` | A switch's thumb while off, and the ring around colour and gradient thumbs. Derived: `#ffffff`. |
|
|
1694
|
+
| `--k-toggle-track` | hook | `#f4f5f91c` | `#1a1a1d1c` | Toggle track, off. Derived: `var(--k-fill-selected)`. |
|
|
1695
|
+
| `--k-toggle-track-on` | hook | `#ffffff` | `#1c1c1e` | Toggle track, on. Derived: `var(--k-accent)`. |
|
|
1696
|
+
| `--k-toggle-thumb` | hook | `#111111` | `#ffffff` | Toggle thumb. Derived: `var(--k-on-accent)`. |
|
|
1697
|
+
| `--k-segmented-track` | hook | `#151516` | `#ededef` | Segmented control background. Derived: `var(--k-bg-inset)`. |
|
|
1698
|
+
| `--k-segmented-active` | hook | `#232324` | `#ffffff` | Active segment. Derived: `var(--k-bg-raised)`. |
|
|
1699
|
+
| `--k-timeline-playhead` | hook | `#ffffff` | `#1c1c1e` | Timeline playhead. Derived: `var(--k-accent)`. |
|
|
1700
|
+
| `--k-split-gutter` | hook | `#f4f5f90f` | `#1a1a1d0f` | Split pane divider. Derived: `var(--k-border-subtle)`. |
|
|
1701
|
+
| `--k-compare-divider` | hook | `#f4f5f9` | `#1a1a1d` | Before/after divider line. Derived: `var(--k-fg)`. |
|
|
1702
|
+
| `--k-compare-stage` | hook | `#f4f5f90e` | `#1a1a1d0e` | Compare stage background. Derived: `var(--k-fill)`. |
|
|
1703
|
+
| `--k-activity-rail` | hook | `#58585c` | `#d3d3d7` | Activity rail line. Derived: `var(--k-border-strong)`. |
|
|
1704
|
+
| `--k-picker-card` | hook | `#f4f5f90e` | `#1a1a1d0e` | Picker card. Derived: `var(--k-fill)`. |
|
|
1705
|
+
| `--k-pane-bg` | hook | `#1c1c1d` | `#f5f5f7` | knobs/ui pane card. Derived: `var(--k-bg)`. |
|
|
1706
|
+
| `--k-button-bg` | hook | `#f4f5f90e` | `#1a1a1d0e` | Button fill. Derived: `var(--k-fill)`. |
|
|
1707
|
+
| `--k-button-border` | hook | `#00000000` | `#00000000` | Button border. Derived: `transparent`. |
|
|
1708
|
+
| `--k-button-fg` | hook | `#f4f5f9` | `#1a1a1d` | Button text. Derived: `var(--k-fg)`. |
|
|
1709
|
+
| `--k-row-hover` | hook | `#f4f5f907` | `#1a1a1d07` | Row hover wash. Derived: `color-mix(in oklch,var(--k-fill) 50%,transparent)`. |
|
|
1710
|
+
| `--k-group-label` | hook | `#9e9ea3` | `#96969b` | Folder and group headings. Derived: `var(--k-fg-3)`. |
|
|
1711
|
+
| `--k-modified` | hook | `#9e9ea3` | `#96969b` | Marker for values changed from their default. Derived: `var(--k-fg-3)`. |
|
|
1712
|
+
|
|
1713
|
+
### Renamed
|
|
1714
|
+
|
|
1715
|
+
| Old | New |
|
|
1716
|
+
|---|---|
|
|
1717
|
+
| `--ku-accent` | `--k-accent` |
|
|
1718
|
+
| `--ku-radius` | `--k-radius` |
|
|
1719
|
+
| `--ku-font` | `--k-font` |
|
|
1720
|
+
| `--ku-mono` | `--k-mono` |
|
|
1721
|
+
| `--k-page` | `--k-bg-page` |
|
|
1722
|
+
| `--ku-page` | `--k-bg-page` |
|
|
1723
|
+
| `--ku-bg` | `--k-bg` |
|
|
1724
|
+
| `--k-surface` | `--k-fill` |
|
|
1725
|
+
| `--ku-surface` | `--k-fill` |
|
|
1726
|
+
| `--k-surface-hi` | `--k-fill-selected` |
|
|
1727
|
+
| `--ku-surface-hi` | `--k-fill-selected` |
|
|
1728
|
+
| `--k-text` | `--k-fg` |
|
|
1729
|
+
| `--ku-text` | `--k-fg` |
|
|
1730
|
+
| `--k-label` | `--k-fg-2` |
|
|
1731
|
+
| `--ku-label` | `--k-fg-2` |
|
|
1732
|
+
| `--k-dim` | `--k-fg-3` |
|
|
1733
|
+
| `--ku-dim` | `--k-fg-3` |
|
|
1734
|
+
| `--k-edge` | `--k-border-subtle` |
|
|
1735
|
+
| `--k-line` | `--k-border` |
|
|
1736
|
+
| `--ku-line` | `--k-border` |
|
|
1737
|
+
| `--k-line-hi` | `--k-border-strong` |
|
|
1738
|
+
| `--ku-line-hi` | `--k-border-strong` |
|
|
1739
|
+
| `--ku-warn` | `--k-warn` |
|
|
1740
|
+
| `--k-critical` | `--k-danger` |
|
|
1741
|
+
| `--ku-critical` | `--k-danger` |
|
|
1742
|
+
| `--k-ui-select` | `--k-selection` |
|
|
1743
|
+
| `--ku-select` | `--k-selection` |
|
|
1744
|
+
| `--k-panel-radius` | `--k-radius-panel` |
|
|
1745
|
+
| `--ku-panel-radius` | `--k-radius-panel` |
|
|
1746
|
+
| `--ku-row` | `--k-row` |
|
|
1747
|
+
| `--ku-font-size` | `--k-font-size` |
|
|
1748
|
+
| `--ku-value-font` | `--k-value-font` |
|
|
1749
|
+
| `--k-lift-shadow` | `--k-shadow-raised` |
|
|
1750
|
+
| `--k-shadow` | `--k-shadow-overlay` |
|
|
1751
|
+
| `--ku-shadow` | `--k-shadow-overlay` |
|
|
1752
|
+
| `--ku-rim` | `--k-rim` |
|
|
1753
|
+
| `--ku-hover-in` | `--k-hover-in` |
|
|
1754
|
+
| `--ku-hover-out` | `--k-hover-out` |
|
|
1755
|
+
| `--k-fill-rest` | `--k-slider-fill` |
|
|
1756
|
+
| `--k-handle` | `--k-slider-handle` |
|
|
1757
|
+
| `--k-thumb` | `--k-toggle-thumb-off` |
|
|
1758
|
+
| `--k-seg-bg` | `--k-segmented-track` |
|
|
1759
|
+
| `--ku-seg-bg` | `--k-segmented-track` |
|
|
1760
|
+
| `--k-seg-active` | `--k-segmented-active` |
|
|
1761
|
+
| `--k-ui-divider` | `--k-split-gutter` |
|
|
1762
|
+
| `--ku-divider` | `--k-split-gutter` |
|
|
1763
|
+
| `--k-compare-line` | `--k-compare-divider` |
|
|
1764
|
+
| `--k-activity-line` | `--k-activity-rail` |
|
|
1765
|
+
| `--k-ui-pane` | `--k-pane-bg` |
|
|
1766
|
+
| `--ku-pane-bg` | `--k-pane-bg` |
|
|
1767
|
+
| `--k-button-line` | `--k-button-border` |
|
|
1768
|
+
| `--k-button-color` | `--k-button-fg` |
|
|
1769
|
+
| `--k-group-ink` | `--k-group-label` |
|
|
1770
|
+
| `--k-group-color` | `--k-group-label` |
|
|
1771
|
+
| `--k-tray-pad` | `--_k-tray-pad` |
|
|
1772
|
+
| `--k-pad` | `--_k-pad` |
|
|
1773
|
+
| `--k-inset` | `--_k-inset` |
|
|
1774
|
+
| `--ku-inset` | `--_k-inset` |
|
|
1775
|
+
| `--k-gap` | `--_k-gap` |
|
|
1776
|
+
| `--k-radius-md` | `--_k-radius-md` |
|
|
1777
|
+
| `--k-radius-sm` | `--_k-radius-sm` |
|
|
1778
|
+
| `--k-control-h` | `--_k-control-h` |
|
|
1779
|
+
| `--ku-control-h` | `--_k-control-h` |
|
|
1780
|
+
| `--k-value-size` | `--_k-value-size` |
|
|
1781
|
+
| `--ku-value-size` | `--_k-value-size` |
|
|
1782
|
+
| `--k-text-title` | `--_k-text-title` |
|
|
1783
|
+
| `--k-text-body` | `--_k-text-body` |
|
|
1784
|
+
| `--k-text-control` | `--_k-text-control` |
|
|
1785
|
+
| `--k-text-meta` | `--_k-text-meta` |
|
|
1786
|
+
| `--ku-text-meta` | `--_k-text-meta` |
|
|
1787
|
+
| `--k-text-cap` | `--_k-text-cap` |
|
|
1788
|
+
| `--k-font-weight` | `--_k-weight` |
|
|
1789
|
+
| `--ku-font-weight` | `--_k-weight` |
|
|
1790
|
+
| `--k-label-weight` | `--_k-weight-label` |
|
|
1791
|
+
| `--ku-label-weight` | `--_k-weight-label` |
|
|
1792
|
+
| `--k-title-weight` | `--_k-weight-title` |
|
|
1793
|
+
| `--ku-title-weight` | `--_k-weight-title` |
|
|
1794
|
+
|
|
1795
|
+
<!-- tokens:end -->
|
|
1796
|
+
|
|
1403
1797
|
## Show, hide and unlock
|
|
1404
1798
|
|
|
1405
1799
|
**Backtick.** `` ` `` shows and hides every panel and chip on the page at once, the way a page's
|
|
@@ -1558,7 +1952,7 @@ The package has two core exports selected through the standard `development` con
|
|
|
1558
1952
|
| --- | --- |
|
|
1559
1953
|
| `split(container, { panes, direction?, id?, storage?, onLayout?, preset?, theme? })` | Resizable panes and docks that collapse to rails or headers; layouts saved by `id`. |
|
|
1560
1954
|
| `tree(container, { items, onSelect?, onMove?, onRename?, toggles?, filter?, typeahead?, … })` | A virtualised outliner with keyboard moves, drag reorder and rename; `typeahead: false \| (key) => boolean` leaves letters to the page. |
|
|
1561
|
-
| `codeDiff(container, { patch? \| files?, file?, layout?, wrap?, height?, onFile?, onLines?, … })` | A code diff
|
|
1955
|
+
| `codeDiff(container, { patch? \| files?, file?, layout?, wrap?, height?, onFile?, onLines?, … })` | A code diff, no card (needs `@pierre/diffs`); files and layouts fade in place. |
|
|
1562
1956
|
| `diffReview(container, { patch? \| files?, id?, … })` | The changed files (a filter, viewed ticks) in a tree beside the diff, in a split. |
|
|
1563
1957
|
| `readout(field, { preset?, theme?, label? })` | A readout on its own: `{ element, push(value), value, dispose() }`. |
|
|
1564
1958
|
| `popover(anchor, content, { placement?, offset?, role?, label?, focus?, trap?, closeOnOutside?, onClose? })` | A floating layer that flips and shifts; `{ element, open, reposition(), close() }`. |
|
|
@@ -1568,11 +1962,14 @@ The package has two core exports selected through the standard `development` con
|
|
|
1568
1962
|
| `toast(message, { tone?, linger?, action?, within? })` | A status pill at the bottom centre. |
|
|
1569
1963
|
| `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. |
|
|
1570
1964
|
| `timeline(container, { duration, time?, fps?, units?, markers?, ranges?, snap?, onSeek?, … })` | A zoomable ruler and playhead with markers, loop ranges and playback. |
|
|
1571
|
-
| `compare(container, { before, after, mode?, wipe?, timeline?, onMode?, onWipe?, onView?, … })` / `compareStrip(container, { before, after, timeline })` | A before/after viewer for videos and images (a split screen zoomed together, wipe
|
|
1965
|
+
| `compare(container, { before, after, mode?, modes?, wipe?, timeline?, onMode?, onWipe?, onView?, … })` / `compareStrip(container, { before, after, timeline })` | A before/after viewer for videos and images (a split screen zoomed together, or a wipe; a difference opted in) on one clock, with a loop; a frame strip with difference bars, on its own. |
|
|
1966
|
+
| `selectionPill(container, { selected, rows?, radius?, copy?, color?, spring? })` | The one highlight that travels between rows: a fill on the state spring (or `spring`; the walkthrough's chapters and rail use `SPRINGS.snappy`) taking each row's box and radius, with a bright copy of the rows clipped to it; `update({ snap? })`, `dispose()`. |
|
|
1967
|
+
| `icon(name, { small? })` / `ICON_NAMES` | Phosphor regular glyphs as SVG markup in `currentColor`, 16 px (12 px `small`). |
|
|
1572
1968
|
| `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 }`. |
|
|
1573
1969
|
| `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. |
|
|
1574
1970
|
| `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. |
|
|
1575
1971
|
| `planBento(aspects, { width, height, gap?, tolerance?, rowHeight?, groups?, hero? })` | The bento's layout on its own: rects, mode, coverage and crop. |
|
|
1972
|
+
| `walkthrough(container, { doc, onFeedback?, storage?, keys?, rail?, advance? })` / `mediaSlot(container, { media, steps? })` | The review page an agent writes (sections, steps, media from compares to histories, stacks and tables, notes and picks handed back); one step's media on its own. |
|
|
1576
1973
|
| `overlayLook({ preset?, theme? })` | The look every overlay takes. |
|
|
1577
1974
|
| `hints.register(root, () => targets, { listen?, rank? })` / `data-hint` | Joins the page's hint mode (`s`); `hints.start()`, `hints.stop()`. |
|
|
1578
1975
|
| `restyle(target, { preset?, theme?, density? })` | Re-themes the pieces and panels already on screen, live; on a document, later overlays too. |
|