@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.
@@ -2965,13 +2965,17 @@ var css2 = `
2965
2965
  .knobs-tree-row[aria-expanded="true"]>.knobs-tree-caret{transform:rotate(90deg)}
2966
2966
  .knobs-tree-row:not([aria-expanded])>.knobs-tree-caret{visibility:hidden}
2967
2967
  .knobs-tree-icon{flex:none;display:flex;align-items:center;justify-content:center;width:14px;margin-right:6px;color:var(--_k-fg-3);font-size:calc(var(--_k-font-size) - 1px)}
2968
- .knobs-tree-label{position:relative;flex:1;min-width:0;overflow:hidden;font-weight:var(--_k-weight)}
2968
+ /* The label wins the room: it keeps its width while the detail (which only takes what's left)
2969
+ fades out at its end, and gives way only once the detail is gone. Each fades where it's cut. */
2970
+ .knobs-tree-label{position:relative;flex:0 1 auto;min-width:0;overflow:hidden;font-weight:var(--_k-weight)}
2971
+ .knobs-tree-row[data-editing]>.knobs-tree-label{flex-grow:1}
2969
2972
  .knobs-tree-row[data-ancestor] .knobs-tree-label{color:var(--_k-fg-2)}
2970
2973
  .knobs-tree-label .knobs-tree-mark{color:var(--_k-accent)}
2971
2974
  .knobs-tree-row[data-off~="visible"] :is(.knobs-tree-label,.knobs-tree-icon){opacity:.45}
2972
2975
  .knobs-tree-row[data-off~="locked"] .knobs-tree-label{color:var(--_k-fg-2)}
2973
- .knobs-tree-detail{flex:none;margin-left:6px;color:var(--_k-fg-3);font-variant-numeric:tabular-nums}
2974
- .knobs-tree-tools{flex:none;display:flex;align-items:center;gap:1px;margin-left:4px}
2976
+ .knobs-tree-detail{flex:1 1 0;min-width:0;overflow:hidden;padding-left:6px;text-align:right;color:var(--_k-fg-3);font-variant-numeric:tabular-nums}
2977
+ .knobs-tree-row[data-editing]>.knobs-tree-detail{flex-grow:0;visibility:hidden}
2978
+ .knobs-tree-tools{flex:none;display:flex;align-items:center;gap:1px;margin-left:auto;padding-left:4px}
2975
2979
  .knobs-ui .knobs-tree-toggle{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:calc(var(--_k-radius) * 999);color:var(--_k-fg-3);cursor:pointer;opacity:0;transition:opacity .12s,color .12s,background .12s}
2976
2980
  .knobs-ui .knobs-tree-toggle svg{width:12px;height:12px}
2977
2981
  .knobs-tree-row:is(:hover,[data-active],[aria-selected="true"]) .knobs-tree-toggle,.knobs-ui .knobs-tree-toggle[aria-pressed="false"]{opacity:1}
@@ -5327,7 +5331,7 @@ function tree(container, options) {
5327
5331
  text.className = "knobs-tree-text";
5328
5332
  label.append(text);
5329
5333
  const detail = doc.createElement("span");
5330
- detail.className = "knobs-tree-detail";
5334
+ detail.className = "knobs-tree-detail knobs-scroll-fade-x";
5331
5335
  const tools = doc.createElement("span");
5332
5336
  tools.className = "knobs-tree-tools";
5333
5337
  el.append(caret, icon, label, detail, tools);
@@ -2964,13 +2964,17 @@ var css2 = `
2964
2964
  .knobs-tree-row[aria-expanded="true"]>.knobs-tree-caret{transform:rotate(90deg)}
2965
2965
  .knobs-tree-row:not([aria-expanded])>.knobs-tree-caret{visibility:hidden}
2966
2966
  .knobs-tree-icon{flex:none;display:flex;align-items:center;justify-content:center;width:14px;margin-right:6px;color:var(--_k-fg-3);font-size:calc(var(--_k-font-size) - 1px)}
2967
- .knobs-tree-label{position:relative;flex:1;min-width:0;overflow:hidden;font-weight:var(--_k-weight)}
2967
+ /* The label wins the room: it keeps its width while the detail (which only takes what's left)
2968
+ fades out at its end, and gives way only once the detail is gone. Each fades where it's cut. */
2969
+ .knobs-tree-label{position:relative;flex:0 1 auto;min-width:0;overflow:hidden;font-weight:var(--_k-weight)}
2970
+ .knobs-tree-row[data-editing]>.knobs-tree-label{flex-grow:1}
2968
2971
  .knobs-tree-row[data-ancestor] .knobs-tree-label{color:var(--_k-fg-2)}
2969
2972
  .knobs-tree-label .knobs-tree-mark{color:var(--_k-accent)}
2970
2973
  .knobs-tree-row[data-off~="visible"] :is(.knobs-tree-label,.knobs-tree-icon){opacity:.45}
2971
2974
  .knobs-tree-row[data-off~="locked"] .knobs-tree-label{color:var(--_k-fg-2)}
2972
- .knobs-tree-detail{flex:none;margin-left:6px;color:var(--_k-fg-3);font-variant-numeric:tabular-nums}
2973
- .knobs-tree-tools{flex:none;display:flex;align-items:center;gap:1px;margin-left:4px}
2975
+ .knobs-tree-detail{flex:1 1 0;min-width:0;overflow:hidden;padding-left:6px;text-align:right;color:var(--_k-fg-3);font-variant-numeric:tabular-nums}
2976
+ .knobs-tree-row[data-editing]>.knobs-tree-detail{flex-grow:0;visibility:hidden}
2977
+ .knobs-tree-tools{flex:none;display:flex;align-items:center;gap:1px;margin-left:auto;padding-left:4px}
2974
2978
  .knobs-ui .knobs-tree-toggle{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:calc(var(--_k-radius) * 999);color:var(--_k-fg-3);cursor:pointer;opacity:0;transition:opacity .12s,color .12s,background .12s}
2975
2979
  .knobs-ui .knobs-tree-toggle svg{width:12px;height:12px}
2976
2980
  .knobs-tree-row:is(:hover,[data-active],[aria-selected="true"]) .knobs-tree-toggle,.knobs-ui .knobs-tree-toggle[aria-pressed="false"]{opacity:1}
