tweakit 0.5.2 → 0.6.0
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/AGENTS.md +31 -29
- package/README.md +55 -29
- package/THIRD-PARTY-NOTICES.md +0 -1
- package/dist/tweaks/auto.js +2 -0
- package/dist/tweaks/auto.js.map +6 -0
- package/dist/tweaks/bezier-JFEZMCLP.js +2 -0
- package/dist/tweaks/bezier-JFEZMCLP.js.map +6 -0
- package/dist/tweaks/chunk-6UO4DHBN.js +2 -0
- package/dist/tweaks/chunk-6UO4DHBN.js.map +6 -0
- package/dist/tweaks/chunk-DIQMLBAS.js +2 -0
- package/dist/tweaks/chunk-DIQMLBAS.js.map +6 -0
- package/dist/tweaks/chunk-XE6OUJBG.js +2 -0
- package/dist/tweaks/chunk-XE6OUJBG.js.map +6 -0
- package/dist/tweaks/clip-WW7JYO4X.js +2 -0
- package/dist/tweaks/clip-WW7JYO4X.js.map +6 -0
- package/dist/tweaks/color-YMMFURP6.js +2 -0
- package/dist/tweaks/color-YMMFURP6.js.map +6 -0
- package/dist/tweaks/core.js +2 -1
- package/dist/tweaks/core.js.map +6 -0
- package/dist/tweaks/{fold-UGN7TXGX.js → fold-U6YRZLCJ.js} +2 -1
- package/dist/tweaks/fold-U6YRZLCJ.js.map +6 -0
- package/dist/tweaks/gradient-JER7PW4D.js +2 -0
- package/dist/tweaks/gradient-JER7PW4D.js.map +6 -0
- package/dist/tweaks/heavy-CLD7UAVI.js +2 -0
- package/dist/tweaks/heavy-CLD7UAVI.js.map +6 -0
- package/dist/tweaks/image-H6QSLGO3.js +4 -0
- package/dist/tweaks/image-H6QSLGO3.js.map +6 -0
- package/dist/tweaks/image-store-WRKY2NPU.js +2 -0
- package/dist/tweaks/image-store-WRKY2NPU.js.map +6 -0
- package/dist/tweaks/interval-TEUHQGZN.js +2 -0
- package/dist/tweaks/interval-TEUHQGZN.js.map +6 -0
- package/dist/tweaks/markup-NIBJD6E2.js +2 -0
- package/dist/tweaks/markup-NIBJD6E2.js.map +6 -0
- package/dist/tweaks/monitor-3BY7YCGX.js +3 -0
- package/dist/tweaks/monitor-3BY7YCGX.js.map +6 -0
- package/dist/tweaks/motion-FQPUBKKM.js +2 -0
- package/dist/tweaks/motion-FQPUBKKM.js.map +6 -0
- package/dist/tweaks/plot-2HBNBZSG.js +3 -0
- package/dist/tweaks/plot-2HBNBZSG.js.map +6 -0
- package/dist/tweaks/point-VV6XV3I6.js +2 -0
- package/dist/tweaks/point-VV6XV3I6.js.map +6 -0
- package/dist/tweaks/shadow-5E4E55I2.js +2 -0
- package/dist/tweaks/shadow-5E4E55I2.js.map +6 -0
- package/dist/tweaks/sides-P3STEVYY.js +2 -0
- package/dist/tweaks/sides-P3STEVYY.js.map +6 -0
- package/dist/tweaks/spring-HUVSXNVN.js +2 -0
- package/dist/tweaks/spring-HUVSXNVN.js.map +6 -0
- package/dist/tweaks/tabs-76FW7LQ7.js +2 -0
- package/dist/tweaks/tabs-76FW7LQ7.js.map +6 -0
- package/dist/tweaks.css +1 -1
- package/dist/tweaks.js +6 -3
- package/dist/tweaks.js.map +6 -0
- package/dist/types/tweaks/easing.d.ts +8 -11
- package/dist/types/tweaks/panel.d.ts +6 -4
- package/dist/types/tweaks/schema.d.ts +4 -8
- package/dist/types/tweaks/standalone.d.ts +3 -3
- package/dist/types/tweaks/types.d.ts +278 -101
- package/package.json +27 -5
- package/src/tweaks/auto.ts +12 -0
- package/src/tweaks/clip.ts +94 -0
- package/src/tweaks/controls/basic.ts +403 -0
- package/src/tweaks/controls/bezier.ts +99 -0
- package/src/tweaks/controls/color.ts +412 -0
- package/src/tweaks/controls/gradient.ts +240 -0
- package/src/tweaks/controls/image.ts +299 -0
- package/src/tweaks/controls/interval.ts +104 -0
- package/src/tweaks/controls/monitor.ts +105 -0
- package/src/tweaks/controls/motion.ts +141 -0
- package/src/tweaks/controls/plot.ts +220 -0
- package/src/tweaks/controls/point.ts +119 -0
- package/src/tweaks/controls/shadow.ts +83 -0
- package/src/tweaks/controls/sides.ts +86 -0
- package/src/tweaks/controls/spring.ts +148 -0
- package/src/tweaks/controls/tabs.ts +47 -0
- package/src/tweaks/core.ts +9 -0
- package/src/tweaks/easing.ts +93 -0
- package/src/tweaks/enhance.ts +20 -0
- package/src/tweaks/feedback.ts +119 -0
- package/src/tweaks/fold.ts +21 -0
- package/src/tweaks/heavy.ts +302 -0
- package/src/tweaks/icons.ts +19 -0
- package/src/tweaks/image-store.ts +101 -0
- package/src/tweaks/lazy.ts +46 -0
- package/src/tweaks/markup.ts +154 -0
- package/src/tweaks/panel.ts +869 -0
- package/src/tweaks/schema.ts +189 -0
- package/src/tweaks/shared.ts +525 -0
- package/src/tweaks/single.ts +22 -0
- package/src/tweaks/standalone.ts +89 -0
- package/src/tweaks/types.ts +526 -0
- package/src/tweaks.css +959 -0
- package/src/wide-gamut.ts +310 -0
- package/dist/tweaks/bezier-2RABC6LD.js +0 -1
- package/dist/tweaks/chunk-4ATI34PB.js +0 -1
- package/dist/tweaks/chunk-7OSCMZ2J.js +0 -1
- package/dist/tweaks/chunk-W3JNIJ2F.js +0 -1
- package/dist/tweaks/clip-ECGCECEC.js +0 -1
- package/dist/tweaks/colour-2RE3FQ5E.js +0 -1
- package/dist/tweaks/gradient-DB6XTDHO.js +0 -1
- package/dist/tweaks/heavy-PKOMHB3C.js +0 -1
- package/dist/tweaks/image-7MCLBOZ3.js +0 -1
- package/dist/tweaks/interval-L2E4L3AZ.js +0 -1
- package/dist/tweaks/markup-ZJJ6CCWM.js +0 -1
- package/dist/tweaks/monitor-IEK742PG.js +0 -2
- package/dist/tweaks/motion-N5UNVR55.js +0 -1
- package/dist/tweaks/plot-VVTOY3RA.js +0 -2
- package/dist/tweaks/point-YMMHSLO7.js +0 -1
- package/dist/tweaks/shadow-IKYOV3VN.js +0 -1
- package/dist/tweaks/sides-FH227Z54.js +0 -1
- package/dist/tweaks/spring-3PKAFIH7.js +0 -1
- package/dist/tweaks/tabs-BKK5NIIY.js +0 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/* Standalone controls — the kit's controls without the panel: mountControl() builds one control
|
|
2
|
+
* into a host of your own, createColorPicker() hands out the color editor surface alone. */
|
|
3
|
+
import { el, getControl, onReady, setDisabled, valueChanged } from "./shared.js";
|
|
4
|
+
import { metaFor, VALUELESS } from "./schema.js";
|
|
5
|
+
import { ensureForMetas } from "./lazy.js";
|
|
6
|
+
import { createControl } from "./controls/basic.js";
|
|
7
|
+
import { addHintMarker } from "./feedback.js";
|
|
8
|
+
import type { SchemaValue, HeldValue, KnownType, MountOptions, MountedControl, ColorPickerOptions, ColorPicker, ColorMode, Control , Get } from "./types.js";
|
|
9
|
+
import type { Meta } from "./schema.js";
|
|
10
|
+
import type { PickerBody, PickerOptions } from "./controls/color.js";
|
|
11
|
+
|
|
12
|
+
// Outside a .tw-panel the cascade delivers none of the kit's tokens, so the wrapper carries .tw-portal.
|
|
13
|
+
const scope = (cls: string) => el("div", `${cls} tw-standalone tw-portal`);
|
|
14
|
+
// Run `build` once the lazy module has landed (synchronously when none is missing); the handled fork keeps a failed chunk out of the unhandled-rejection channel.
|
|
15
|
+
const whenLoaded = <H>(metas: Array<Pick<Meta, "type">>, build: () => void, handle: H): Promise<H> => {
|
|
16
|
+
const pend = ensureForMetas(metas);
|
|
17
|
+
if (!pend) { build(); return Promise.resolve(handle); }
|
|
18
|
+
const ready = pend.then(build).then(() => handle);
|
|
19
|
+
ready.catch(() => {});
|
|
20
|
+
return ready;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** One control from a schema value, into `host`; a folder or tabs set is tweaks()'s to build. */
|
|
24
|
+
export function mountControl<const V extends HeldValue>(host: Element, value: V & KnownType<V>, opts?: MountOptions): MountedControl;
|
|
25
|
+
export function mountControl(host: Element, value: SchemaValue, opts: MountOptions = {}): MountedControl {
|
|
26
|
+
const key = opts.key ?? "value";
|
|
27
|
+
const meta = metaFor(key, value);
|
|
28
|
+
if (!meta || meta.type === "folder" || meta.type === "tabs") throw new TypeError(`[tweaks] mountControl: "${key}" is not a single-control schema value`);
|
|
29
|
+
if (opts.label != null) meta.label = String(opts.label); // the option wins over the derived label; "" = no label
|
|
30
|
+
const valued = !VALUELESS.has(meta.type); // display/action controls carry no value: set/get are inert
|
|
31
|
+
const root = scope("tw-control"); // the control fills it in on ready
|
|
32
|
+
host.append(root);
|
|
33
|
+
let ctrl: Control | null = null, destroyed = false, last: unknown = meta.value, parked: unknown, queued = false;
|
|
34
|
+
// Gated the way the panel gates notify(): a same-value set() stays silent instead of echoing.
|
|
35
|
+
const emit = (v: unknown) => { if (!valueChanged(last, v)) return; last = v; opts.onChange && opts.onChange(v, key); };
|
|
36
|
+
const build = () => {
|
|
37
|
+
if (destroyed) return; // destroy() before the chunk landed — nothing to mount
|
|
38
|
+
ctrl = createControl(meta, valued ? emit : () => {});
|
|
39
|
+
if (!ctrl) return; // a failed chunk degrades to an empty wrapper
|
|
40
|
+
if (meta.hint) addHintMarker(ctrl.el, meta.hint);
|
|
41
|
+
// Resolved once at mount: with no siblings for a predicate to read, `get` answers undefined.
|
|
42
|
+
const none: Get = () => undefined;
|
|
43
|
+
try { if (meta.render && !meta.render(none)) ctrl.el.classList.add("tw-cond-hidden"); } catch {}
|
|
44
|
+
try { const d = typeof meta.disabled === "function" ? meta.disabled(none) : meta.disabled; if (d) setDisabled(ctrl.el, true); } catch {} // each predicate fails on its own
|
|
45
|
+
root.append(ctrl.el);
|
|
46
|
+
if (!valued) return;
|
|
47
|
+
last = ctrl.get(); // the form the control opened on
|
|
48
|
+
if (queued) { ctrl.set(parked); emit(ctrl.get()); } // a set() from the lazy window applies and announces now
|
|
49
|
+
};
|
|
50
|
+
const handle: MountedControl = {
|
|
51
|
+
el: root,
|
|
52
|
+
set: (v) => { if (destroyed || !valued) return; if (ctrl) { ctrl.set(v); emit(ctrl.get()); } else { parked = v; queued = true; } },
|
|
53
|
+
get: () => (!valued ? undefined : ctrl ? ctrl.get() : queued ? parked : meta.value),
|
|
54
|
+
destroy: () => { destroyed = true; if (ctrl && typeof ctrl.destroy === "function") ctrl.destroy(); root.dataset.twDestroyed = ""; root.remove(); }, // onLive listeners release on their next event by this mark
|
|
55
|
+
ready: null,
|
|
56
|
+
};
|
|
57
|
+
handle.ready = whenLoaded([meta], build, handle);
|
|
58
|
+
return handle;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The color editor surface alone, for a host page that brings its own swatch. */
|
|
62
|
+
export function createColorPicker(opts: ColorPickerOptions = {}): ColorPicker {
|
|
63
|
+
const root = scope("tw-color-picker");
|
|
64
|
+
let body: PickerBody | null = null, destroyed = false, last = opts.value, parked: string, queued = false, parkedMode: ColorMode = null;
|
|
65
|
+
const emit = (v: string) => { if (!valueChanged(last, v)) return; last = v; opts.onChange && opts.onChange(v); };
|
|
66
|
+
const build = () => {
|
|
67
|
+
if (destroyed) return;
|
|
68
|
+
const make = getControl<PickerBody, PickerOptions>("colorpicker"); // registered by the color module beside its control
|
|
69
|
+
if (!make) return;
|
|
70
|
+
body = make({ value: opts.value, mode: opts.mode }, emit);
|
|
71
|
+
last = body.get();
|
|
72
|
+
root.append(body.el);
|
|
73
|
+
if (parkedMode) body.setMode(parkedMode);
|
|
74
|
+
if (queued) { body.set(parked); emit(body.get()); }
|
|
75
|
+
onReady(body.reflow); // next frame: the thumbs need a real box
|
|
76
|
+
};
|
|
77
|
+
const picker: ColorPicker = {
|
|
78
|
+
el: root,
|
|
79
|
+
set: (v) => { if (destroyed) return; if (body) { body.set(v); emit(body.get()); } else { parked = v; queued = true; } },
|
|
80
|
+
get: () => (body ? body.get() : queued ? parked : opts.value),
|
|
81
|
+
mode: () => (body ? body.mode() : parkedMode || opts.mode || "oklch"),
|
|
82
|
+
setMode: (m) => { if (body) body.setMode(m); else parkedMode = m; },
|
|
83
|
+
reflow: () => { if (body) body.reflow(); },
|
|
84
|
+
destroy: () => { destroyed = true; root.remove(); },
|
|
85
|
+
ready: null,
|
|
86
|
+
};
|
|
87
|
+
picker.ready = whenLoaded([{ type: "color" }], build, picker);
|
|
88
|
+
return picker;
|
|
89
|
+
}
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/* Public types for the Tweakit API — the schema you hand to tweaks() and the
|
|
2
|
+
* panel you get back. These are what ship as .d.ts, so consumers get autocomplete on
|
|
3
|
+
* the schema shorthands + the verbose control forms + the panel methods. */
|
|
4
|
+
|
|
5
|
+
/** An option for a select / radio grid: a bare string or number, or `{ value, label }`; the param is the value as written. */
|
|
6
|
+
export type Option = string | number | { value: string | number; label?: string; color?: string }; // color: any CSS color, shown as a swatch beside a dropdown option (and the chosen one on the row)
|
|
7
|
+
|
|
8
|
+
/** A color stop in a gradient. */
|
|
9
|
+
export type GradientStop = { color: string; pos: number };
|
|
10
|
+
|
|
11
|
+
/** The CSS `<color-interpolation-method>` a gradient blends through — the space of the
|
|
12
|
+
* mode the stops were authored in. Template it as `linear-gradient(in <interpolation> …)`
|
|
13
|
+
* so your CSS ramp matches the editor's preview exactly. */
|
|
14
|
+
export type GradientInterpolation = "srgb" | "hsl" | "hwb" | "oklch" | "oklab" | "lch" | "lab" | "display-p3" | "rec2020";
|
|
15
|
+
|
|
16
|
+
/** What a sides control accepts: one number for all four; `[vertical, horizontal]`,
|
|
17
|
+
* `[top, horizontal, bottom]` or `[top, right, bottom, left]` (CSS order); a CSS shorthand
|
|
18
|
+
* string like `"1rem 2rem"` (its unit becomes the control's when `unit` isn't given; mixed
|
|
19
|
+
* units warn and read as px); or any subset of
|
|
20
|
+
* the four as an object. With `corners: true` the four are the corners from top-left round,
|
|
21
|
+
* the same CSS order, under the same keys, and they floor at 0 unless `min` says otherwise. */
|
|
22
|
+
export type SidesInput = number | readonly number[] | string | { top?: number; right?: number; bottom?: number; left?: number };
|
|
23
|
+
/** A sides control's value: the four numbers in CSS order plus the shorthand they collapse
|
|
24
|
+
* to (`css`, in the control's unit, `0` bare), so `el.style.padding = css`. */
|
|
25
|
+
export interface SidesValue { top: number; right: number; bottom: number; left: number; css: string }
|
|
26
|
+
|
|
27
|
+
/** What a shadow control accepts — any subset of its fields (px, `color` any color the kit
|
|
28
|
+
* parses); or, as `value`, a `box-shadow` string: optional `inset`, two to four px lengths,
|
|
29
|
+
* one color, one layer. */
|
|
30
|
+
export interface ShadowInput { inset?: boolean; x?: number; y?: number; blur?: number; spread?: number; color?: string }
|
|
31
|
+
/** A shadow control's value — the fields, plus the `box-shadow` CSS they resolve to (`css`),
|
|
32
|
+
* built from the numbers and the picker's own color notation, so `el.style.boxShadow = css`. */
|
|
33
|
+
export interface ShadowValue { inset: boolean; x: number; y: number; blur: number; spread: number; color: string; css: string }
|
|
34
|
+
/** What a motion control accepts — any subset of either mode's fields. A `curve` may be
|
|
35
|
+
* the four cubic-bezier numbers or a CSS keyword (`ease`, `ease-out`, …); `duration` is
|
|
36
|
+
* the easing's, in ms. Spring keys select the spring mode unless `mode` says otherwise. */
|
|
37
|
+
export interface MotionInput { mode?: "easing" | "spring"; curve?: readonly [number, number, number, number] | string; duration?: number; stiffness?: number; damping?: number; mass?: number; visualDuration?: number; bounce?: number }
|
|
38
|
+
/** A motion control's value: an easing curve with its duration, or a spring — each also
|
|
39
|
+
* carrying the CSS it resolves to, `duration` in ms and an `easing` string (`cubic-bezier(…)`,
|
|
40
|
+
* or a sampled `linear(…)` for the spring), so a host can write
|
|
41
|
+
* `transition: transform ${duration}ms ${easing}` in either mode. */
|
|
42
|
+
export type MotionValue =
|
|
43
|
+
| { mode: "easing"; curve: [number, number, number, number]; duration: number; easing: string }
|
|
44
|
+
| { mode: "spring"; stiffness: number; damping: number; mass: number; visualDuration?: number; bounce?: number; duration: number; easing: string };
|
|
45
|
+
|
|
46
|
+
/** How a gradient blends between neighboring stops: `"linear"` (CSS's own straight blend,
|
|
47
|
+
* the default) or a CSS easing — `ease`, `ease-in`, `ease-out`, `ease-in-out`, or any
|
|
48
|
+
* `cubic-bezier(x1, y1, x2, y2)`. No browser eases a gradient natively, so `gradientCss()`
|
|
49
|
+
* samples the curve into stops; template through it rather than by hand. */
|
|
50
|
+
export type GradientEasing = "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | `cubic-bezier(${string})`;
|
|
51
|
+
|
|
52
|
+
/** A gradient control's value — the stops, the blend space, and the easing between stops.
|
|
53
|
+
* Hand it to `gradientCss(value, angle?)` for the CSS, or `gradientStops(value)` for just
|
|
54
|
+
* the stop list (a conic / radial gradient). */
|
|
55
|
+
export interface GradientValue { stops: GradientStop[]; interpolation?: GradientInterpolation; easing?: GradientEasing }
|
|
56
|
+
|
|
57
|
+
/** Reader passed to a conditional `render` / `disabled` predicate — `get("siblingKey")`. */
|
|
58
|
+
export type Get = (key: string) => unknown;
|
|
59
|
+
|
|
60
|
+
/** Per-control extras that can ride on any object-form value. */
|
|
61
|
+
export interface ControlOptions {
|
|
62
|
+
/** The label shown for this control, in place of the title-cased key; `""` shows no label.
|
|
63
|
+
* Honored by every verbose `{ type }` form (in a plain folder object, `label` is a child). */
|
|
64
|
+
label?: string;
|
|
65
|
+
/** Show/hide this control based on other values. */
|
|
66
|
+
render?: (get: Get) => boolean;
|
|
67
|
+
/** Gray-out + lock this control (static, or derived from other values). */
|
|
68
|
+
disabled?: boolean | ((get: Get) => boolean);
|
|
69
|
+
/** A tooltip revealed by an ⓘ marker beside the label. */
|
|
70
|
+
hint?: string;
|
|
71
|
+
/** `false` leaves this control out of what `opts.persist` saves, so it opens on its schema
|
|
72
|
+
* value every load (presets, undo and `toJSON()` still carry it). On a control, not a
|
|
73
|
+
* plain folder object, where `persist` is a child. */
|
|
74
|
+
persist?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The verbose object forms a schema value can take: `{ type: "…", … }`. */
|
|
78
|
+
export type SchemaObject =
|
|
79
|
+
| { type: "slider"; value?: number; min?: number; max?: number; step?: number; soft?: boolean; unit?: string;
|
|
80
|
+
/** Draws the readout from the value ("7:30 AM" for minutes of the day), in place of its
|
|
81
|
+
* digits; `unit` still follows it. Display only, like `unit`: the param stays the number. */
|
|
82
|
+
format?: (value: number) => string;
|
|
83
|
+
/** Reads a typed readout back into a value, so the inline editor shows and takes the
|
|
84
|
+
* formatted text (without one, it edits the digits). Text it can't read (it throws, or
|
|
85
|
+
* returns anything but a finite number) leaves the value where it was. */
|
|
86
|
+
parse?: (text: string) => number }
|
|
87
|
+
| { type: "number"; value?: number; min?: number; max?: number; step?: number; soft?: boolean; unit?: string }
|
|
88
|
+
| { type: "checkbox"; value?: boolean }
|
|
89
|
+
| { type: "list"; options: readonly Option[]; value?: string | number }
|
|
90
|
+
| { type: "radiogrid" | "segmented"; options: readonly Option[]; value?: string | number; cols?: number }
|
|
91
|
+
| { type: "color"; value?: string }
|
|
92
|
+
| { type: "text"; value?: string; rows?: number; placeholder?: string }
|
|
93
|
+
| { type: "interval"; value?: readonly [number, number]; min?: number; max?: number; step?: number }
|
|
94
|
+
| { type: "spring"; mode?: "time" | "physics"; visualDuration?: number; bounce?: number; stiffness?: number; damping?: number; mass?: number; value?: { mode?: "time" | "physics"; visualDuration?: number; bounce?: number; stiffness?: number; damping?: number; mass?: number } }
|
|
95
|
+
| { type: "cubicbezier"; value?: readonly [number, number, number, number] }
|
|
96
|
+
| ({ type: "shadow"; value?: ShadowInput | string } & ShadowInput)
|
|
97
|
+
| { type: "sides"; value?: SidesInput; corners?: boolean; unit?: string; step?: number; min?: number; max?: number; top?: number; right?: number; bottom?: number; left?: number }
|
|
98
|
+
| ({ type: "motion"; value?: MotionInput } & MotionInput)
|
|
99
|
+
| { type: "point"; components: ReadonlyArray<{ key: string; label?: string; value?: number; min?: number; max?: number; step?: number }>; value?: { readonly [key: string]: number }; pad?: boolean; invertY?: boolean }
|
|
100
|
+
| { type: "point"; value: { readonly [key: string]: number }; components?: undefined; pad?: boolean; invertY?: boolean } // the value names its components
|
|
101
|
+
| { type: "gradient"; value?: GradientValue | ReadonlyArray<GradientStop | readonly [string, number]>; stops?: ReadonlyArray<GradientStop | readonly [string, number]>; interpolation?: GradientInterpolation; easing?: GradientEasing }
|
|
102
|
+
| { type: "plot"; expr?: string; fn?: (x: number) => number; xMin?: number; xMax?: number; yMin?: number; yMax?: number; samples?: number; editable?: boolean; min?: number; max?: number }
|
|
103
|
+
| { type: "fpsgraph" }
|
|
104
|
+
| { type: "monitor"; get?: () => number | string; value?: number | string; graph?: boolean; view?: "graph" | "text"; min?: number; max?: number; interval?: number; rows?: number; decimals?: number }
|
|
105
|
+
| { type: "image"; value?: string;
|
|
106
|
+
/** Supplied images to pick from, as URLs or `{ value, label }`. The control becomes a row naming the chosen
|
|
107
|
+
* image that opens a pane of tiles with the upload below them, and the param is the chosen URL or the upload's data URL. */
|
|
108
|
+
options?: ReadonlyArray<string | { value: string; label?: string }>;
|
|
109
|
+
/** Tiles per row in the gallery, 1 to 6 (by default one per option for two or three, 2 for one or four, then 3). */
|
|
110
|
+
cols?: number;
|
|
111
|
+
/** The largest upload taken, in bytes (10 MB unless set); a bigger file is refused with a note. */
|
|
112
|
+
maxSize?: number;
|
|
113
|
+
/** An upload longer than this on its long side, in px, is drawn down to it before it becomes the param, and one
|
|
114
|
+
* heavier than 1.5 MB is redrawn at its size, so a phone photo can't fill storage (2560 unless set; `false` takes
|
|
115
|
+
* uploads as they are). A JPEG comes out as a JPEG at
|
|
116
|
+
* `quality`; anything else as a PNG when that is 1.5 MB or less (a screenshot, not a photo), else as a JPEG, or a WebP
|
|
117
|
+
* when it has transparency. A GIF, an SVG, an animated WebP or APNG, a light picture within `fit`, or one the redraw wouldn't make smaller is kept as it is. */
|
|
118
|
+
fit?: number | false;
|
|
119
|
+
/** The JPEG or WebP quality of a shrunk upload, 0 to 1 (0.85 unless set). */
|
|
120
|
+
quality?: number;
|
|
121
|
+
/** `false` leaves the upload button out of a gallery: the options are the whole choice. */
|
|
122
|
+
upload?: boolean;
|
|
123
|
+
/** `false` leaves Clear out of a gallery, and Delete no longer empties it: for a choice that must keep a value. */
|
|
124
|
+
clear?: boolean }
|
|
125
|
+
| { type: "button"; action: () => void; label?: string }
|
|
126
|
+
| { type: "buttongroup"; buttons: Record<string, () => void> | ReadonlyArray<{ label: string; action: () => void }> }
|
|
127
|
+
| { type: "separator" }
|
|
128
|
+
| { type: "tabs"; pages: { readonly [title: string]: Schema } };
|
|
129
|
+
|
|
130
|
+
/** A schema value — a shorthand, a verbose `{ type }` object, or a nested folder. */
|
|
131
|
+
export type SchemaValue = SchemaValueOf<SchemaNest>;
|
|
132
|
+
// `F` is the nested-folder branch: strict for a schema annotated `Schema`, loose for what tweaks() infers from (HeldSchema).
|
|
133
|
+
type SchemaValueOf<F> =
|
|
134
|
+
| number // bare number → slider (0–1 for a value ≤ 1, else 0–3×value; a negative spans that either side of zero)
|
|
135
|
+
| boolean // → checkbox
|
|
136
|
+
| string // → text input; a hex or color-function string → color picker
|
|
137
|
+
| readonly [number] | readonly [number, number] | readonly [number, number, number] | readonly [number, number, number, number] // [value, min?, max?, step?] → slider
|
|
138
|
+
| readonly [readonly [number, number], number, number] | readonly [readonly [number, number], number, number, number] // [[lo,hi], min, max, step?] → interval
|
|
139
|
+
| readonly number[] | readonly (number | readonly number[])[] // either of the above, widened by a schema held in a variable
|
|
140
|
+
| readonly Option[] // a list of options → list (dropdown)
|
|
141
|
+
| { options: readonly Option[]; value?: string | number } // a list with a chosen default
|
|
142
|
+
| { action: () => void; label?: string } // → button
|
|
143
|
+
| (SchemaObject & ControlOptions) // verbose form (+ render/disabled/hint)
|
|
144
|
+
| F // nested object → folder
|
|
145
|
+
| SchemaFolder; // a nested folder with its own render / disabled predicate
|
|
146
|
+
|
|
147
|
+
/** The object you hand to `tweaks(name, schema, opts?)`. No value is a function, so a verbose
|
|
148
|
+
* form's callbacks (`format`, `fn`) take their parameter types from that form alone. */
|
|
149
|
+
export interface Schema { readonly [key: string]: SchemaValue; }
|
|
150
|
+
// A folder's string `type` names a control (a known name with none of its fields is still a folder), so a name no control
|
|
151
|
+
// has can't pass as a folder: `const s: Schema = { a: { type: "slidr", value: 1 } }` compiled that way.
|
|
152
|
+
type SchemaNest = Schema & { readonly type?: SchemaObject["type"] | Exclude<SchemaValue, string> };
|
|
153
|
+
/** What tweaks() and mountControl() infer from: `Schema` with a folder's `type` left open, since a schema held in a
|
|
154
|
+
* variable widens a verbose form's `type` to `string`; their own check then rules on the names it can still see. */
|
|
155
|
+
export interface HeldSchema { readonly [key: string]: HeldValue; }
|
|
156
|
+
/** A single value as mountControl() infers it; see `HeldSchema`. */
|
|
157
|
+
export type HeldValue = SchemaValueOf<HeldSchema>;
|
|
158
|
+
/** A nested folder that carries its own `render` / `disabled` predicate or `hint`: chrome, not
|
|
159
|
+
* children. Its index admits the predicate, so it gives up a child named `type`: that sets it
|
|
160
|
+
* apart from a verbose form, whose callbacks would otherwise be typed against the predicate too. */
|
|
161
|
+
export interface SchemaFolder {
|
|
162
|
+
readonly type?: undefined;
|
|
163
|
+
readonly render?: (get: Get) => boolean;
|
|
164
|
+
readonly disabled?: boolean | ((get: Get) => boolean);
|
|
165
|
+
readonly hint?: string;
|
|
166
|
+
readonly [key: string]: SchemaValue | ((get: Get) => boolean) | undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Params inferred from a schema ──
|
|
170
|
+
// tweaks() takes its schema as a const type parameter, so each value keeps its literal shape and
|
|
171
|
+
// these types read the control it builds the way schema.ts does, in the same order.
|
|
172
|
+
|
|
173
|
+
/** A spring control's value: the physics, plus the time pair and mode when tuned by feel. */
|
|
174
|
+
export interface SpringValue { stiffness: number; damping: number; mass: number; visualDuration?: number; bounce?: number; mode?: "time" | "physics" }
|
|
175
|
+
|
|
176
|
+
type OptionValue<O> = O extends string | number ? O : O extends { readonly value: infer V extends string | number } ? V : never;
|
|
177
|
+
// A list's param: one of its options, or a `value` off the list, which the control keeps as given until a pick.
|
|
178
|
+
type ChoiceValue<V> = (V extends { readonly options: readonly (infer O)[] } ? OptionValue<O> : never) | (V extends { readonly value: infer D extends string | number } ? D : never);
|
|
179
|
+
type Alnum = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
|
|
180
|
+
type Dashed<S extends string, Acc extends string = "", Run = false> =
|
|
181
|
+
S extends `${infer C}${infer R}` ? C extends Alnum ? Dashed<R, `${Acc}${C}`, false> : Run extends true ? Dashed<R, Acc, true> : Dashed<R, `${Acc}-`, true> : Acc;
|
|
182
|
+
type TrimDash<S extends string> = S extends `-${infer R}` ? TrimDash<R> : S extends `${infer R}-` ? TrimDash<R> : S;
|
|
183
|
+
/** A tab title's key under `params`: lower-case, each run of other characters a `-`, trimmed
|
|
184
|
+
* (`"Layout & Grid"` → `"layout-grid"`). Two titles that slug alike are told apart at runtime only. */
|
|
185
|
+
export type TabKey<T extends string> = string extends T ? string : TrimDash<Dashed<Lowercase<T>>> extends infer K extends string ? (K extends "" ? "tab" : K) : never;
|
|
186
|
+
// True when two page titles slug to one key: the runtime suffixes the later one (`a`, `a-2`) in an
|
|
187
|
+
// order a type can't follow, so those pages read as the loose Params rather than merge into one.
|
|
188
|
+
type TabClash<P> = true extends { [T in keyof P & string]: TabKey<T> extends { [U in Exclude<keyof P & string, T>]: TabKey<U> }[Exclude<keyof P & string, T>] ? true : false }[keyof P & string] ? true : false;
|
|
189
|
+
|
|
190
|
+
type Valueless = "button" | "buttongroup" | "separator" | "fpsgraph" | "monitor";
|
|
191
|
+
// The field each falsy-capable form hinges on (schema.ts REQUIRED): without it the object isn't
|
|
192
|
+
// that control but a shorthand or a folder holding a child named `type`.
|
|
193
|
+
type Hinge = { list: "options"; radiogrid: "options"; segmented: "options"; point: "components" | "value"; tabs: "pages" };
|
|
194
|
+
// A hinge field there but unusable skips the control at runtime, so it puts nothing on params.
|
|
195
|
+
type TypedValue<V> =
|
|
196
|
+
V extends { readonly type: "slider" | "number" } ? number
|
|
197
|
+
: V extends { readonly type: "checkbox" } ? boolean
|
|
198
|
+
: V extends { readonly type: "list" | "radiogrid" | "segmented" } ? (V extends { readonly options: readonly unknown[] } ? ChoiceValue<V> : never)
|
|
199
|
+
: V extends { readonly type: "color" | "text" | "image" | "plot" } ? string
|
|
200
|
+
: V extends { readonly type: "interval" } ? [number, number]
|
|
201
|
+
: V extends { readonly type: "cubicbezier" } ? [number, number, number, number]
|
|
202
|
+
: V extends { readonly type: "spring" } ? SpringValue
|
|
203
|
+
: V extends { readonly type: "motion" } ? MotionValue
|
|
204
|
+
: V extends { readonly type: "shadow" } ? ShadowValue
|
|
205
|
+
: V extends { readonly type: "sides" } ? SidesValue
|
|
206
|
+
: V extends { readonly type: "gradient" } ? GradientValue
|
|
207
|
+
: V extends { readonly type: "point" } ? (V extends { readonly components: readonly [] } ? never : V extends { readonly components: readonly (infer C)[] } ? { [K in C extends { readonly key: infer Key extends string } ? Key : never]: number }
|
|
208
|
+
: V extends { readonly value: infer P extends object } ? (keyof P extends never ? never : { -readonly [K in keyof P & string]: number }) : never)
|
|
209
|
+
: V extends { readonly type: "tabs" } ? (V extends { readonly pages: infer P extends object } ? (keyof P extends never ? never : TabClash<P> extends true ? Record<string, Params> : { [T in keyof P & string as TabKey<T>]: ParamsOf<P[T]> }) : never)
|
|
210
|
+
: unknown;
|
|
211
|
+
/** The value a schema entry puts on `params`. */
|
|
212
|
+
export type ValueOf<V> =
|
|
213
|
+
V extends { readonly type: infer T } ? (
|
|
214
|
+
string extends T ? any // a `type` widened by a schema held in a variable: the control can't be known
|
|
215
|
+
: T extends SchemaObject["type"] ? (T extends keyof Hinge ? ([Extract<Hinge[T], keyof V>] extends [never] ? ShorthandValue<V> : VerboseValue<V, T>) : VerboseValue<V, T>)
|
|
216
|
+
: ShorthandValue<V>)
|
|
217
|
+
: ShorthandValue<V>;
|
|
218
|
+
type VerboseValue<V, T> = T extends Valueless ? never : TypedValue<V>;
|
|
219
|
+
type ShorthandValue<V> =
|
|
220
|
+
V extends readonly [readonly [number, number], ...unknown[]] ? [number, number]
|
|
221
|
+
: V extends readonly [number] | readonly [number, number] | readonly [number, number, number] | readonly [number, number, number, number] ? number
|
|
222
|
+
: V extends readonly [number, ...number[]] ? V[number] // five numbers or more are a list of them
|
|
223
|
+
: V extends readonly [] ? undefined // an empty list, with no choice to hold
|
|
224
|
+
: V extends readonly number[] ? number // a tuple widened by a schema held in a variable: a slider, or a list of numbers
|
|
225
|
+
: V extends readonly (number | readonly number[])[] ? [number, number]
|
|
226
|
+
: V extends number ? number
|
|
227
|
+
: V extends boolean ? boolean
|
|
228
|
+
: V extends string ? string
|
|
229
|
+
: V extends readonly (infer O)[] ? OptionValue<O>
|
|
230
|
+
: V extends { readonly action: (...a: any[]) => any } ? never
|
|
231
|
+
: V extends { readonly options: readonly unknown[] } ? ChoiceValue<V>
|
|
232
|
+
: V extends object ? Entries<V, true>
|
|
233
|
+
: unknown;
|
|
234
|
+
// Keys the runtime skips at every level, since they would write through to Object.prototype.
|
|
235
|
+
type Reserved = "__proto__" | "constructor" | "prototype";
|
|
236
|
+
// A nested folder's render / disabled / hint are its chrome, not children; at the root and on a
|
|
237
|
+
// tab page they are controls like any other key.
|
|
238
|
+
type FolderChrome<V, K> = K extends "disabled" | "hint" ? true : K extends "render" ? (V extends (...a: any[]) => any ? true : false) : false;
|
|
239
|
+
type Entries<S, Nested extends boolean> = string extends keyof S ? Params
|
|
240
|
+
: { -readonly [K in keyof S & string as K extends Reserved ? never : ValueOf<S[K]> extends never ? never : Nested extends true ? (FolderChrome<S[K], K> extends true ? never : K) : K]: ValueOf<S[K]> };
|
|
241
|
+
/** The live values a schema builds, nested like the schema; a plain `Schema` gives the loose `Params`. */
|
|
242
|
+
export type ParamsOf<S> = Entries<S, false>;
|
|
243
|
+
|
|
244
|
+
// ── Type names checked at the call ──
|
|
245
|
+
// Schema's folder branch takes any object, so `{ type: "slidr", value: 1 }` fit it and compiled. tweaks() and mountControl()
|
|
246
|
+
// intersect their argument with these, which keep a known name, a widened `string` (a schema held in a variable: the
|
|
247
|
+
// runtime decides), and every shorthand as they are, and turn an unknown name into the list of known ones, so it fails
|
|
248
|
+
// right where it's written. A known name without its hinge field is a folder at runtime and isn't checked further; with it,
|
|
249
|
+
// or for a name with no hinge, the runtime builds that control, so its fields must fit the form: `{ type: "image",
|
|
250
|
+
// options: [1] }` read as a folder of a text and a slider otherwise, and compiled.
|
|
251
|
+
type CheckValue<V> =
|
|
252
|
+
V extends { readonly type: infer T } ? (string extends T ? V : T extends SchemaObject["type"] ? (V extends { readonly type: "tabs"; readonly pages: infer P } ? { readonly pages: { [K in keyof P]: KnownTypes<P[K]> } }
|
|
253
|
+
: T extends keyof Hinge ? ([Extract<Hinge[T], keyof V>] extends [never] ? unknown : FitsForm<V, T>) : FitsForm<V, T>) : { readonly type: NotAControlType })
|
|
254
|
+
: V extends readonly unknown[] | ((...a: any[]) => any) ? unknown
|
|
255
|
+
: V extends object ? KnownTypes<V>
|
|
256
|
+
: unknown;
|
|
257
|
+
// The verbose form a known name builds (an alias finds the member that lists it), and V held to it when it doesn't fit.
|
|
258
|
+
type FormOf<O, T> = O extends { readonly type: infer N } ? (T extends N ? O & ControlOptions : never) : never;
|
|
259
|
+
type FitsForm<V, T> = V extends FormOf<SchemaObject, T> ? unknown : FormOf<SchemaObject, T>;
|
|
260
|
+
// A brand, not the list of names: a literal intersected with the list collapses the whole entry to `never`, and the error
|
|
261
|
+
// read "not assignable to type 'never'" on every key. The brand keeps the entry and puts its text in the error.
|
|
262
|
+
type NotAControlType = { readonly "names no control: see the type list in AGENTS.md": true };
|
|
263
|
+
/** A schema whose every `type` names a control (`tweaks()` checks it; `{ type: "slidr" }` is an error). */
|
|
264
|
+
export type KnownTypes<S> = { [K in keyof S]: CheckValue<S[K]> };
|
|
265
|
+
/** One schema value whose `type` names a control (`mountControl()` checks it). */
|
|
266
|
+
export type KnownType<V> = CheckValue<V>;
|
|
267
|
+
|
|
268
|
+
/** A theme — token overrides applied over the default monochrome look. Every key is
|
|
269
|
+
* optional, so a partial theme only moves what it names; unset tokens keep their default.
|
|
270
|
+
* Keys are the `--tw-*` custom properties themselves. Apply at build via
|
|
271
|
+
* `tweaks(name, schema, { theme })`, or live via `panel.setTheme(theme)`. The kit's whole
|
|
272
|
+
* appearance runs on these custom properties — this is the public theming surface. */
|
|
273
|
+
export interface Theme {
|
|
274
|
+
/** Brand accent — slider fills, focus rings, active highlights (default: neutral, no accent). */
|
|
275
|
+
"--tw-accent"?: string;
|
|
276
|
+
/** Text drawn on the accent (active segment / radio label); the primary text tone unless set, so set it with a light accent. */
|
|
277
|
+
"--tw-on-accent"?: string;
|
|
278
|
+
/** Panel background, reused for recessed wells. */
|
|
279
|
+
"--tw-base"?: string;
|
|
280
|
+
/** Popover / dropdown background. */
|
|
281
|
+
"--tw-dropdown-bg"?: string;
|
|
282
|
+
/** Control surface and its hover / active steps. */
|
|
283
|
+
"--tw-surface"?: string; "--tw-surface-hover"?: string; "--tw-surface-active"?: string;
|
|
284
|
+
/** Hairline border and its hover step. */
|
|
285
|
+
"--tw-border"?: string; "--tw-border-hover"?: string;
|
|
286
|
+
/** Text-selection highlight wash. */
|
|
287
|
+
"--tw-selection"?: string;
|
|
288
|
+
/** Text tones — panel title, section heading, primary value text, control label, and the muted / faint / focus tones. */
|
|
289
|
+
"--tw-text-root"?: string; "--tw-text-section"?: string; "--tw-text-primary"?: string; "--tw-text-label"?: string;
|
|
290
|
+
"--tw-text-secondary"?: string; "--tw-text-tertiary"?: string; "--tw-text-focus"?: string;
|
|
291
|
+
/** Copy-confirmation accent, and the invalid-state accent (the plot's bad-expression cue). */
|
|
292
|
+
"--tw-success"?: string; "--tw-danger"?: string;
|
|
293
|
+
/** Popover shadow, the panel's container elevation, and its lifted (floating / dragging) variant. */
|
|
294
|
+
"--tw-shadow-dropdown"?: string; "--tw-shadow-panel"?: string; "--tw-shadow-panel-lifted"?: string;
|
|
295
|
+
/** Font stack, and the monospace stack (the plot's expression field). */
|
|
296
|
+
"--tw-font-sans"?: string; "--tw-font-mono"?: string;
|
|
297
|
+
/** Control corner radius, row height and the panel's width (256px by default), as CSS lengths. */
|
|
298
|
+
"--tw-radius"?: string; "--tw-row-height"?: string; "--tw-width"?: string;
|
|
299
|
+
/** Any other token. */
|
|
300
|
+
[token: `--${string}`]: string | undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Options for a programmatic write — `set()`, `setMany()`, `fromJSON()`. */
|
|
304
|
+
export interface SetOptions {
|
|
305
|
+
/** Apply without calling your `on()` listeners: for mirroring a value the host already
|
|
306
|
+
* acted on (another tab, its own state), so the change doesn't echo back. Conditional
|
|
307
|
+
* controls, persistence and undo still follow it: it's an undo step like any other. */
|
|
308
|
+
silent?: boolean;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Where a floating panel starts: `x` / `y` are its insets in px (16 by default) from the
|
|
312
|
+
* viewport's top-left. The far edge keeps a gap of `y` when the height caps; CSS
|
|
313
|
+
* `--tw-float-gap` overrides it. */
|
|
314
|
+
export interface FloatingPlacement { x?: number; y?: number }
|
|
315
|
+
|
|
316
|
+
/** Panel options (third arg to `tweaks()`). */
|
|
317
|
+
export interface TweaksOptions {
|
|
318
|
+
/** Theme overrides as `--tw-*` tokens; `null` / omitted = default monochrome. */
|
|
319
|
+
theme?: Theme | null;
|
|
320
|
+
/** Persist values to localStorage + enable presets — a key, or `true` to key by name. Uploaded images go to
|
|
321
|
+
* IndexedDB under the same key, and come back by `ready`. */
|
|
322
|
+
persist?: string | boolean;
|
|
323
|
+
/** With `persist`, `false` keeps saving values but leaves the Presets menu out of the
|
|
324
|
+
* toolbar (the presets API still works). On by default. */
|
|
325
|
+
presets?: boolean;
|
|
326
|
+
/** Start the panel already floated (fixed-positioned): `true` → 16px from the top-left, or a
|
|
327
|
+
* placement `{ x, y }`. Its height caps at the window and it scrolls, header and all. */
|
|
328
|
+
floating?: boolean | FloatingPlacement;
|
|
329
|
+
/** Drag the header to reposition the panel — on by default. An inline panel lifts into a
|
|
330
|
+
* floating layer on first drag; set `false` to pin it in place. */
|
|
331
|
+
draggable?: boolean;
|
|
332
|
+
/** Set `false` for a bare panel (no copy/reset/presets toolbar). */
|
|
333
|
+
toolbar?: boolean;
|
|
334
|
+
/** Enable debounced undo/redo (⌘Z / ⇧⌘Z while the panel is hovered/focused). */
|
|
335
|
+
undo?: boolean;
|
|
336
|
+
/** Custom reset handler (e.g. restore real app defaults + rebuild). */
|
|
337
|
+
onReset?: () => void;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** The live values bag, loosely typed — `params[key]` for each control. A schema literal passed
|
|
341
|
+
* to `tweaks()` gets its exact shape instead (`ParamsOf`). */
|
|
342
|
+
export type Params = Record<string, any>;
|
|
343
|
+
/** The keys `set()` and `setMany()` take: a typed panel's controls by dotted path (`size`, `card.size`), so a typo is a
|
|
344
|
+
* compile error; any string for a panel built from a plain `Schema`. An object-valued control's own fields read as
|
|
345
|
+
* paths too (`drop.blur`): the types can't tell its value from a folder, so they allow what the runtime refuses. Past ten
|
|
346
|
+
* folders deep the rest of a path is any string, since spelling every one out overran tsc's depth limit. */
|
|
347
|
+
export type PanelPath<P> = PathsOf<P, []>;
|
|
348
|
+
type PathsOf<P, D extends unknown[]> = string extends keyof P ? string : D["length"] extends 10 ? string
|
|
349
|
+
: { [K in keyof P & string]: K | (P[K] extends readonly unknown[] ? never : P[K] extends object ? `${K}.${PathsOf<P[K], [...D, 0]>}` : never) }[keyof P & string];
|
|
350
|
+
|
|
351
|
+
/** The whole-panel state `toJSON()` returns / `fromJSON()` accepts: every control value
|
|
352
|
+
* (nested to mirror folders/tabs) plus UI state — which folders are collapsed and which
|
|
353
|
+
* tab is active. JSON-safe; persist it however you like. Both `values` and `ui` are
|
|
354
|
+
* optional on the way in, so `fromJSON({ values })` restores values only and
|
|
355
|
+
* `fromJSON({ ui })` restores chrome only. (Panel collapse + floating position are not
|
|
356
|
+
* captured — position has its own `opts.persist` channel.) */
|
|
357
|
+
export interface PanelState {
|
|
358
|
+
/** Every control value, **nested** to mirror folders/tabs (e.g. `{ shadow: { radius: 16 } }`)
|
|
359
|
+
* — not the dotted form `set()`/`setMany()` take. `fromJSON` matches by that nested path and
|
|
360
|
+
* skips keys with no matching control. Normally you pass back what `toJSON()` produced. */
|
|
361
|
+
values?: Record<string, any>;
|
|
362
|
+
ui?: {
|
|
363
|
+
/** Folder collapse state → `true` when collapsed. Keys are the folder's path with each
|
|
364
|
+
* segment JSON-pointer-escaped (`.`→`~0`, `~`→`~1`) then joined by `.` — injective, so a
|
|
365
|
+
* literal dot in a key can't collide with the nesting separator. For plain keys that's
|
|
366
|
+
* the dotted path, so `fromJSON({ ui: { folders: { shadow: true } } })` starts one closed. */
|
|
367
|
+
folders?: Record<string, boolean>;
|
|
368
|
+
/** Active tab → the active page's key, under the tabs control's path (escaped as above). */
|
|
369
|
+
tabs?: Record<string, string | null>;
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** The panel object `tweaks()` returns; `P` is its values' shape. A bare `Panel` takes any panel, typed or not (its
|
|
374
|
+
* default is `any`, not `Params`: `set()`'s typed paths would make a typed panel unassignable to `Panel<Params>`). */
|
|
375
|
+
export interface Panel<P = any> {
|
|
376
|
+
/** The panel root element — append it to the DOM. */
|
|
377
|
+
el: HTMLElement;
|
|
378
|
+
/** Live values, updated in place as controls change. */
|
|
379
|
+
params: P;
|
|
380
|
+
/** Resolves once any lazily-loaded controls have hydrated and, for a panel that persists an image control, its
|
|
381
|
+
* stored images have loaded (resolves immediately when there's neither). */
|
|
382
|
+
ready: Promise<Panel<P>>;
|
|
383
|
+
/** Subscribe to changes; `last` is the changed control's dotted path (`size`, `card.size`), as `set()` and `changes()`
|
|
384
|
+
* name it, or undefined when many changed at once (a restore, `reset()`, `fromJSON`, `setMany`). */
|
|
385
|
+
on(fn: (params: P, last?: string) => void): () => void;
|
|
386
|
+
/** Set a control's value programmatically. Nested controls take a dotted path
|
|
387
|
+
* ("folder.child", "tabs.page.child"); at runtime a bare key also reaches a nested control
|
|
388
|
+
* when it's unambiguous, though a typed panel's types take the full path. A key matching
|
|
389
|
+
* no control, or a folder or tab, is refused with a console warning. */
|
|
390
|
+
set(key: PanelPath<P>, value: unknown, options?: SetOptions): void;
|
|
391
|
+
/** Set several controls at once — a flat map of keys (dotted for nested controls), e.g.
|
|
392
|
+
* `setMany({ "shadow.radius": 28, blur: 48 })`. Resolves each key like `set()`, then fires
|
|
393
|
+
* listeners + persists ONCE for the whole batch, where N separate `set()` calls would fire
|
|
394
|
+
* N times. Unknown / ambiguous / reserved keys warn and are skipped. */
|
|
395
|
+
setMany(values: { readonly [K in PanelPath<P>]?: unknown }, options?: SetOptions): void;
|
|
396
|
+
/** Reset every control to its default (or run `opts.onReset`). */
|
|
397
|
+
reset(): void;
|
|
398
|
+
/** Serialize the whole panel — every control value plus UI state (collapsed folders,
|
|
399
|
+
* active tabs) — to a plain JSON-safe object, independent of localStorage/presets. Hand
|
|
400
|
+
* it to your own storage: a file, a query string / share link, a server. `JSON.stringify`
|
|
401
|
+
* on the panel calls this too. Most meaningful once `ready` (lazy controls have built). */
|
|
402
|
+
toJSON(): PanelState;
|
|
403
|
+
/** The values that differ from their defaults, keyed by dotted path — `setMany()`'s shape
|
|
404
|
+
* (`{ "shadow.radius": 28, blur: 48 }`), the hand-off for baking tuned values into source.
|
|
405
|
+
* ⇧-click on the toolbar's copy button copies this instead of the full snapshot. Empty
|
|
406
|
+
* before `ready`. */
|
|
407
|
+
changes(): Record<string, unknown>;
|
|
408
|
+
/** Restore a panel from a `toJSON()` object: values are applied where their path still
|
|
409
|
+
* exists (missing ones skipped, like loading a preset), then folder/tab UI state. Fires
|
|
410
|
+
* listeners once. Both halves are optional — pass `{ values }` to restore values only, or
|
|
411
|
+
* `{ ui }` to restore chrome only. The value restore is undoable (one history step); the
|
|
412
|
+
* UI restore is intentionally outside undo, so a Cmd-Z reverts values without thrashing
|
|
413
|
+
* folders/tabs. */
|
|
414
|
+
fromJSON(state: PanelState, options?: SetOptions): void;
|
|
415
|
+
/** Re-theme the panel live; `null` reverts to the default. */
|
|
416
|
+
setTheme(theme?: Theme | null): void;
|
|
417
|
+
/** Retitle the panel live — the header follows. The name it was built
|
|
418
|
+
* with stays its storage key (`persist: true`), so saved values and presets keep their
|
|
419
|
+
* home across a rename. */
|
|
420
|
+
setName(name: string): void;
|
|
421
|
+
/** Save the current values as a named preset (needs `opts.persist`). False when there is nowhere to save it, or storage refused it. */
|
|
422
|
+
savePreset(name: string): boolean;
|
|
423
|
+
/** Load a named preset. */
|
|
424
|
+
loadPreset(name: string): boolean;
|
|
425
|
+
/** Delete a named preset. */
|
|
426
|
+
deletePreset(name: string): void;
|
|
427
|
+
/** List saved preset names. */
|
|
428
|
+
presets(): string[];
|
|
429
|
+
/** Undo / redo (need `opts.undo`). */
|
|
430
|
+
undo(): void;
|
|
431
|
+
redo(): void;
|
|
432
|
+
/** Tear the panel down — remove it from the DOM, release every global listener, close any open popover; the API goes inert. */
|
|
433
|
+
destroy(): void;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** A built control's handle (internal-ish, but used across modules). */
|
|
437
|
+
export interface Control {
|
|
438
|
+
el: HTMLElement;
|
|
439
|
+
set(value: unknown): void;
|
|
440
|
+
get(): unknown;
|
|
441
|
+
/** Release any loop the control owns (the monitor's poll, the FPS graph's rAF). Only
|
|
442
|
+
* the live controls supply it; the panel calls it from `destroy()`. */
|
|
443
|
+
destroy?(): void;
|
|
444
|
+
/** Show a short message on the control for a moment. Only the image control supplies it; the panel calls it when
|
|
445
|
+
* storage refuses the control's value. */
|
|
446
|
+
note?(message: string): void;
|
|
447
|
+
/** The hard range set() fits a number into, as the control resolved it (derived bounds too); a point's per component.
|
|
448
|
+
* Only the numeric controls supply it; the panel's clamp notice reads it. */
|
|
449
|
+
range?: [number, number] | Record<string, [number, number]>;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Options for `mountControl(host, value, opts?)` — one control, no panel. */
|
|
453
|
+
export interface MountOptions {
|
|
454
|
+
/** The key passed to `onChange` and used to derive the label when none is given (default `"value"`). */
|
|
455
|
+
key?: string;
|
|
456
|
+
/** The label shown on the control; `""` shows none (the host page already has one). Wins over
|
|
457
|
+
* the title-cased key and over a verbose form's own `label`. */
|
|
458
|
+
label?: string;
|
|
459
|
+
/** Fired with the new value whenever the control changes — by hand or through `set()`. */
|
|
460
|
+
onChange?: (value: unknown, key: string) => void;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** The handle `mountControl()` returns. */
|
|
464
|
+
export interface MountedControl {
|
|
465
|
+
/** The wrapper (`.tw-control`) the control lives in, already appended to the host and
|
|
466
|
+
* stable from creation — it carries the kit's tokens, so the control renders outside a
|
|
467
|
+
* panel. On the code-split build a lazily-loaded control fills it in once `ready` resolves. */
|
|
468
|
+
el: HTMLElement;
|
|
469
|
+
/** Apply a value — the control updates AND `onChange` fires (when the value actually
|
|
470
|
+
* changed). Before `ready`, the value is queued and applied once the control builds. */
|
|
471
|
+
set(value: unknown): void;
|
|
472
|
+
/** The current value (the queued or schema value before `ready`; `undefined` for a
|
|
473
|
+
* valueless control such as a button). */
|
|
474
|
+
get(): unknown;
|
|
475
|
+
/** Remove the control from the host and release anything it owns. */
|
|
476
|
+
destroy(): void;
|
|
477
|
+
/** Resolves with this handle once the control has built — immediately on the single-file build. */
|
|
478
|
+
ready: Promise<MountedControl>;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** The notations the color picker edits in (its mode dropdown). The emitted value follows
|
|
482
|
+
* the mode: `hex` gives `#RRGGBB`, the wide-gamut modes give a CSS color like `oklch(…)`. */
|
|
483
|
+
export type ColorMode = "hex" | "srgb" | "css" | "hsl" | "hwb" | "oklch" | "oklab" | "lch" | "lab" | "p3" | "rec2020";
|
|
484
|
+
|
|
485
|
+
/** Options for `createColorPicker(opts?)` — the picker body alone, no trigger row. */
|
|
486
|
+
export interface ColorPickerOptions {
|
|
487
|
+
/** The color it opens on — any CSS color string (default `#7c5cff`). */
|
|
488
|
+
value?: string;
|
|
489
|
+
/** The notation it opens in (default `oklch`). */
|
|
490
|
+
mode?: ColorMode;
|
|
491
|
+
/** Fired with the new color string whenever it changes — by hand or through `set()`. */
|
|
492
|
+
onChange?: (value: string) => void;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** The handle `createColorPicker()` returns. Bring your own trigger: append `el` to a popover
|
|
496
|
+
* or a panel of your own; the kit owns the editor surface and the gamut math. */
|
|
497
|
+
export interface ColorPicker {
|
|
498
|
+
/** The picker's root (`.tw-color-picker`), stable from creation; it carries the kit's tokens,
|
|
499
|
+
* so the picker renders anywhere. On the code-split build the editor fills it in once the
|
|
500
|
+
* color chunk has loaded. */
|
|
501
|
+
el: HTMLElement;
|
|
502
|
+
/** Apply a color — the picker updates AND `onChange` fires (when the color actually changed). */
|
|
503
|
+
set(value: string): void;
|
|
504
|
+
/** The current color, serialized in the picker's mode. */
|
|
505
|
+
get(): string;
|
|
506
|
+
/** The current notation. */
|
|
507
|
+
mode(): ColorMode;
|
|
508
|
+
/** Switch notation — formatting only, nothing fires. */
|
|
509
|
+
setMode(mode: ColorMode): void;
|
|
510
|
+
/** Re-rasterize and re-place the thumbs. It reflows itself a frame after creation; call
|
|
511
|
+
* this when you show it later (a popover opening) or after it resizes. */
|
|
512
|
+
reflow(): void;
|
|
513
|
+
/** Remove the picker from the DOM. */
|
|
514
|
+
destroy(): void;
|
|
515
|
+
/** Resolves with this handle once the editor has built — immediately on the single-file build. */
|
|
516
|
+
ready: Promise<ColorPicker>;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** `detail` of the `tw:change` event a `[data-tw]` host dispatches (bubbling) whenever its
|
|
520
|
+
* control changes — `key` is the host's `data-key` (or its label), `value` the new value,
|
|
521
|
+
* also written to the host's `data-value`. */
|
|
522
|
+
export interface TwChangeDetail { key: string; value: unknown; }
|
|
523
|
+
|
|
524
|
+
declare global {
|
|
525
|
+
interface HTMLElementEventMap { "tw:change": CustomEvent<TwChangeDetail>; }
|
|
526
|
+
}
|