@kitlangton/knobs 0.4.0-next.11 → 0.4.0-next.12

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
@@ -202,10 +202,10 @@ keeps a gradient's stop count and walks its hue along a lightness ramp. On a cur
202
202
  a built-in shape and jitters it. Mutate nudges colours as it does a `color` and moves stops and
203
203
  points without letting them pass each other.
204
204
 
205
- ### Inspector fields: vectors, ranges, angles, seeds, key combinations and toolbars
205
+ ### Inspector fields: vectors, ranges, angles, seeds, key combinations, text and toolbars
206
206
 
207
207
  ```ts
208
- import { action, angle, defineControls, keybind, range, seed, toolbar, vec2, vec3 } from "knobs"
208
+ import { action, angle, defineControls, keybind, range, seed, text, toolbar, vec2, vec3 } from "knobs"
209
209
 
210
210
  const node = defineControls("node", {
211
211
  replay: action("Replay", { run: replay, shortcut: "Shift+K", global: true, rebind: true }),
@@ -222,6 +222,8 @@ const node = defineControls("node", {
222
222
  yaw: angle(90), // degrees
223
223
  seed: seed(1234), // a whole number
224
224
  fire: keybind("Space"), // "Meta+Shift+K", "" for none
225
+ name: text("Lamp", { placeholder: "Untitled" }), // one line
226
+ notes: text("", { multiline: true }), // lines in a box
225
227
  }, { onAction: (id) => run(id) })
226
228
  ```
227
229
 
