@kitlangton/knobs 0.4.0-next.7 → 0.4.0-next.9

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 CHANGED
@@ -1213,9 +1213,9 @@ compare(viewer, { before, after, timeline: clock })
1213
1213
  compareStrip(strip, { before, after, timeline: clock })
1214
1214
  ```
1215
1215
 
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 glass pill 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 (`Space` too, focused); a click on the stage never plays. `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 raised 16×28 grip 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 grip 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.
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 glass pill 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 raised 16×28 grip 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 grip 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
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, or `Space` once it's focused (a click on the picture focuses it), plays and pauses; a click on the picture never plays; 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.
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.
1219
1219
 
1220
1220
  ### Activity
1221
1221
 
@@ -1335,10 +1335,14 @@ Click or `Enter` zooms a tile to fill the view, aspect-fit and centred: one cont
1335
1335
 
1336
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).
1337
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
+
1338
1342
  ### Walkthrough
1339
1343
 
1340
1344
  ```ts
1341
- import { walkthrough, type WalkthroughDoc } from "knobs/ui"
1345
+ import { walkthrough, type WalkthroughDoc } from "knobs/review"
1342
1346
 
1343
1347
  const doc: WalkthroughDoc = {
1344
1348
  id: "2026-10-19-onboarding-prs",
@@ -1370,7 +1374,7 @@ Media is a bare URL (a video or picture), or `compare`, `video`, `image`, `diff`
1370
1374
  "more": "A mock provider answers 410 Gone.\n\n**Pick another model** opens the picker." }
1371
1375
  ```
1372
1376
 
1373
- **Pull requests** read as small chips: an icon for their state, the number, the title and state on hover, in a quiet tint (open green, merged violet, draft and closed neutral). A section's PR `link` shows as its chip on the meta line ("PR 3 of 9 · ⟨#57158⟩"), and in titles, captions, summaries and `more`, a link to a pull request whose words are its number, and a bare `#57148` the page can resolve, become chips too. The doc's `prs` gives their state and title by number (`{ "57148": { state: "merged", title: "…" } }`), and `repo: "owner/name"` links bare numbers it doesn't list. So a caption needn't spell out "#57158 (Draft · Green)".
1377
+ **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)".
1374
1378
 
1375
1379
  **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.
1376
1380
 
@@ -1416,6 +1420,8 @@ The demo is `examples/walkthrough.html`.
1416
1420
 
1417
1421
  **`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 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.
1418
1422
 
1423
+ 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.
1424
+
1419
1425
  **`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.
1420
1426
 
1421
1427
  ```json
@@ -1937,7 +1943,7 @@ The package has two core exports selected through the standard `development` con
1937
1943
  | `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. |
1938
1944
  | `timeline(container, { duration, time?, fps?, units?, markers?, ranges?, snap?, onSeek?, … })` | A zoomable ruler and playhead with markers, loop ranges and playback. |
1939
1945
  | `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. |
1940
- | `selectionPill(container, { selected, rows?, radius?, copy?, color? })` | The one highlight that travels between rows: a fill on the state spring taking each row's box and radius, with a bright copy of the rows clipped to it; `update({ snap? })`, `dispose()`. |
1946
+ | `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()`. |
1941
1947
  | `icon(name, { small? })` / `ICON_NAMES` | Phosphor regular glyphs as SVG markup in `currentColor`, 16 px (12 px `small`). |
1942
1948
  | `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 }`. |
1943
1949
  | `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. |