@kitlangton/knobs 0.4.0-next.10 → 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). */
@@ -130,6 +136,8 @@ export interface RemoteControlsOptions {
130
136
  readonly nullDefaults?: boolean;
131
137
  readonly values?: State;
132
138
  readonly presets?: Readonly<Record<string, State>>;
139
+ /** Groups pinned above the tab strip, outside the presets (see `ControlsOptions.pinned`). */
140
+ readonly pinned?: readonly string[];
133
141
  /** Numbered presets per group, e.g. the phone's preset sets (see `ControlsOptions.groupPresets`). */
134
142
  readonly groupPresets?: Readonly<Record<string, readonly GroupPreset[]>>;
135
143
  /** Display labels by group id, section id and preset id (see `ControlsOptions.groupLabels`). */
@@ -50,6 +50,12 @@ export interface FieldMeta {
50
50
  * Fields without a tab show on every tab.
51
51
  */
52
52
  readonly tab?: string;
53
+ /**
54
+ * Pins the field's group to the top of a panel, under the header and above the tab strip, on
55
+ * every tab (the first field of a group that says wins; a field without a group pins itself).
56
+ * Panel presets and saved variants leave pinned fields alone. See `ControlsOptions.pinned`.
57
+ */
58
+ readonly pinned?: boolean;
53
59
  /**
54
60
  * A collapsible sub-heading inside the field's group: a flat heading with a hairline, never a
55
61
  * card inside the card. Consecutive fields that share it sit under one heading.
@@ -57,7 +63,7 @@ export interface FieldMeta {
57
63
  readonly subsection?: string;
58
64
  }
59
65
  /** Meta keys copied from a field's options or a descriptor onto the field. */
60
- export declare const META_KEYS: readonly ["label", "group", "groupLabel", "section", "sectionLabel", "groupDescription", "sectionDescription", "shortcut", "random", "disabled", "readOnly", "folded", "sectionFolded", "tab", "subsection"];
66
+ export declare const META_KEYS: readonly ["label", "group", "groupLabel", "section", "sectionLabel", "groupDescription", "sectionDescription", "shortcut", "random", "disabled", "readOnly", "folded", "sectionFolded", "tab", "pinned", "subsection"];
61
67
  /** The meta `source` gives, without undefined keys. */
62
68
  export declare function pickMeta(source: FieldMeta | undefined, skip?: readonly string[]): FieldMeta;
63
69
  /** Marks a knob as half of a flat spring: two number keys drawn as one spring card. */
@@ -95,6 +101,12 @@ export interface Choice<T extends string = string> extends FieldMeta {
95
101
  * spare and there are at most four options, and a menu otherwise, switching as the width changes.
96
102
  */
97
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;
98
110
  }
99
111
  export interface Toggle extends FieldMeta {
100
112
  readonly kind: "toggle";
@@ -209,6 +221,15 @@ export interface KeybindField extends FieldMeta {
209
221
  readonly kind: "keybind";
210
222
  readonly value: string;
211
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
+ }
212
233
  /**
213
234
  * A toolbar button's key and where it presses the button: while the panel has focus (`"panel"`),
214
235
  * anywhere on the page but a text field (`"page"`), or nowhere (`"none"`: the chip only, for a
@@ -239,8 +260,10 @@ export interface ToolbarItem {
239
260
  export interface ToolbarField extends FieldMeta {
240
261
  readonly kind: "toolbar";
241
262
  readonly items: readonly ToolbarItem[];
263
+ /** `false`: icons only, each label in its tooltip (see `toolbar`). */
264
+ readonly labels?: boolean;
242
265
  }
243
- 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;
244
267
  /** A readout's own: polled while its row shows, instead of pushed with `controls.watch`. */
245
268
  interface ReadoutMeta extends FieldMeta {
246
269
  /** Read each frame while the row is on screen (pushing with `controls.watch` is the other way). */
@@ -329,7 +352,20 @@ export declare const isReadout: (field: {
329
352
  /** Whether a field holds a value (actions, toolbars and readouts don't). */
330
353
  export declare const hasValue: (field: Field) => field is ValueField;
331
354
  export type Schema = Readonly<Record<string, Field>>;
332
- 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;
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;
363
+ /**
364
+ * The schema's pinned keys (values, actions and readouts alike): every key of a group in `groups`,
365
+ * or whose first field that gives `pinned` says `true`, and ungrouped fields that say so themselves.
366
+ */
367
+ export declare function pinnedKeys(schema: Schema, groups?: readonly string[]): ReadonlySet<string>;
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;
333
369
  /** The keys of a schema's readouts (what `controls.watch` takes). */
334
370
  export type ReadoutKeys<S extends Schema> = {
335
371
  [K in keyof S]: [S[K] & ReadoutField] extends [never] ? never : K;
@@ -360,9 +396,17 @@ export declare function valueKeys(schema: Schema): string[];
360
396
  export declare function knob(value: number, options: Omit<Knob, "kind" | "value" | "step"> & {
361
397
  step?: number;
362
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
+ */
363
406
  export declare function choice<const T extends readonly [string, ...string[]]>(value: T[number], options: T, meta?: FieldMeta & {
364
407
  readonly labels?: Readonly<Partial<Record<T[number], string>>>;
365
408
  readonly display?: "menu" | "segmented";
409
+ readonly variants?: boolean;
366
410
  }): Choice<T[number]>;
367
411
  /**
368
412
  * `action("Replay", { run, shortcut: "Shift+K", global: true, rebind: true })`: a button row. Its
@@ -449,6 +493,7 @@ export declare function range(value: readonly [number, number], options: FieldMe
449
493
  folded?: boolean;
450
494
  sectionFolded?: boolean;
451
495
  tab?: string;
496
+ pinned?: boolean;
452
497
  subsection?: string;
453
498
  };
454
499
  /** Degrees on a dial (0–360 and wrapping by default): `angle(45)`, `angle(30, { min: -90, max: 90 })`. */
@@ -465,6 +510,15 @@ export declare function seed(value: number, options?: FieldMeta & {
465
510
  }): SeedField;
466
511
  /** A key combination captured by pressing it (`"Meta+Shift+K"`, `""` for none). */
467
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;
468
522
  /**
469
523
  * Colour over a 0…1 parameter: `gradient([{ at: 0, color: "#fff4d6" }, { at: 1, color: "#20242c" }])`.
470
524
  * Colours take what `color` takes and are kept as `oklch()` strings. `space` blends in `oklab`
@@ -503,8 +557,14 @@ type ResolvedShortcut = {
503
557
  export declare function toolbarShortcut(item: ToolbarItem): ResolvedShortcut | undefined;
504
558
  /** An action row's shortcut as `{ key, scope }`, or undefined without one. */
505
559
  export declare function actionShortcut(field: ActionField): ResolvedShortcut | undefined;
506
- /** A wrapping row of buttons with icons and shortcut chips; holds no value. */
507
- 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;
508
568
  type ReadoutOptions = Omit<ReadoutMeta, "shortcut" | "random" | "readOnly">;
509
569
  /** A live value: `watch({ label: "FPS", decimals: 0 })`, then `controls.watch("fps", 59.9)`. */
510
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>[];
@@ -348,6 +355,12 @@ export interface ControlsOptions<S extends Schema> extends LabelOptions {
348
355
  /** An undo stack the host owns; undo and redo call it (see `HostHistory`). */
349
356
  history?: HostHistory;
350
357
  presets?: Readonly<Record<string, Partial<Values<S>>>>;
358
+ /**
359
+ * Groups pinned to the top of a panel, under the header and above the tab strip, on every tab
360
+ * (as fields' `pinned: true` pins theirs). Presets and saved variants leave them alone: applying
361
+ * one keeps their values, saving one keeps them out, and they're modified against their defaults.
362
+ */
363
+ pinned?: readonly string[];
351
364
  /**
352
365
  * Numbered presets per group, shown as 1, 2, 3… on the group's card in a page. A group's
353
366
  * presets cover every key any of them names; keys a preset leaves out are at their defaults,
@@ -426,6 +439,8 @@ export interface Controls<S extends Schema = Schema> {
426
439
  readonly sectionFolded: SectionFolded | undefined;
427
440
  /** A preset's display label (`presetLabels`, `defaultLabel`), else its id. */
428
441
  presetLabel(id: string): string;
442
+ /** Whether `key` is pinned (its group is, see `ControlsOptions.pinned`), outside the presets. */
443
+ isPinned(key: string): boolean;
429
444
  readonly error: string | null;
430
445
  get(): Values<S>;
431
446
  getServerSnapshot(): Values<S>;
@@ -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;
@@ -407,6 +417,8 @@ export interface GpuPanelBinding {
407
417
  readonly id: string;
408
418
  readonly root: HTMLElement;
409
419
  readonly body: HTMLElement;
420
+ /** The pinned groups' cards, above the tab strip (empty while none are pinned). */
421
+ readonly pinned?: HTMLElement;
410
422
  readonly inline: boolean;
411
423
  readonly header: HTMLElement;
412
424
  readonly titleEl: 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,7 +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;
36
+ /** Whether any entry but the defaults gives one of `keys` a value other than its default. */
37
+ export declare function presetsTouch(entries: readonly PresetEntry<any>[], keys: readonly string[], defaults: Readonly<Record<string, unknown>>): boolean;
31
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";
32
44
  export interface PresetStrip {
33
45
  readonly element: HTMLElement;
34
46
  readonly track: HTMLElement;
@@ -43,6 +55,10 @@ export interface PresetStrip {
43
55
  getButtons(): readonly HTMLButtonElement[];
44
56
  getActiveState(): ActivePresetState;
45
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;
46
62
  selectById(id: string): void;
47
63
  /** Saves the current values as "Variant N" and opens its name to typing; returns a status message. */
48
64
  save(): string;
@@ -52,9 +68,13 @@ export interface PresetStrip {
52
68
  readonly nameField: () => HTMLInputElement | null;
53
69
  /** Deletes the selected chip when it's a saved variant; returns a status message. */
54
70
  deleteActive(): string | undefined;
71
+ /** Whether the strip should show: presets to pick (or saving on) that touch what's shown. */
72
+ shows(): boolean;
55
73
  update(): void;
56
74
  dispose(): void;
57
75
  }
58
76
  export declare function createPresetStrip<S extends Schema>(doc: Document, controls: Controls<S>, showVariants: boolean, run: (action: () => unknown) => void, onAfterChange?: () => void,
59
77
  /** What a preset's chip says (its id stays its identity); `controls.presetLabel` by default. */
60
- presetLabel?: (id: string) => string): PresetStrip;
78
+ presetLabel?: (id: string) => string,
79
+ /** Whether these entries matter where the strip is shown (a tab they don't touch hides it). */
80
+ relevant?: (entries: readonly PresetEntry<any>[]) => boolean): PresetStrip;
@@ -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;