@@ -5326,7 +5330,7 @@ function tree(container, options) {
5326
5330
  text.className = "knobs-tree-text";
5327
5331
  label.append(text);
5328
5332
  const detail = doc.createElement("span");
5329
- detail.className = "knobs-tree-detail";
5333
+ detail.className = "knobs-tree-detail knobs-scroll-fade-x";
5330
5334
  const tools = doc.createElement("span");
5331
5335
  tools.className = "knobs-tree-tools";
5332
5336
  el.append(caret, icon, label, detail, tools);
@@ -12702,11 +12706,12 @@ var init_player = __esm(() => {
12702
12706
 
12703
12707
  // src/ui/solid.ts
12704
12708
  import {
12705
- children,
12706
12709
  createEffect,
12710
+ createMemo,
12707
12711
  createRoot,
12708
12712
  getOwner,
12709
12713
  onCleanup,
12714
+ splitProps,
12710
12715
  untrack
12711
12716
  } from "solid-js";
12712
12717
  import { insert } from "solid-js/web";
package/dist/review.js CHANGED
@@ -3006,13 +3006,17 @@ var css2 = `
3006
3006
  .knobs-tree-row[aria-expanded="true"]>.knobs-tree-caret{transform:rotate(90deg)}
3007
3007
  .knobs-tree-row:not([aria-expanded])>.knobs-tree-caret{visibility:hidden}
3008
3008
  .knobs-tree-icon{flex:none;display:flex;align-items:center;justify-content:center;width:14px;margin-right:6px;color:var(--_k-fg-3);font-size:calc(var(--_k-font-size) - 1px)}
3009
- .knobs-tree-label{position:relative;flex:1;min-width:0;overflow:hidden;font-weight:var(--_k-weight)}
3009
+ /* The label wins the room: it keeps its width while the detail (which only takes what's left)
3010
+ fades out at its end, and gives way only once the detail is gone. Each fades where it's cut. */
3011
+ .knobs-tree-label{position:relative;flex:0 1 auto;min-width:0;overflow:hidden;font-weight:var(--_k-weight)}
3012
+ .knobs-tree-row[data-editing]>.knobs-tree-label{flex-grow:1}
3010
3013
  .knobs-tree-row[data-ancestor] .knobs-tree-label{color:var(--_k-fg-2)}
3011
3014
  .knobs-tree-label .knobs-tree-mark{color:var(--_k-accent)}
3012
3015
  .knobs-tree-row[data-off~="visible"] :is(.knobs-tree-label,.knobs-tree-icon){opacity:.45}
3013
3016
  .knobs-tree-row[data-off~="locked"] .knobs-tree-label{color:var(--_k-fg-2)}
3014
- .knobs-tree-detail{flex:none;margin-left:6px;color:var(--_k-fg-3);font-variant-numeric:tabular-nums}
3015
- .knobs-tree-tools{flex:none;display:flex;align-items:center;gap:1px;margin-left:4px}
3017
+ .knobs-tree-detail{flex:1 1 0;min-width:0;overflow:hidden;padding-left:6px;text-align:right;color:var(--_k-fg-3);font-variant-numeric:tabular-nums}
3018
+ .knobs-tree-row[data-editing]>.knobs-tree-detail{flex-grow:0;visibility:hidden}
3019
+ .knobs-tree-tools{flex:none;display:flex;align-items:center;gap:1px;margin-left:auto;padding-left:4px}
3016
3020
  .knobs-ui .knobs-tree-toggle{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:calc(var(--_k-radius) * 999);color:var(--_k-fg-3);cursor:pointer;opacity:0;transition:opacity .12s,color .12s,background .12s}
3017
3021
  .knobs-ui .knobs-tree-toggle svg{width:12px;height:12px}
3018
3022
  .knobs-tree-row:is(:hover,[data-active],[aria-selected="true"]) .knobs-tree-toggle,.knobs-ui .knobs-tree-toggle[aria-pressed="false"]{opacity:1}
@@ -10484,7 +10488,7 @@ function tree(container, options) {
10484
10488
  text.className = "knobs-tree-text";
10485
10489
  label.append(text);
10486
10490
  const detail = doc.createElement("span");
10487
- detail.className = "knobs-tree-detail";
10491
+ detail.className = "knobs-tree-detail knobs-scroll-fade-x";
10488
10492
  const tools = doc.createElement("span");
10489
10493
  tools.className = "knobs-tree-tools";
10490
10494
  el.append(caret, icon, label, detail, tools);
@@ -45,6 +45,8 @@ export interface ToolbarDescriptor extends DescriptorMeta {
45
45
  readonly global?: boolean;
46
46
  readonly disabled?: boolean;
47
47
  }[];
48
+ /** `false`: icons only (see `toolbar`). */
49
+ readonly labels?: boolean;
48
50
  readonly default?: undefined;
49
51
  readonly bind?: undefined;
50
52
  }
@@ -77,11 +79,11 @@ export interface ReadoutDescriptor extends DescriptorMeta {
77
79
  export type ControlDescriptor = ValueDescriptor | SpringBindingDescriptor | ActionDescriptor | ToolbarDescriptor | ReadoutDescriptor;
78
80
  export interface ValueDescriptor extends DescriptorMeta {
79
81
  readonly bind?: undefined;
80
- readonly kind: "knob" | "number" | "choice" | "toggle" | "bool" | "spring" | "color" | "point" | "easing" | "vec2" | "vec3" | "range" | "angle" | "seed" | "keybind" | "gradient" | "curve";
82
+ readonly kind: "knob" | "number" | "choice" | "toggle" | "bool" | "spring" | "color" | "point" | "easing" | "vec2" | "vec3" | "range" | "angle" | "seed" | "keybind" | "text" | "gradient" | "curve";
81
83
  /**
82
84
  * Springs take `{ duration, bounce }`, colours a CSS string, points and vec2 `{ x, y }`, vec3
83
85
  * `{ x, y, z }`, easings `[x1, y1, x2, y2]`, ranges `[low, high]`, keybinds a combination
84
- * string (`"Meta+K"`), gradients `[{ at, color }]` and curves `[{ x, y }]`.
86
+ * string (`"Meta+K"`), text a string, gradients `[{ at, color }]` and curves `[{ x, y }]`.
85
87
  */
86
88
  readonly default: Value;
87
89
  readonly min?: number;
@@ -103,6 +105,10 @@ export interface ValueDescriptor extends DescriptorMeta {
103
105
  readonly axes?: readonly string[];
104
106
  /** Angles: carry past the ends a whole turn (on for a full turn by default). */
105
107
  readonly wrap?: boolean;
108
+ /** Text: lines in a box that grows with them (see `text`). */
109
+ readonly multiline?: boolean;
110
+ /** Text: shown in the empty field. */
111
+ readonly placeholder?: string;
106
112
  /** Gradients: blend in `oklab` (default) or `srgb`. */
107
113
  readonly space?: GradientSpace;
108
114
  /** Curves: the y range, `[0, 1]` by default (`min` and `max` work too). */
@@ -101,6 +101,12 @@ export interface Choice<T extends string = string> extends FieldMeta {
101
101
  * spare and there are at most four options, and a menu otherwise, switching as the width changes.
102
102
  */
103
103
  readonly display?: "menu" | "segmented";
104
+ /**
105
+ * The options are versions of the design: each is a preset in the panel's top strip (the
106
+ * default option is the default preset), which is where you switch them, and the choice gets no
107
+ * row of its own. See `choice`.
108
+ */
109
+ readonly variants?: true;
104
110
  }
105
111
  export interface Toggle extends FieldMeta {
106
112
  readonly kind: "toggle";
@@ -215,6 +221,15 @@ export interface KeybindField extends FieldMeta {
215
221
  readonly kind: "keybind";
216
222
  readonly value: string;
217
223
  }
224
+ /** Words: a name, a caption, a prompt. Typed into a field; `multiline` for a box of lines. */
225
+ export interface TextField extends FieldMeta {
226
+ readonly kind: "text";
227
+ readonly value: string;
228
+ /** Lines (Enter starts a new one) in a box that grows with them; one line otherwise. */
229
+ readonly multiline?: boolean;
230
+ /** Shown in the empty field. */
231
+ readonly placeholder?: string;
232
+ }
218
233
  /**
219
234
  * A toolbar button's key and where it presses the button: while the panel has focus (`"panel"`),
220
235
  * anywhere on the page but a text field (`"page"`), or nowhere (`"none"`: the chip only, for a
@@ -245,8 +260,10 @@ export interface ToolbarItem {
245
260
  export interface ToolbarField extends FieldMeta {
246
261
  readonly kind: "toolbar";
247
262
  readonly items: readonly ToolbarItem[];
263
+ /** `false`: icons only, each label in its tooltip (see `toolbar`). */
264
+ readonly labels?: boolean;
248
265
  }
249
- export type ValueField = Knob | Choice | Toggle | SpringField | ColorField | PointField | EasingField | VecField | RangeField | AngleField | SeedField | KeybindField | GradientField | CurveField;
266
+ export type ValueField = Knob | Choice | Toggle | SpringField | ColorField | PointField | EasingField | VecField | RangeField | AngleField | SeedField | KeybindField | TextField | GradientField | CurveField;
250
267
  /** A readout's own: polled while its row shows, instead of pushed with `controls.watch`. */
251
268
  interface ReadoutMeta extends FieldMeta {
252
269
  /** Read each frame while the row is on screen (pushing with `controls.watch` is the other way). */
@@ -335,12 +352,20 @@ export declare const isReadout: (field: {
335
352
  /** Whether a field holds a value (actions, toolbars and readouts don't). */
336
353
  export declare const hasValue: (field: Field) => field is ValueField;
337
354
  export type Schema = Readonly<Record<string, Field>>;
355
+ /**
356
+ * The schema's variants choice (`choice(…, { variants: true })`), if it has one. A pinned choice
357
+ * (`isPinned`) is never the variants choice: pinned wins, and it stays an ordinary choice.
358
+ */
359
+ export declare function variantsOf(schema: Schema, isPinned?: (key: string) => boolean): {
360
+ key: string;
361
+ field: Choice;
362
+ } | undefined;
338
363
  /**
339
364
  * The schema's pinned keys (values, actions and readouts alike): every key of a group in `groups`,
340
365
  * or whose first field that gives `pinned` says `true`, and ungrouped fields that say so themselves.
341
366
  */
342
367
  export declare function pinnedKeys(schema: Schema, groups?: readonly string[]): ReadonlySet<string>;
343
- export type FieldValue<F extends Field> = F extends Knob ? number : F extends Choice<infer T> ? T : F extends Toggle ? boolean : F extends SpringField ? SpringValue : F extends ColorField ? string : F extends PointField ? Point : F extends EasingField ? Bezier : F extends VecField<3> ? Vec3 : F extends VecField<2> ? Point : F extends RangeField ? readonly [number, number] : F extends AngleField | SeedField ? number : F extends KeybindField ? string : F extends GradientField ? readonly GradientStop[] : F extends CurveField ? readonly CurvePoint[] : never;
368
+ export type FieldValue<F extends Field> = F extends Knob ? number : F extends Choice<infer T> ? T : F extends Toggle ? boolean : F extends SpringField ? SpringValue : F extends ColorField ? string : F extends PointField ? Point : F extends EasingField ? Bezier : F extends VecField<3> ? Vec3 : F extends VecField<2> ? Point : F extends RangeField ? readonly [number, number] : F extends AngleField | SeedField ? number : F extends KeybindField | TextField ? string : F extends GradientField ? readonly GradientStop[] : F extends CurveField ? readonly CurvePoint[] : never;
344
369
  /** The keys of a schema's readouts (what `controls.watch` takes). */
345
370
  export type ReadoutKeys<S extends Schema> = {
346
371
  [K in keyof S]: [S[K] & ReadoutField] extends [never] ? never : K;
@@ -371,9 +396,17 @@ export declare function valueKeys(schema: Schema): string[];
371
396
  export declare function knob(value: number, options: Omit<Knob, "kind" | "value" | "step"> & {
372
397
  step?: number;
373
398
  }): Knob;
399
+ /**
400
+ * `choice("glass", ["glass", "solid", "flat"])`: one of a set of options, as segments or a menu.
401
+ * With `variants: true` the options are versions of the design instead: each becomes a preset in
402
+ * the panel's top strip (the default option is the default preset, the others follow in order),
403
+ * the choice has no row, and Random leaves it be. The store's `presets` add values to a variant
404
+ * by its option (`{ Roll: { speed: 0.8 } }`).
405
+ */
374
406
  export declare function choice<const T extends readonly [string, ...string[]]>(value: T[number], options: T, meta?: FieldMeta & {
375
407
  readonly labels?: Readonly<Partial<Record<T[number], string>>>;
376
408
  readonly display?: "menu" | "segmented";
409
+ readonly variants?: boolean;
377
410
  }): Choice<T[number]>;
378
411
  /**
379
412
  * `action("Replay", { run, shortcut: "Shift+K", global: true, rebind: true })`: a button row. Its
@@ -477,6 +510,15 @@ export declare function seed(value: number, options?: FieldMeta & {
477
510
  }): SeedField;
478
511
  /** A key combination captured by pressing it (`"Meta+Shift+K"`, `""` for none). */
479
512
  export declare function keybind(value?: string, meta?: FieldMeta): KeybindField;
513
+ /**
514
+ * Words typed into a field: `text("Hello")`, `text("", { multiline: true, placeholder: "Notes" })`.
515
+ * One line unless `multiline` (a line break typed or set there becomes a space). Out of Random
516
+ * and Mutate.
517
+ */
518
+ export declare function text(value?: string, options?: FieldMeta & {
519
+ multiline?: boolean;
520
+ placeholder?: string;
521
+ }): TextField;
480
522
  /**
481
523
  * Colour over a 0…1 parameter: `gradient([{ at: 0, color: "#fff4d6" }, { at: 1, color: "#20242c" }])`.
482
524
  * Colours take what `color` takes and are kept as `oklch()` strings. `space` blends in `oklab`
@@ -515,8 +557,14 @@ type ResolvedShortcut = {
515
557
  export declare function toolbarShortcut(item: ToolbarItem): ResolvedShortcut | undefined;
516
558
  /** An action row's shortcut as `{ key, scope }`, or undefined without one. */
517
559
  export declare function actionShortcut(field: ActionField): ResolvedShortcut | undefined;
518
- /** A wrapping row of buttons with icons and shortcut chips; holds no value. */
519
- export declare function toolbar(items: readonly ToolbarItem[], meta?: FieldMeta): ToolbarField;
560
+ /**
561
+ * A wrapping row of buttons with icons and shortcut chips; holds no value. When every item has an
562
+ * icon and the labels don't fit on one row, it's one row of icons, each label in its tooltip;
563
+ * `labels: false` always shows icons only, `labels: true` always shows the labels.
564
+ */
565
+ export declare function toolbar(items: readonly ToolbarItem[], meta?: FieldMeta & {
566
+ readonly labels?: boolean;
567
+ }): ToolbarField;
520
568
  type ReadoutOptions = Omit<ReadoutMeta, "shortcut" | "random" | "readOnly">;
521
569
  /** A live value: `watch({ label: "FPS", decimals: 0 })`, then `controls.watch("fps", 59.9)`. */
522
570
  export declare function watch(options?: ReadoutOptions): WatchField;
@@ -243,6 +243,13 @@ export interface MountPanelOptions extends LabelOptions {
243
243
  readonly density?: DensityMode;
244
244
  readonly renderer?: RendererMode;
245
245
  readonly variants?: boolean;
246
+ /**
247
+ * The presets as chips in a tray (`"chips"`) or as rows like a menu's (`"list"`). `"auto"` (the
248
+ * default) shows chips while they fit on two lines and the list once they'd take more.
249
+ */
250
+ readonly presetDisplay?: "auto" | "list" | "chips";
251
+ /** Shown in the panel's quiet style while it has no rows (`"This story has no controls"`). */
252
+ readonly empty?: string;
246
253
  readonly history?: boolean;
247
254
  readonly footer?: boolean;
248
255
  readonly actions?: readonly ControlAction<any>[];
@@ -1,4 +1,4 @@
1
- export { knob, choice, toggle, spring, color, point, easing, action, flatSpring, vec2, vec3, range, angle, seed, keybind, gradient, curve, toolbar, toolbarShortcut, actionShortcut, watch, sparkline, graph, meter, table, log, hasValue, isReadout, wrapAngle, SPRING, type FieldMeta, type ActionField, type SpringPart, type ValueField, type Knob, type Choice, type Toggle, type SpringField, type ColorField, type PointField, type EasingField, type VecField, type Vec3, type RangeField, type AngleField, type SeedField, type KeybindField, type GradientField, type CurveField, type ToolbarField, type ToolbarItem, type ToolbarShortcut, type WatchField, type SparklineField, type GraphField, type GraphSeries, type GraphThreshold, type MeterField, type TableField, type LogField, type ReadoutField, type ReadoutKeys, type ReadoutInput, type Point, type Field, type Schema, type Values, type Value, type State, } from "./core/schema.js";
1
+ export { knob, choice, toggle, spring, color, point, easing, action, flatSpring, vec2, vec3, range, angle, seed, keybind, text, gradient, curve, toolbar, toolbarShortcut, actionShortcut, watch, sparkline, graph, meter, table, log, hasValue, isReadout, wrapAngle, SPRING, type FieldMeta, type ActionField, type SpringPart, type ValueField, type Knob, type Choice, type Toggle, type SpringField, type ColorField, type PointField, type EasingField, type VecField, type Vec3, type RangeField, type AngleField, type SeedField, type KeybindField, type TextField, type GradientField, type CurveField, type ToolbarField, type ToolbarItem, type ToolbarShortcut, type WatchField, type SparklineField, type GraphField, type GraphSeries, type GraphThreshold, type MeterField, type TableField, type LogField, type ReadoutField, type ReadoutKeys, type ReadoutInput, type Point, type Field, type Schema, type Values, type Value, type State, } from "./core/schema.js";
2
2
  export { springCss, springMotion, type SpringValue } from "./core/spring.js";
3
3
  export { reducedSpring, SPRINGS, type SpringName } from "./core/motion.js";
4
4
  export { colorHex, parseColor, type Oklch } from "./core/color.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The header of the knobs/ui split pane `host` sits in, when the pane has one (a title) and no
3
+ * panel's buttons have joined it yet. A panel there with no title of its own puts its buttons in
4
+ * it (after the pane's actions, before its collapse button) rather than drawing a header row
5
+ * that would hold only them: one header per surface.
6
+ */
7
+ export declare function paneHeadOf(host: HTMLElement): HTMLElement | undefined;
8
+ /** Moves `actions` into the pane header `head`; the returned function takes them back out. */
9
+ export declare function dock(actions: HTMLElement, head: HTMLElement): () => void;
@@ -333,6 +333,16 @@ export interface GpuKeybindRow {
333
333
  readonly wave: RowWave;
334
334
  isModified(): boolean;
335
335
  }
336
+ /** A text row: the canvas draws its label; the field itself is the DOM's, above the canvas. */
337
+ export interface GpuTextRow {
338
+ readonly kind: "text";
339
+ readonly key: string;
340
+ readonly display: string;
341
+ readonly row: HTMLElement;
342
+ readonly labelEl: HTMLElement;
343
+ readonly wave: RowWave;
344
+ isModified(): boolean;
345
+ }
336
346
  export interface GpuToolbarRow {
337
347
  readonly kind: "toolbar";
338
348
  readonly key: string;
@@ -370,7 +380,7 @@ export interface GpuSubheadRow {
370
380
  readonly wave: RowWave;
371
381
  isModified(): boolean;
372
382
  }
373
- export type GpuRowBinding = GpuReadoutRow | GpuVecRow | GpuRangeRow | GpuAngleRow | GpuSeedRow | GpuKeybindRow | GpuToolbarRow | GpuSubheadRow | GpuActionRow | GpuKnobRow | GpuChoiceRow | GpuToggleRow | GpuSpringRow | GpuColorRow | GpuPointRow | GpuEasingRow | GpuGradientRow | GpuCurveRow;
383
+ export type GpuRowBinding = GpuReadoutRow | GpuVecRow | GpuRangeRow | GpuAngleRow | GpuSeedRow | GpuKeybindRow | GpuTextRow | GpuToolbarRow | GpuSubheadRow | GpuActionRow | GpuKnobRow | GpuChoiceRow | GpuToggleRow | GpuSpringRow | GpuColorRow | GpuPointRow | GpuEasingRow | GpuGradientRow | GpuCurveRow;
374
384
  export interface GpuGroupBinding {
375
385
  readonly element: HTMLElement;
376
386
  readonly labelEl: HTMLElement;
@@ -1,4 +1,4 @@
1
- import { toolbarShortcut, type AngleField, type KeybindField, type Point, type RangeField, type SeedField, type ToolbarField, type ToolbarItem, type Vec3, type VecField } from "../core/schema.js";
1
+ import { toolbarShortcut, type AngleField, type KeybindField, type Point, type TextField, type RangeField, type SeedField, type ToolbarField, type ToolbarItem, type Vec3, type VecField } from "../core/schema.js";
2
2
  import { type EditorRow, type RowContext } from "./editors.js";
3
3
  type Vec = Point & {
4
4
  readonly z?: number;
@@ -30,6 +30,12 @@ export declare function keyCap(o: {
30
30
  wake(): void;
31
31
  }): KeyCap;
32
32
  export declare function keybindRow(c: RowContext<KeybindField>): EditorRow;
33
+ /**
34
+ * Words in a field: one line beside the label, or (`multiline`) a box under it that grows with
35
+ * its lines. Each keystroke sets the value; one focus's typing is one undo step. Enter (⌘Enter in
36
+ * a box) or Esc lets go of it.
37
+ */
38
+ export declare function textRow(c: RowContext<TextField>): EditorRow;
33
39
  export interface ToolbarRow extends EditorRow {
34
40
  readonly field: ToolbarField;
35
41
  /** The item a keydown presses, if any (`global` says whether it's a page-wide shortcut). */
@@ -42,8 +48,10 @@ export interface ToolbarRow extends EditorRow {
42
48
  type Shortcut = ReturnType<typeof toolbarShortcut>;
43
49
  /**
44
50
  * A toolbar: its buttons in equal columns, as many as fit and balanced across the rows (see
45
- * `toolbarColumns`), each with its icon and label at the start and its shortcut at the end.
46
- * `keyOf` is each item's shortcut as it is now (the store's rebinding, else its own).
51
+ * `toolbarColumns`), each with its icon and label at the start and its shortcut at the end. When
52
+ * every button has an icon and the labels don't fit on one row (or `labels: false`), it's one row
53
+ * of icons, each label and shortcut in its tooltip. `keyOf` is each item's shortcut as it is now
54
+ * (the store's rebinding, else its own).
47
55
  */
48
56
  export declare function toolbarRow(c: RowContext<ToolbarField>, press: (item: ToolbarItem) => void, keyOf?: (item: ToolbarItem) => Shortcut): ToolbarRow;
49
57
  /** The value a vector shows, as `{ x, y }` or `{ x, y, z }`. */
@@ -28,9 +28,19 @@ export declare function activeEntryId(controls: Controls<any>): string | null;
28
28
  * cycling never gets stuck when a saved variant has the exact same values as a built-in preset.
29
29
  */
30
30
  export declare function resolveActivePresetEntry<V extends Record<string, any>>(entries: readonly PresetEntry<V>[], currentValues: V, lastAppliedId: string | null, baseId?: string | null): ActivePresetState;
31
+ /**
32
+ * How many lines `count` chips take in `avail` px, at least `min` px each with `gap` between (as
33
+ * the strip's auto-fit grid lays them); 1 before there's room to measure.
34
+ */
35
+ export declare function presetLines(count: number, min: number, avail: number, gap: number): number;
31
36
  /** Whether any entry but the defaults gives one of `keys` a value other than its default. */
32
37
  export declare function presetsTouch(entries: readonly PresetEntry<any>[], keys: readonly string[], defaults: Readonly<Record<string, unknown>>): boolean;
33
38
  export declare function nextPresetIndex(count: number, activeIndex: number, direction: number): number;
39
+ /**
40
+ * How the strip lays out its presets: `"chips"` in a tray of equal columns, `"list"` as rows like a
41
+ * menu's, or `"auto"` (the default): chips while they fit on two lines, else the list.
42
+ */
43
+ export type PresetDisplay = "auto" | "list" | "chips";
34
44
  export interface PresetStrip {
35
45
  readonly element: HTMLElement;
36
46
  readonly track: HTMLElement;
@@ -45,6 +55,10 @@ export interface PresetStrip {
45
55
  getButtons(): readonly HTMLButtonElement[];
46
56
  getActiveState(): ActivePresetState;
47
57
  cycle(direction: -1 | 1): void;
58
+ /** Applies the nth chip (0-based), as a click on it does. */
59
+ select(index: number): void;
60
+ /** Lays the presets out as chips or a list (see `PresetDisplay`). */
61
+ display(mode: PresetDisplay): void;
48
62
  selectById(id: string): void;
49
63
  /** Saves the current values as "Variant N" and opens its name to typing; returns a status message. */
50
64
  save(): string;
@@ -28,11 +28,12 @@ export declare function crumbTrail(path: {
28
28
  */
29
29
  export declare function escTarget(trail: readonly Crumb[], title: string): string | undefined;
30
30
  /**
31
- * How much of a trail fits `room` px: crumbs from `from` on show (the ones before it fold into
32
- * one "…"), and the Esc hint shows only when it fits too. The deepest crumb always shows.
33
- * `widths` are the crumbs' own; `sep` a separator's (with its gaps), `more` the "…"'s.
31
+ * How much of a trail fits `room` px: crumbs from `from` on show, the ones before it dropped, and
32
+ * the Esc hint shows only when it fits too. Without room for the deepest crumb, none show
33
+ * (`from` is the trail's length): the line never runs past the title's room into the buttons.
34
+ * `widths` are the crumbs' own; `sep` a separator's (with its gaps).
34
35
  */
35
- export declare function fitCrumbs(widths: readonly number[], room: number, sep: number, more: number, hint?: number): {
36
+ export declare function fitCrumbs(widths: readonly number[], room: number, sep: number, hint?: number): {
36
37
  from: number;
37
38
  hint: boolean;
38
39
  };
@@ -41,6 +42,16 @@ export declare function fitCrumbs(widths: readonly number[], room: number, sep:
41
42
  * the first nine, as they read.
42
43
  */
43
44
  export declare function digitOption(key: string, count: number): number | undefined;
45
+ /**
46
+ * Where a digit goes in a panel: a focused row's options take 1–9 (even past the last), a number
47
+ * row and a page card's numbered presets keep theirs, and anywhere else 1–9 pick the preset
48
+ * strip's chips (the design's variants) while the strip has that many. Undefined leaves the key.
49
+ */
50
+ export declare function digitRoute(key: string, at: {
51
+ options: boolean;
52
+ keeps: boolean;
53
+ presets: number;
54
+ }): "options" | "presets" | undefined;
44
55
  /**
45
56
  * The digit hints for a row's options: one per option that shows (up to nine), or, while they
46
57
  * don't (a menu, a folded editor), one standing for them all ("1–5").
@@ -0,0 +1 @@
1
+ export declare const canvasCss = "\n.knobs-ui.knobs-canvas{--_kc-radius:var(--k-ui-canvas-radius,var(--_k-radius-panel));--_kc-fade:var(--k-ui-canvas-fade,16px);\n position:relative;width:100%;height:100%;overflow:hidden;outline:none;background:var(--k-ui-canvas-bg,var(--_k-bg-page));contain:strict;\n touch-action:none;overscroll-behavior:none;user-select:none;-webkit-user-select:none;cursor:default;\n font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}\n:where(.knobs-canvas-stage,.knobs-canvas-stage *,.knobs-canvas-zoom,.knobs-canvas-zoom *){box-sizing:border-box}\n/* No hard edges: what slides out of view fades over the last few px. */\n.knobs-canvas-stage{position:absolute;inset:0;overflow:hidden;\n -webkit-mask-image:linear-gradient(to right,transparent,#000 var(--_kc-fade),#000 calc(100% - var(--_kc-fade)),transparent),linear-gradient(to bottom,transparent,#000 var(--_kc-fade),#000 calc(100% - var(--_kc-fade)),transparent);\n -webkit-mask-composite:source-in;mask-image:linear-gradient(to right,transparent,#000 var(--_kc-fade),#000 calc(100% - var(--_kc-fade)),transparent),linear-gradient(to bottom,transparent,#000 var(--_kc-fade),#000 calc(100% - var(--_kc-fade)),transparent);mask-composite:intersect}\n.knobs-canvas-dots{position:absolute;left:-128px;top:-128px;right:-128px;bottom:-128px;pointer-events:none;will-change:transform;\n background-image:radial-gradient(circle,var(--k-ui-canvas-dot,color-mix(in srgb,var(--_k-fg) 22%,transparent)) 1px,transparent 1.25px)}\n.knobs-canvas-world{position:absolute;left:0;top:0;width:0;height:0;transform-origin:0 0}\n.knobs-canvas[data-moving] .knobs-canvas-world{will-change:transform}\n.knobs-canvas:is([data-panning],[data-space]) .knobs-canvas-world{pointer-events:none}\n.knobs-canvas[data-space]{cursor:grab}\n.knobs-canvas[data-grabbing]{cursor:grabbing}\n.knobs-canvas-tile{position:absolute;left:0;top:0;display:flex;flex-direction:column;width:max-content;transform-origin:50% 50%}\n.knobs-canvas-title{flex:none;contain:inline-size;height:24px;padding:0 2px 8px;overflow:hidden;white-space:nowrap;color:var(--_k-fg-2);\n font-size:var(--_k-type-small,12px);line-height:16px;-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 16px),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 16px),transparent)}\n.knobs-canvas-card{position:relative;flex:none;padding:var(--k-ui-canvas-pad,16px);border-radius:var(--_kc-radius);background:var(--k-ui-canvas-card,var(--_k-bg));\n overflow:hidden;user-select:text;-webkit-user-select:text}\n.knobs-canvas-card::after{content:\"\";position:absolute;inset:0;border-radius:inherit;box-shadow:inset 0 0 0 1px var(--_k-border);pointer-events:none}\n/* The readout: how far, and the way back to the whole set or 100%. */\n.knobs-canvas-zoom{position:absolute;right:10px;bottom:10px;z-index:1;display:flex;align-items:center;gap:2px;height:28px;padding:0 4px;border-radius:999px;\n background:var(--_k-bg);box-shadow:inset 0 0 0 1px var(--_k-border);color:var(--_k-fg-2);font-size:var(--_k-type-small,12px);\n font-variant-numeric:tabular-nums;font-feature-settings:\"tnum\";white-space:nowrap}\n:where(.knobs-ui) .knobs-canvas-btn{all:revert;box-sizing:border-box;font:inherit;letter-spacing:inherit;margin:0;border:0;outline:none;box-shadow:none;\n display:inline-flex;align-items:center;justify-content:center;height:20px;min-width:20px;padding:0 6px;border-radius:999px;background:transparent;color:inherit;cursor:default;\n transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}\n:where(.knobs-ui) .knobs-canvas-btn:is(:hover,:focus-visible){background:var(--_k-fill-hover);color:var(--_k-fg);transition-duration:0ms}\n:where(.knobs-ui) .knobs-canvas-btn:focus-visible{box-shadow:0 0 0 1.5px var(--_k-accent)}\n.knobs-canvas-pct{min-width:46px}\n.knobs-canvas-btn svg{width:13px;height:13px}\n";
@@ -0,0 +1,116 @@
1
+ /** The camera: a world point `p` is drawn at `p * scale + (x, y)` in the canvas's own px. */
2
+ export interface CanvasView {
3
+ readonly x: number;
4
+ readonly y: number;
5
+ readonly scale: number;
6
+ }
7
+ export interface CanvasRect {
8
+ readonly x: number;
9
+ readonly y: number;
10
+ readonly w: number;
11
+ readonly h: number;
12
+ }
13
+ export interface CanvasPoint {
14
+ readonly x: number;
15
+ readonly y: number;
16
+ }
17
+ export declare const clamp: (v: number, lo: number, hi: number) => number;
18
+ /**
19
+ * Tiles in rows, in order: each row takes tiles until the next would pass `width`, rows sit `gap`
20
+ * apart and tiles top-align in them. Rects land on whole px, so at 100% they're on the pixel grid.
21
+ */
22
+ export declare function packRows(sizes: readonly {
23
+ readonly w: number;
24
+ readonly h: number;
25
+ }[], opts: {
26
+ readonly gap: number;
27
+ readonly width: number;
28
+ readonly x?: number;
29
+ readonly y?: number;
30
+ }): CanvasRect[];
31
+ /**
32
+ * How wide rows run so the packed set comes out about `aspect` (width / height): a bisection
33
+ * between the widest tile and one long row, on how the packing actually comes out.
34
+ */
35
+ export declare function rowWidth(sizes: readonly {
36
+ readonly w: number;
37
+ readonly h: number;
38
+ }[], gap: number, aspect: number): number;
39
+ /**
40
+ * Where every item goes: one with an `x` and `y` stays there; the rest pack in rows (`packRows`),
41
+ * under the placed ones if there are any.
42
+ */
43
+ export declare function layoutCanvas(items: readonly {
44
+ readonly x?: number;
45
+ readonly y?: number;
46
+ readonly w: number;
47
+ readonly h: number;
48
+ }[], opts: {
49
+ readonly gap: number;
50
+ readonly width?: number;
51
+ readonly aspect?: number;
52
+ }): {
53
+ rects: CanvasRect[];
54
+ width: number;
55
+ };
56
+ /** The rect around `rects`, or null for none. */
57
+ export declare function boundsOf(rects: readonly CanvasRect[]): CanvasRect | null;
58
+ /** Screen (canvas px) to world, and back. */
59
+ export declare const toWorld: (v: CanvasView, p: CanvasPoint) => CanvasPoint;
60
+ export declare const toScreen: (v: CanvasView, p: CanvasPoint) => CanvasPoint;
61
+ /** The view at `scale` (clamped to `min`…`max`), with the world point under `at` still under it. */
62
+ export declare function zoomAround(v: CanvasView, scale: number, at: CanvasPoint, min?: number, max?: number): CanvasView;
63
+ /**
64
+ * Two fingers moved: the world point under their old midpoint `from` lands under the new one
65
+ * `to`, with the zoom times `ratio` (zoom around `from`, then follow the midpoint).
66
+ */
67
+ export declare function pinchView(v: CanvasView, from: CanvasPoint, to: CanvasPoint, ratio: number, min?: number, max?: number): CanvasView;
68
+ /** The most one wheel event zooms (a mouse wheel's notch under ⌘ or ctrl). */
69
+ export declare const WHEEL_ZOOM_MAX = 1.25;
70
+ /**
71
+ * One ⌘/ctrl-wheel event's zoom factor, applied directly: a trackpad pinch (ctrl) at 1% per px,
72
+ * ⌘-scroll at .25%, and never more than `WHEEL_ZOOM_MAX` either way per event, so a mouse
73
+ * wheel's big notches step evenly without springing.
74
+ */
75
+ export declare function wheelZoom(deltaY: number, pinch: boolean): number;
76
+ /** The view that shows `r` whole and centred in a `w` × `h` canvas, `pad` px in from its edges. */
77
+ export declare function fitView(r: CanvasRect, w: number, h: number, opts?: {
78
+ readonly pad?: number;
79
+ readonly min?: number;
80
+ readonly max?: number;
81
+ }): CanvasView;
82
+ /**
83
+ * A path between two views, `t` 0 → 1. It zooms around the one canvas point both views agree on,
84
+ * at an even rate in log scale, so every point on screen travels one straight line in one
85
+ * direction (no curve from springing x, y and scale apart). Views at the same scale pan straight.
86
+ */
87
+ export declare function viewPath(from: CanvasView, to: CanvasView): (t: number) => CanvasView;
88
+ /** The translate on the device-pixel grid, so a resting view draws crisp. */
89
+ export declare const snapView: (v: CanvasView, dpr: number) => CanvasView;
90
+ /**
91
+ * The background's dots: `step` world px apart, drawn `size` canvas px apart, and how strongly
92
+ * (0…1): they fade out as they crowd together zooming out and as they spread apart zooming in.
93
+ */
94
+ export declare function dotGrid(scale: number, step?: number): {
95
+ size: number;
96
+ alpha: number;
97
+ };
98
+ /** What a key does to the view: 0 fits, 1 is 100%, = and - zoom in and out. */
99
+ export type CanvasKey = "fit" | "actual" | "in" | "out";
100
+ export declare function keyAction(e: {
101
+ readonly key: string;
102
+ readonly altKey?: boolean;
103
+ }): CanvasKey | null;
104
+ /** How far each zoom key steps. */
105
+ export declare const KEY_ZOOM = 1.5;
106
+ /**
107
+ * The hand's velocity at release (px/s) from recent samples `{ x, y, t (ms) }`: the travel over
108
+ * the last `window` ms. Zero if the hand had already stopped.
109
+ */
110
+ export declare function releaseVelocity(samples: readonly {
111
+ readonly x: number;
112
+ readonly y: number;
113
+ readonly t: number;
114
+ }[], now: number, window?: number): CanvasPoint;
115
+ /** Momentum's time constant (s): the coast slows as iOS's scroll does (0.998 per ms). */
116
+ export declare const COAST_TAU: number;
@@ -0,0 +1,66 @@
1
+ import { type CanvasPoint, type CanvasRect, type CanvasView } from "./canvas-model.js";
2
+ import { type UiLook } from "./look.js";
3
+ export interface CanvasItem {
4
+ readonly id: string;
5
+ /** Where it sits on the canvas (world px). Without both, it packs into the grid. */
6
+ readonly x?: number;
7
+ readonly y?: number;
8
+ /** The card's size (px). Without them, the card takes its content's size. */
9
+ readonly w?: number;
10
+ readonly h?: number;
11
+ /** Shown above the card. */
12
+ readonly title?: string;
13
+ /** Draws the item into its card; return a cleanup for when it goes. */
14
+ readonly render: (el: HTMLElement) => void | (() => void);
15
+ }
16
+ export interface CanvasOptions extends UiLook {
17
+ readonly items: readonly CanvasItem[];
18
+ /** Between packed items, world px (48). */
19
+ readonly gap?: number;
20
+ /** Around what `fit` and `focus` show, px on screen (48). */
21
+ readonly padding?: number;
22
+ /** How wide packed rows run, world px (as wide as makes the set about the canvas's shape). */
23
+ readonly width?: number;
24
+ /** The zoom's range (.05 … 8). */
25
+ readonly minZoom?: number;
26
+ readonly maxZoom?: number;
27
+ /** The zoom readout, with fit and 100% (true). */
28
+ readonly readout?: boolean;
29
+ /** The view moved: a gesture, a key, a fit or a focus. */
30
+ readonly onView?: (view: CanvasView) => void;
31
+ /** The canvas's accessible name. */
32
+ readonly label?: string;
33
+ }
34
+ export interface Canvas {
35
+ readonly element: HTMLElement;
36
+ readonly view: CanvasView;
37
+ /** Each item's place (world px, its title included). */
38
+ readonly rects: ReadonlyMap<string, CanvasRect>;
39
+ /** Shows every item. */
40
+ fit(): void;
41
+ /** Brings an item to the middle and zooms to fit it (at most 200%). */
42
+ focus(id: string): void;
43
+ /** Zooms to `scale` around a point on the canvas (its middle by default). */
44
+ zoomTo(scale: number, at?: CanvasPoint): void;
45
+ /** New items: those that stay keep their content and glide to new places; new ones fade in, gone ones out. */
46
+ update(items: readonly CanvasItem[]): void;
47
+ /** Whether anything is moving (the frame loop runs only then). */
48
+ readonly moving: boolean;
49
+ /** Frames stepped, and the script time each took (ms), for perf checks. */
50
+ stats(): {
51
+ frames: number;
52
+ p50: number;
53
+ p95: number;
54
+ max: number;
55
+ };
56
+ dispose(): void;
57
+ }
58
+ /**
59
+ * An infinite canvas: items as cards on a dotted page that pans and zooms freely. Drag the page
60
+ * (or hold Space, or the middle button, and drag anywhere) or scroll with two fingers to pan;
61
+ * pinch or ⌘/ctrl-scroll to zoom around the pointer. Both follow the hand 1:1 and coast on
62
+ * release. 0 fits everything, 1 is 100%, = and - zoom, and a double-click focuses an item (again,
63
+ * or on the page, fits). Items stay live: clicks and keys reach them. Items without a place pack
64
+ * into rows sized to their content. Nothing moves on load: the first fit is instant.
65
+ */
66
+ export declare function canvas(container: HTMLElement, options: CanvasOptions): Canvas;
@@ -27,6 +27,8 @@ export { codeDiff, diffCounts, diffReview, diffStatus, type CodeDiff, type CodeD
27
27
  export { compareText, comparePaths, countChanges, diffBar, diffFiles, diffFolders, diffStats, diffTree, fileKey, filesUnder, keepFile, matchPath, parsePatch, splitPath, stepFile, type DiffChange, type DiffFile, type DiffInput, type DiffRows, type DiffSource, type DiffTreeItem, } from "./diff-model.js";
28
28
  export { bento, type Bento, type BentoItem, type BentoOptions } from "./bento.js";
29
29
  export { fitRect, FIT_MAX, motionBlur, neighbour as bentoNeighbour, planBento, rowTarget, windowOf as bentoWindow, type BentoDirection, type BentoLayout, type BentoMode, type BentoPlan, type BentoRect, } from "./bento-model.js";
30
+ export { canvas, type Canvas, type CanvasItem, type CanvasOptions } from "./canvas.js";
31
+ export { boundsOf as canvasBounds, dotGrid, fitView, layoutCanvas, packRows, viewPath, zoomAround, type CanvasPoint, type CanvasRect, type CanvasView, } from "./canvas-model.js";
30
32
  export { activity, type Activity, type ActivityOptions, } from "./activity.js";
31
33
  export { diffActivity, flattenActivity, formatDuration, indexActivity, keepSelection as keepActivitySelection, navigateActivity, railSegments, showsSelection, stackRows, type ActivityEvent, type ActivityInput, type ActivityKey, type ActivityKind, type ActivityRow, type RailNode, } from "./activity-model.js";
32
34
  export { optionPicker, type OptionPicker, type OptionPickerOptions, type PickerOption, } from "./picker.js";