@@ -246,7 +248,12 @@ const node = defineControls("node", {
246
248
  Esc cancels and ⌫ clears. Not rolled by Random or Mutate. Letters follow the user's layout
247
249
  (Dvorak's K records and matches as K), with ⌥ combinations read through the browser's layout
248
250
  map; `comboFromEvent`, `matchesCombo` and `letterOf` do the same for a host's own keys.
249
- - **`toolbar(items)`**: a row of buttons with an icon (`play`, `pause`, `frame`,
251
+ - **`text(value?, { multiline?, placeholder? })`**: words typed into a field beside the label, or
252
+ with `multiline` a box under it that grows with its lines (up to eight, then it scrolls). Each
253
+ keystroke sets the value and one focus's typing is one undo step; Enter (`⌘Enter` in a box) or
254
+ Esc lets go. A one-line field turns a line break into a space. Presets, copy and remote
255
+ descriptors carry it like any string; Random and Mutate leave it alone.
256
+ - **`toolbar(items, { labels? })`**: a row of buttons with an icon (`play`, `pause`, `frame`,
250
257
  `retrigger`, `clear`, `target`, `camera`, `sparkle`, … or SVG path data on a 256 grid), a label
251
258
  and a shortcut chip. The buttons share equal columns, as many as fit, balanced across the rows
252
259
  (four that don't fit in one row wrap as two and two), each chip at its button's end. A press calls the item's `run` and `onAction(id)`; `controls.action(key, id)`
@@ -255,7 +262,8 @@ const node = defineControls("node", {
255
262
  `shortcut: { key, scope }` says where one works: `"panel"` (the default), `"page"` (as `global`)
256
263
  or `"none"`, a chip that only shows a key the host binds itself (a canvas's `F`), so the panel's
257
264
  own `f` and `g` still work beside it. The row holds no value; ←/→ move along it and Enter or
258
- Space presses.
265
+ Space presses. When every button has an icon and the labels don't fit on one row, it's one row
266
+ of icons, each label and shortcut in its tooltip; `labels: false` always shows icons only.
259
267
  - **`action(label, { run, shortcut?, global?, rebind? })`**: a button row, whose `shortcut` (a
260
268
  combination such as `"Shift+K"`, or `{ key, scope }`) presses it under the same rules as a
261
269
  toolbar button's: while the panel has focus, or anywhere on the page with `global: true` (not
@@ -268,9 +276,9 @@ const node = defineControls("node", {
268
276
  button's too (by its id), which has no cap of its own to rebind it from.
269
277
 
270
278
  Remote descriptors use the same names: `kind: "vec2" | "vec3" | "range" | "angle" | "seed" |
271
- "keybind"` with `default`, `min`, `max`, `step`, `unit`, `decimals`, `link`, `linked` and `wrap`, and
272
- `{ kind: "toolbar", items: [{ key, label, icon?, shortcut?, global?, disabled? }] }`; vectors take
273
- `axes`. Each kind takes
279
+ "keybind" | "text"` with `default`, `min`, `max`, `step`, `unit`, `decimals`, `link`, `linked` and `wrap`, and
280
+ `{ kind: "toolbar", items: [{ key, label, icon?, shortcut?, global?, disabled? }], labels? }`; vectors take
281
+ `axes`, text `multiline` and `placeholder`. Each kind takes
274
282
  `disabled`, `readOnly` and mixed values like the rest ("—" in place of the value).
275
283
 
276
284
  Spring, point, easing, gradient and curve values compare by their contents everywhere (`isModified`, presets, undo, variants), and unchanged ones keep their identity across updates so memoized React children skip them. Random values stay in range people use (springs 0.2–0.9 s with bounce up to 0.45, mid-light colours of moderate chroma, any point, a named curve). Switches sit out of Random and Mutate unless a field says `random: true`: they usually show or hide things (a view, a scene's part), and a roll that hides the page isn't a new take on it.
@@ -317,6 +325,38 @@ live.watch("events", { level: "warn", text: "Gutter drip over budget" })
317
325
 
318
326
  Readouts cost nothing at rest. They draw on one shared clock, only while on screen and only when something arrived, so a paused feed lets the page, and the GPU overlay, sleep. Graphics are canvases redrawn from a list of drawing ops (the GPU overlay draws the same ops). Text changes at most `hz` times a second (4 by default), in boxes that only grow, so nothing reflows as numbers change. Scales snap to round steps (1, 2 or 5 × 10ⁿ) and ease there on a spring, in log space, so a spike rescales smoothly. The store keeps each readout's history, so a panel mounted later starts with it. On their own, outside a panel: `readout(sparkline({ unit: "ms" }), { preset })` from `knobs/ui` returns `{ element, push(value), value, dispose() }`.
319
327
 
328
+ ## Versions of a design: variants in the strip
329
+
330
+ When a page has several versions of one design (nine hero animations, three card layouts), the
331
+ preset strip at the top of the panel is where you switch them. Each version is a preset. Don't
332
+ add a `variant` choice beside it: two ways to switch the same thing is one too many.
333
+
334
+ When the page's code branches on which version is on, give that choice `variants: true`:
335
+
336
+ ```ts
337
+ const controls = defineControls("upsell", {
338
+ variant: choice("Stand up", ["Stand up", "Anamorphic", "Roll", "Trace"], { variants: true }),
339
+ speed: knob(1, { min: 0.25, max: 2, step: 0.05 }),
340
+ }, {
341
+ presets: { Roll: { speed: 0.8 } }, // optional: values a variant changes beside itself
342
+ })
343
+
344
+ render(controls.get().variant) // "Stand up", until another chip is picked
345
+ ```
346
+
347
+ - **Each option is a chip in the strip.** The default option is the `default` preset, named after
348
+ it; the others follow in order, then any other `presets`. Their ids are the options, and
349
+ `labels` name the chips.
350
+ - **The choice has no row.** The strip is the one place to switch, with a click, a sideways pan,
351
+ `[` / `]`, or `1`–`9` from anywhere in the panel but a number row.
352
+ - **Random leaves it be** (`random: false` unless you say otherwise). Random explores around the
353
+ active variant, and Reset goes back to it, as with any preset.
354
+ - **`presets` keyed by an option add to that variant.** The default variant's values are the
355
+ schema's defaults, so it takes none there.
356
+
357
+ Versions that differ only in their values need no choice at all: they're plain `presets`, with
358
+ `defaultLabel` naming the first.
359
+
320
360
  ## Presets: names and the base
321
361
 
322
362
  The strip holds the schema's `presets` (and `default`) beside the variants you save. Saving (`+` or `⇧S`) puts a field where the new chip goes, its name ("Variant 3") selected so typing replaces it; `Enter` keeps what you typed, `Esc` keeps "Variant 3", and the field opens out to the chip's width without moving anything else. `F2` or `Enter` on the strip, or a double-click, renames a saved one the same way (`controls.variants.rename(id, name)`); names are kept with the variants in the store's storage. The schema's presets keep their names.
@@ -344,7 +384,7 @@ Panels and switchers are keyboard-first:
344
384
  | Arrows on a pad or curve handle | Move the puck or handle (`⇧` ×10, `⌥` finest); `j` / `k` or `Tab` move on |
345
385
  | On a gradient strip or curve graph | Arrows move the selected stop or point; `Tab` / `⇧Tab` and `[` / `]` pick another; `n` adds one, `Delete` removes it; `Enter` edits a stop's colour (`Esc` returns) |
346
386
  | `[` / `]` | Cycle presets and saved variants from anywhere in the panel; with no preset strip, cycle tabs |
347
- | `1`–`9` | Pick the focused row's Nth option: a choice (segmented or menu) or a curve's, easing's or gradient's presets. Tiny digit hints show on the options once focus settles. A row with options keeps the digits; elsewhere they do what they did before (inline number edits, a page's group presets) |
387
+ | `1`–`9` | Pick the focused row's Nth option: a choice (segmented or menu) or a curve's, easing's or gradient's presets. Anywhere else in the panel, pick the preset strip's Nth chip (a design's variants). While you use keys, tiny digit hints show on the options once focus settles, and on the strip's chips while focus is on the strip or the panel. A number row keeps its digits for inline edits, and a page's card for its group presets |
348
388
  | `⌥1`–`⌥9` | Show tab 1–9 |
349
389
  | `/` | Focus the search field (`Esc` clears it, then hands focus back; `↓` or `Enter` go to the first row left) |
350
390
  | `Home` / `End` | Jump slider to `min` or `max` |
@@ -395,8 +435,10 @@ Three things show which, in both renderers, on floating and inline panels and on
395
435
  Cohesion`, the deepest crumb bright and its parents dim. Crumbs glide to their places as focus
396
436
  moves, and one that changes fades out as the new one fades in. For a moment after a key below
397
437
  the panel's level, a faint `esc ↑ Shape` follows it, then fades. Clicking a crumb takes focus to
398
- its level; clicking the title takes it to the panel. When it doesn't fit, the first crumbs fold
399
- into `…`. `crumbs: false` on the mount drops the line (the lens and the armed frame stay).
438
+ its level; clicking the title takes it to the panel. The line lives in the room between the
439
+ title and the header's buttons: when it doesn't fit, the first crumbs drop off, and without room
440
+ for the deepest it stays empty, so it never runs under the buttons and the title never moves.
441
+ `crumbs: false` on the mount drops the line (the lens and the armed frame stay).
400
442
 
401
443
  Tune them with tokens on the panel or any ancestor:
402
444
 
@@ -709,6 +751,18 @@ mountPanel(station, column, {
709
751
  the close X): a list picks which show, not their order. A button left out takes no slot; Undo,
710
752
  Redo and Reset keep theirs while they're away (`data-shown="false"`, `disabled`, out of the
711
753
  tab order and the accessibility tree).
754
+ - **On a surface.** An inline panel in a split's pane (or inside anything with the class
755
+ `knobs-surface`) is one surface with it: no card, padding or background of its own, its groups
756
+ plain sections on hairlines, its rows and preset chips keeping their inset so their text lines
757
+ up with the pane's. Without a title, its header buttons join the pane's header (left of the
758
+ collapse button) and it draws no header band; elsewhere an untitled header shows only while one
759
+ of its buttons does.
760
+ - **Presets as a list.** `presetDisplay: "auto"` (the default) shows the preset strip's chips
761
+ while they fit on two lines and a list once they'd take more (many presets, or long names):
762
+ left-aligned rows, like a menu's, with the selection gliding between them. `"list"` and
763
+ `"chips"` pick one.
764
+ - **Empty.** `empty: "This story has no controls"` shows that line, quietly, while the panel has
765
+ no rows. Without it an empty panel shows nothing.
712
766
  - **Height-bounded panels.** The header (title and buttons) stays pinned while the rows scroll
713
767
  under it; the preset strip, tabs and search scroll with the rows. Once rows are under it, and
714
768
  only then, it frosts and softens its foot with a short fade, scroll-driven with no script, in
@@ -778,6 +832,9 @@ panel.tab("emitter") // shows a tab; panel.tab() answers the one showing
778
832
  groups included, as they cover every tab; give a field `random: false` to keep it out of
779
833
  Random. Keyboard order and search follow the panel top to bottom: pinned rows, the tabs, the
780
834
  preset strip, then the tab's rows. `controls.isPinned(key)` says whether a key is pinned.
835
+ A pinned `variants: true` choice is an ordinary choice with a row: pinned wins, and its options
836
+ make no presets. A tab whose rows no preset moves shows no preset strip; a variants choice moves
837
+ every tab (or its own, if it names one).
781
838
 
782
839
  ```ts
783
840
  const motion = defineControls(
@@ -1361,6 +1418,23 @@ Click or `Enter` zooms a tile to fill the view, aspect-fit and centred: one cont
1361
1418
 
1362
1419
  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).
1363
1420
 
1421
+ ### Infinite canvas
1422
+
1423
+ ```ts
1424
+ import { canvas } from "knobs/ui"
1425
+
1426
+ const board = canvas(container, {
1427
+ items: stories.map((s) => ({ id: s.id, title: s.name, render: (el) => s.mount(el) })),
1428
+ })
1429
+ board.focus("button-primary")
1430
+ ```
1431
+
1432
+ **`canvas(container, options)`** puts items on a dotted page that pans and zooms freely, as a components lab's grid. Items take `id`, `render(el)` (return a cleanup), `title` (shown above the card), `w`/`h` (the card's size; without them it takes its content's) and `x`/`y` (a place; items without one pack into rows under the placed ones, in order, the rows as wide as makes the set about the canvas's shape, or `width`). Each item is one hairline card; the dots scale with the zoom and fade out at the extremes; the edges fade over 16px (`--k-ui-canvas-fade`).
1433
+
1434
+ Dragging the page, two-finger scrolling, or holding Space or the middle button and dragging anywhere pans; a pinch or `⌘`/`Ctrl`-scroll zooms around the pointer. Both follow the hand 1:1 and a fling coasts. What the hand didn't do springs (no bounce): `0` fits, `1` is 100%, `=`/`-` step 1.5×, and a double-click on an item focuses it (again, or on the page, fits). A mouse wheel's notch under `⌘` zooms directly too, at most 1.25× an event. Each jump zooms around the one point both views share, so everything on screen travels in a straight line. Items stay live: clicks and keys reach them, and a dragged Space still presses a focused button if nothing moved. Nothing moves on load: the first fit is instant, and until someone touches the view a change in size or content fits again. The world is one transformed layer; at rest its translate snaps to device pixels and, after a zoom, it's drawn again at the new scale so text is crisp.
1435
+
1436
+ The handle has `fit()`, `focus(id)` (at most 200%), `zoomTo(scale, at?)`, `update(items)` (items that stay keep their content and glide to new places; new ones come in, gone ones soften out), `view`, `rects`, `moving`, `stats()` and `dispose()`; `onView` reports the view, `minZoom`/`maxZoom` (.05–8) bound it, `gap` (48) parts packed items, `padding` (48) frames a fit and `readout: false` drops the zoom pill. `layoutCanvas`, `packRows`, `fitView`, `zoomAround`, `viewPath` and `dotGrid` are the math on its own. `Canvas` in `knobs/ui/react` and `knobs/ui/solid` follows `items`. The demo is `examples/canvas.html` (`?n=200`, `?perf`).
1437
+
1364
1438
  ### Review pieces (`knobs/review`)
1365
1439
 
1366
1440
  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.
@@ -1908,7 +1982,7 @@ The package has two core exports selected through the standard `development` con
1908
1982
  | Function | Behavior |
1909
1983
  | --- | --- |
1910
1984
  | `knob(value, { min, max, step?, unit?, decimals?, detents?, shortcut?, label?, group?, groupLabel?, section?, sectionLabel? })` | Numeric slider control with rolling readout and inline editor. `detents: false` lets drags glide through the default, the base and the middle. |
1911
- | `choice(value, ["a", "b"], { label?, group?, shortcut? }?)` | Nonempty string choices with literal union inference. |
1985
+ | `choice(value, ["a", "b"], { label?, group?, shortcut?, labels?, display?, variants? }?)` | Nonempty string choices with literal union inference. `variants: true` makes the options the preset strip's chips, with no row (see [Versions of a design](#versions-of-a-design-variants-in-the-strip)). |
1912
1986
  | `toggle(value, { label?, group?, shortcut?, random? }?)` | Boolean switch control. Out of Random and Mutate unless `random: true`. |
1913
1987
  | `spring({ duration?, bounce? }?, meta?)` | SwiftUI-style spring: two sliders beside its live curve. |
1914
1988
  | `color("#hex" \| "oklch(L C H)" \| { l, c, h }, meta?)` | OKLCH colour kept inside sRGB, as an `oklch()` string; swatch, hex and hue/chroma/lightness strips. |
@@ -1922,7 +1996,8 @@ The package has two core exports selected through the standard `development` con
1922
1996
  | `range([low, high], { min, max, step?, unit? })` | One slider with two thumbs. |
1923
1997
  | `angle(degrees, { min?, max?, step?, wrap? })` | A dial, 0° up and clockwise; 0–360 wrapping by default. |
1924
1998
  | `seed(value, { min?, max? })` / `keybind(combo?)` | A whole number with a die; a captured key combination. Both stay out of Random and Mutate by default. |
1925
- | `toolbar([{ id?, label, icon?, shortcut?, global?, disabled?, run? }])` | A row of buttons with shortcut chips; presses call `onAction(id)`. `shortcut` is a key, or `{ key, scope: "page" \| "panel" \| "none" }`; `toolbarShortcut(item)` reads either. |
1999
+ | `text(value?, { multiline?, placeholder? })` | Words in a field, or lines in a box that grows with them. Out of Random and Mutate. |
2000
+ | `toolbar([{ id?, label, icon?, shortcut?, global?, disabled?, run? }], { labels? })` | A row of buttons with shortcut chips; presses call `onAction(id)`. `shortcut` is a key, or `{ key, scope: "page" \| "panel" \| "none" }`; `toolbarShortcut(item)` reads either. |
1926
2001
  | `watch` / `sparkline` / `graph` / `meter` / `table` / `log` (`{ label?, unit?, decimals?, format?, hz?, get?, … }`) | Readout rows that hold no value; push with `controls.watch(key, value)`. See [Readouts](#readouts-live-values-sparklines-graphs-meters-tables-and-logs). |
1927
2002
  | `springCss(spring)` / `springMotion(spring)` / `bezierCss(curve)` / `colorHex(color)` | Turn values into CSS, ω/ζ, or hex. |
1928
2003
  | `fromDescriptors(descriptors)` | Converts runtime `ControlDescriptor[]` into a `Schema`. |
@@ -1995,6 +2070,7 @@ The package has two core exports selected through the standard `development` con
1995
2070
  | `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. |
1996
2071
  | `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. |
1997
2072
  | `planBento(aspects, { width, height, gap?, tolerance?, rowHeight?, groups?, hero? })` | The bento's layout on its own: rects, mode, coverage and crop. |
2073
+ | `canvas(container, { items, gap?, padding?, width?, minZoom?, maxZoom?, readout?, onView? })` | An infinite canvas: items as cards on a dotted page that pans and zooms freely; `fit`, `focus(id)`, `zoomTo`. |
1998
2074
  | `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. |
1999
2075
  | `overlayLook({ preset?, theme? })` | The look every overlay takes. |
2000
2076
  | `hints.register(root, () => targets, { listen?, rank? })` / `data-hint` | Joins the page's hint mode (`s`); `hints.start()`, `hints.stop()`. |
@@ -1,24 +1,3 @@
1
- var __chunkGraphs, __chunkSeen, __chunkNonce, __preload = (id, seenOnly) => {
2
- for (var [base, graph, ids] of __chunkGraphs || [])
3
- for (var stack = [id], g = globalThis, d = g.document, head, j, node, k, link;j = stack.pop(); )
4
- if (!__chunkSeen[j] && (node = graph[j])) {
5
- __chunkSeen[j] = 1;
6
- for (k = 1;k < node.length; k++)
7
- stack.push(ids[node[k]]);
8
- if (!seenOnly && j !== id && (head = d && d.head)) {
9
- if (__chunkNonce === void 0)
10
- __chunkNonce = (k = d.querySelector("meta[property=csp-nonce]")) && (k.nonce || k.getAttribute("nonce")) || "";
11
- if (link = d.createElement("link"), link.rel = "modulepreload", link.crossOrigin = "", __chunkNonce)
12
- link.nonce = __chunkNonce;
13
- link.href = new g.URL(node[0], base), head.appendChild(link);
14
- }
15
- }
16
- }, __chunks = (base, ids, nodes, entry) => {
17
- for (var graph = {}, i = 0;i < ids.length; i++)
18
- graph[ids[i]] = nodes[i];
19
- (__chunkGraphs ||= []).push([base, graph, ids]), __chunkSeen ||= {}, __preload(ids[entry], 1);
20
- };
21
-
22
1
  // src/core/color.ts
23
2
  var round = (n, places) => Number(n.toFixed(places)), clamp01 = (n) => Math.min(1, Math.max(0, n)), gamma = (c) => c <= 0.0031308 ? 12.92 * c : 1.055 * c ** 0.4166666666666667 - 0.055, linear = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
24
3
  function oklchToLinear({ l, c, h }) {
@@ -529,6 +508,16 @@ function pickMeta(source, skip = []) {
529
508
  return out;
530
509
  }
531
510
  var READOUT_KINDS = /* @__PURE__ */ new Set(["watch", "sparkline", "graph", "meter", "table", "log"]), isReadout2 = (field) => READOUT_KINDS.has(field.kind), hasValue2 = (field) => field.kind !== "action" && field.kind !== "toolbar" && !READOUT_KINDS.has(field.kind);
511
+ function variantsOf(schema, isPinned = () => !1) {
512
+ let found;
513
+ for (let [key, field] of Object.entries(schema))
514
+ if (field.kind === "choice" && field.variants && !isPinned(key)) {
515
+ if (found)
516
+ throw Error(`Only one choice can hold the variants (${found.key}, ${key}).`);
517
+ found = { key, field };
518
+ }
519
+ return found;
520
+ }
532
521
  function pinnedKeys(schema, groups = []) {
533
522
  let byGroup = new Map(groups.map((g) => [g, !0]));
534
523
  for (let f of Object.values(schema))
@@ -571,8 +560,9 @@ function choice2(value, options, meta) {
571
560
  value,
572
561
  options: Object.freeze([...options]),
573
562
  ...labels && Object.keys(labels).length ? { labels } : {},
574
- ...meta?.display ? { display: meta.display } : {}
575
- }, meta);
563
+ ...meta?.display ? { display: meta.display } : {},
564
+ ...meta?.variants ? { variants: !0 } : {}
565
+ }, meta?.variants ? { ...meta, random: meta.random ?? !1 } : meta);
576
566
  }
577
567
  function action2(label, meta) {
578
568
  let { shortcut, global, rebind, run, ...rest } = meta ?? {}, key = shortcutOf({ ...shortcut ? { shortcut } : {}, ...global ? { global } : {} }), combo = key ? normalizeCombo2(key.key) : "";
@@ -717,6 +707,17 @@ function keybind2(value = "", meta) {
717
707
  let field = { kind: "keybind", value: normalizeCombo2(value) };
718
708
  return withMeta(field, { random: !1, ...meta });
719
709
  }
710
+ function text2(value = "", options) {
711
+ let { multiline, placeholder, ...meta } = options ?? {}, field = {
712
+ kind: "text",
713
+ value: "",
714
+ ...multiline ? { multiline: !0 } : {},
715
+ ...placeholder ? { placeholder } : {}
716
+ };
717
+ return withMeta({ ...field, value: normalizeText(field, value) }, { random: !1, ...meta });
718
+ }
719
+ var normalizeText = (field, input) => field.multiline ? input.replace(/\r\n?/g, `
720
+ `) : input.replace(/\r\n?|\n/g, " ");
720
721
  function gradient2(stops, options = {}) {
721
722
  let value = normalizeStops(stops);
722
723
  if (!value)
@@ -771,7 +772,8 @@ function toolbar2(items, meta) {
771
772
  throw Error("A toolbar needs at least one item.");
772
773
  return withMeta({
773
774
  kind: "toolbar",
774
- items: Object.freeze(items.map((item) => Object.freeze({ ...item })))
775
+ items: Object.freeze(items.map((item) => Object.freeze({ ...item }))),
776
+ ...meta?.labels !== void 0 ? { labels: meta.labels } : {}
775
777
  }, meta);
776
778
  }
777
779
  var readoutMeta = (o) => {
@@ -915,6 +917,8 @@ function normalizeField(field, input) {
915
917
  return typeof input === "number" && Number.isFinite(input) ? Math.round(Math.min(field.max, Math.max(field.min, input))) : field.value;
916
918
  case "keybind":
917
919
  return typeof input === "string" ? normalizeCombo2(input) : field.value;
920
+ case "text":
921
+ return typeof input === "string" ? normalizeText(field, input) : field.value;
918
922
  case "gradient":
919
923
  return normalizeStops(input) ?? field.value;
920
924
  case "curve":
@@ -1173,4 +1177,4 @@ function urlFlag2(name) {
1173
1177
  }
1174
1178
  var storageKey = (id, part) => `knobs:${encodeURIComponent(id)}:${part}:v1`;
1175
1179
 
1176
- export { __preload, __chunks, maxChroma, oklchToRgb, parseColor2, fitColor, formatColor, colorHex2, EASINGS2, EASING_Y, easingName, bezierPoint2, bezierCss2, normalizeCombo2, letterOf2, comboFromEvent2, heldMods, matchesCombo2, comboParts2, comboLabel2, isTypingTarget, GRADIENT_SAMPLES, sampleGradient2, gradientSamples, gradientCss2, stopColorAt, sampleCurve2, curveSamples, CURVE_SHAPES2, inRange, sameRamp, randomStops, mutateStops, randomPoints, mutatePoints, pickMeta, isReadout2, hasValue2, pinnedKeys, SPRING2, valueKeys, knob2, choice2, action2, flatSpring2, toggle2, spring2, color2, point2, easing2, vec22, vec32, range2, angle2, seed2, keybind2, gradient2, curve2, toolbarShortcut2, actionShortcut2, toolbar2, watch2, sparkline2, graph2, meter2, table2, log2, wrapAngle2, normalizeField, normalize, isRecord, same, equal, Spring, springMotion2, springResponse, springEasing, springCss2, SPRINGS2, FADE, MOTION, reducedMotion, reducedSpring2, springLinear, DEFAULT_LINEAR, motionCss, RESHAPE, RESET_KEYS, REFRESHES, readoutState, pushReadout, READOUT, memoryStorage2, browserStorage, urlFlag2, storageKey };
1180
+ export { maxChroma, oklchToRgb, parseColor2, fitColor, formatColor, colorHex2, EASINGS2, EASING_Y, easingName, bezierPoint2, bezierCss2, normalizeCombo2, letterOf2, comboFromEvent2, heldMods, matchesCombo2, comboParts2, comboLabel2, isTypingTarget, GRADIENT_SAMPLES, sampleGradient2, gradientSamples, gradientCss2, stopColorAt, sampleCurve2, curveSamples, CURVE_SHAPES2, inRange, sameRamp, randomStops, mutateStops, randomPoints, mutatePoints, pickMeta, isReadout2, hasValue2, variantsOf, pinnedKeys, SPRING2, valueKeys, knob2, choice2, action2, flatSpring2, toggle2, spring2, color2, point2, easing2, vec22, vec32, range2, angle2, seed2, keybind2, text2, gradient2, curve2, toolbarShortcut2, actionShortcut2, toolbar2, watch2, sparkline2, graph2, meter2, table2, log2, wrapAngle2, normalizeField, normalize, isRecord, same, equal, Spring, springMotion2, springResponse, springEasing, springCss2, SPRINGS2, FADE, MOTION, reducedMotion, reducedSpring2, springLinear, DEFAULT_LINEAR, motionCss, RESHAPE, RESET_KEYS, REFRESHES, readoutState, pushReadout, READOUT, memoryStorage2, browserStorage, urlFlag2, storageKey };