tweakit 0.5.2 → 0.7.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.
Files changed (120) hide show
  1. package/AGENTS.md +32 -30
  2. package/README.md +55 -29
  3. package/THIRD-PARTY-NOTICES.md +0 -1
  4. package/dist/tweaks/auto.js +2 -0
  5. package/dist/tweaks/auto.js.map +6 -0
  6. package/dist/tweaks/bezier-S2BOXQBM.js +2 -0
  7. package/dist/tweaks/bezier-S2BOXQBM.js.map +6 -0
  8. package/dist/tweaks/chunk-3MZAAMC2.js +2 -0
  9. package/dist/tweaks/chunk-3MZAAMC2.js.map +6 -0
  10. package/dist/tweaks/chunk-ETB35KFV.js +2 -0
  11. package/dist/tweaks/chunk-ETB35KFV.js.map +6 -0
  12. package/dist/tweaks/chunk-YB5GKWZI.js +2 -0
  13. package/dist/tweaks/chunk-YB5GKWZI.js.map +6 -0
  14. package/dist/tweaks/clip-A3VI53BK.js +2 -0
  15. package/dist/tweaks/clip-A3VI53BK.js.map +6 -0
  16. package/dist/tweaks/color-UGYMAF3W.js +2 -0
  17. package/dist/tweaks/color-UGYMAF3W.js.map +6 -0
  18. package/dist/tweaks/core.js +2 -1
  19. package/dist/tweaks/core.js.map +6 -0
  20. package/dist/tweaks/fold-GY47HIQA.js +2 -0
  21. package/dist/tweaks/fold-GY47HIQA.js.map +6 -0
  22. package/dist/tweaks/gradient-BTX25WSH.js +2 -0
  23. package/dist/tweaks/gradient-BTX25WSH.js.map +6 -0
  24. package/dist/tweaks/heavy-NPFIGHRM.js +2 -0
  25. package/dist/tweaks/heavy-NPFIGHRM.js.map +6 -0
  26. package/dist/tweaks/image-HTQQ4WX6.js +4 -0
  27. package/dist/tweaks/image-HTQQ4WX6.js.map +6 -0
  28. package/dist/tweaks/image-store-QD5SMZXG.js +2 -0
  29. package/dist/tweaks/image-store-QD5SMZXG.js.map +6 -0
  30. package/dist/tweaks/interval-BHPI4LRN.js +2 -0
  31. package/dist/tweaks/interval-BHPI4LRN.js.map +6 -0
  32. package/dist/tweaks/markup-QM53TQJZ.js +2 -0
  33. package/dist/tweaks/markup-QM53TQJZ.js.map +6 -0
  34. package/dist/tweaks/monitor-722WIYGW.js +3 -0
  35. package/dist/tweaks/monitor-722WIYGW.js.map +6 -0
  36. package/dist/tweaks/motion-HMCZK6HJ.js +2 -0
  37. package/dist/tweaks/motion-HMCZK6HJ.js.map +6 -0
  38. package/dist/tweaks/plot-D3NQ6XNN.js +3 -0
  39. package/dist/tweaks/plot-D3NQ6XNN.js.map +6 -0
  40. package/dist/tweaks/point-XDXKMDXH.js +2 -0
  41. package/dist/tweaks/point-XDXKMDXH.js.map +6 -0
  42. package/dist/tweaks/presets-EUWNSCUW.js +2 -0
  43. package/dist/tweaks/presets-EUWNSCUW.js.map +6 -0
  44. package/dist/tweaks/shadow-AM7VSRNS.js +2 -0
  45. package/dist/tweaks/shadow-AM7VSRNS.js.map +6 -0
  46. package/dist/tweaks/shadow-root-RYUV3AGS.js +3 -0
  47. package/dist/tweaks/shadow-root-RYUV3AGS.js.map +6 -0
  48. package/dist/tweaks/sides-JWIURDA4.js +2 -0
  49. package/dist/tweaks/sides-JWIURDA4.js.map +6 -0
  50. package/dist/tweaks/spring-BF5GBTPM.js +2 -0
  51. package/dist/tweaks/spring-BF5GBTPM.js.map +6 -0
  52. package/dist/tweaks/tabs-PRVUUZXZ.js +2 -0
  53. package/dist/tweaks/tabs-PRVUUZXZ.js.map +6 -0
  54. package/dist/tweaks.css +1 -1
  55. package/dist/tweaks.js +7 -3
  56. package/dist/tweaks.js.map +6 -0
  57. package/dist/types/tweaks/easing.d.ts +8 -11
  58. package/dist/types/tweaks/enhance.d.ts +3 -2
  59. package/dist/types/tweaks/panel.d.ts +6 -4
  60. package/dist/types/tweaks/schema.d.ts +4 -8
  61. package/dist/types/tweaks/standalone.d.ts +3 -3
  62. package/dist/types/tweaks/types.d.ts +499 -116
  63. package/package.json +30 -5
  64. package/src/tweaks/auto.ts +16 -0
  65. package/src/tweaks/clip.ts +156 -0
  66. package/src/tweaks/controls/basic.ts +727 -0
  67. package/src/tweaks/controls/bezier.ts +204 -0
  68. package/src/tweaks/controls/color.ts +861 -0
  69. package/src/tweaks/controls/gradient.ts +419 -0
  70. package/src/tweaks/controls/image.ts +555 -0
  71. package/src/tweaks/controls/interval.ts +189 -0
  72. package/src/tweaks/controls/monitor.ts +211 -0
  73. package/src/tweaks/controls/motion.ts +316 -0
  74. package/src/tweaks/controls/plot.ts +440 -0
  75. package/src/tweaks/controls/point.ts +208 -0
  76. package/src/tweaks/controls/shadow.ts +156 -0
  77. package/src/tweaks/controls/sides.ts +196 -0
  78. package/src/tweaks/controls/spring.ts +308 -0
  79. package/src/tweaks/controls/tabs.ts +68 -0
  80. package/src/tweaks/core.ts +9 -0
  81. package/src/tweaks/easing.ts +118 -0
  82. package/src/tweaks/enhance.ts +35 -0
  83. package/src/tweaks/feedback.ts +218 -0
  84. package/src/tweaks/fold.ts +33 -0
  85. package/src/tweaks/heavy.ts +626 -0
  86. package/src/tweaks/icons.ts +26 -0
  87. package/src/tweaks/image-store.ts +189 -0
  88. package/src/tweaks/lazy.ts +63 -0
  89. package/src/tweaks/markup.ts +317 -0
  90. package/src/tweaks/panel.ts +1693 -0
  91. package/src/tweaks/presets.ts +85 -0
  92. package/src/tweaks/schema.ts +351 -0
  93. package/src/tweaks/shadow-root.ts +178 -0
  94. package/src/tweaks/shared.ts +1137 -0
  95. package/src/tweaks/single.ts +22 -0
  96. package/src/tweaks/standalone.ts +179 -0
  97. package/src/tweaks/types.ts +1027 -0
  98. package/src/tweaks/undo.ts +104 -0
  99. package/src/tweaks.css +965 -0
  100. package/src/wide-gamut.ts +658 -0
  101. package/dist/tweaks/bezier-2RABC6LD.js +0 -1
  102. package/dist/tweaks/chunk-4ATI34PB.js +0 -1
  103. package/dist/tweaks/chunk-7OSCMZ2J.js +0 -1
  104. package/dist/tweaks/chunk-W3JNIJ2F.js +0 -1
  105. package/dist/tweaks/clip-ECGCECEC.js +0 -1
  106. package/dist/tweaks/colour-2RE3FQ5E.js +0 -1
  107. package/dist/tweaks/fold-UGN7TXGX.js +0 -1
  108. package/dist/tweaks/gradient-DB6XTDHO.js +0 -1
  109. package/dist/tweaks/heavy-PKOMHB3C.js +0 -1
  110. package/dist/tweaks/image-7MCLBOZ3.js +0 -1
  111. package/dist/tweaks/interval-L2E4L3AZ.js +0 -1
  112. package/dist/tweaks/markup-ZJJ6CCWM.js +0 -1
  113. package/dist/tweaks/monitor-IEK742PG.js +0 -2
  114. package/dist/tweaks/motion-N5UNVR55.js +0 -1
  115. package/dist/tweaks/plot-VVTOY3RA.js +0 -2
  116. package/dist/tweaks/point-YMMHSLO7.js +0 -1
  117. package/dist/tweaks/shadow-IKYOV3VN.js +0 -1
  118. package/dist/tweaks/sides-FH227Z54.js +0 -1
  119. package/dist/tweaks/spring-3PKAFIH7.js +0 -1
  120. package/dist/tweaks/tabs-BKK5NIIY.js +0 -1
@@ -0,0 +1,1027 @@
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
6
+ * written. */
7
+ // color: any CSS color, shown as a swatch beside a dropdown option (and the chosen one on the row)
8
+ export type Option = string | number | { value: string | number; label?: string; color?: string };
9
+
10
+ /** A color stop in a gradient. */
11
+ export type GradientStop = { color: string; pos: number };
12
+
13
+ /** The CSS `<color-interpolation-method>` a gradient blends through — the space of the
14
+ * mode the stops were authored in. Template it as `linear-gradient(in <interpolation> …)`
15
+ * so your CSS ramp matches the editor's preview exactly. */
16
+ export type GradientInterpolation =
17
+ "srgb" | "hsl" | "hwb" | "oklch" | "oklab" | "lch" | "lab" | "display-p3" | "rec2020";
18
+
19
+ /** What a sides control accepts: one number for all four; `[vertical, horizontal]`,
20
+ * `[top, horizontal, bottom]` or `[top, right, bottom, left]` (CSS order); a CSS shorthand
21
+ * string like `"1rem 2rem"` (its unit becomes the control's when `unit` isn't given; mixed
22
+ * units warn and read as px); or any subset of
23
+ * the four as an object. With `corners: true` the four are the corners from top-left round,
24
+ * the same CSS order, under the same keys, and they floor at 0 unless `min` says otherwise. */
25
+ export type SidesInput =
26
+ number | readonly number[] | string | { top?: number; right?: number; bottom?: number; left?: number };
27
+ /** A sides control's value: the four numbers in CSS order plus the shorthand they collapse
28
+ * to (`css`, in the control's unit, `0` bare), so `el.style.padding = css`. */
29
+ export interface SidesValue {
30
+ top: number;
31
+ right: number;
32
+ bottom: number;
33
+ left: number;
34
+ css: string;
35
+ }
36
+
37
+ /** What a shadow control accepts — any subset of its fields (px, `color` any color the kit
38
+ * parses); or, as `value`, a `box-shadow` string: optional `inset`, two to four px lengths,
39
+ * one color, one layer. */
40
+ export interface ShadowInput {
41
+ inset?: boolean;
42
+ x?: number;
43
+ y?: number;
44
+ blur?: number;
45
+ spread?: number;
46
+ color?: string;
47
+ }
48
+ /** A shadow control's value — the fields, plus the `box-shadow` CSS they resolve to (`css`),
49
+ * built from the numbers and the picker's own color notation, so `el.style.boxShadow = css`. */
50
+ export interface ShadowValue {
51
+ inset: boolean;
52
+ x: number;
53
+ y: number;
54
+ blur: number;
55
+ spread: number;
56
+ color: string;
57
+ css: string;
58
+ }
59
+ /** What a motion control accepts — any subset of either mode's fields. A `curve` may be
60
+ * the four cubic-bezier numbers or a CSS keyword (`ease`, `ease-out`, …); `duration` is
61
+ * the easing's, in ms. Spring keys select the spring mode unless `mode` says otherwise. */
62
+ export interface MotionInput {
63
+ mode?: "easing" | "spring";
64
+ curve?: readonly [number, number, number, number] | string;
65
+ duration?: number;
66
+ stiffness?: number;
67
+ damping?: number;
68
+ mass?: number;
69
+ visualDuration?: number;
70
+ bounce?: number;
71
+ }
72
+ /** A motion control's value: an easing curve with its duration, or a spring — each also
73
+ * carrying the CSS it resolves to, `duration` in ms and an `easing` string (`cubic-bezier(…)`,
74
+ * or a sampled `linear(…)` for the spring), so a host can write
75
+ * `transition: transform ${duration}ms ${easing}` in either mode. */
76
+ export type MotionValue =
77
+ | { mode: "easing"; curve: [number, number, number, number]; duration: number; easing: string }
78
+ | {
79
+ mode: "spring";
80
+ stiffness: number;
81
+ damping: number;
82
+ mass: number;
83
+ visualDuration?: number;
84
+ bounce?: number;
85
+ duration: number;
86
+ easing: string;
87
+ };
88
+
89
+ /** How a gradient blends between neighboring stops: `"linear"` (CSS's own straight blend,
90
+ * the default) or a CSS easing — `ease`, `ease-in`, `ease-out`, `ease-in-out`, or any
91
+ * `cubic-bezier(x1, y1, x2, y2)`. No browser eases a gradient natively, so `gradientCss()`
92
+ * samples the curve into stops; template through it rather than by hand. */
93
+ export type GradientEasing = "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | `cubic-bezier(${string})`;
94
+
95
+ /** A gradient control's value — the stops, the blend space, and the easing between stops.
96
+ * Hand it to `gradientCss(value, angle?)` for the CSS, or `gradientStops(value)` for just
97
+ * the stop list (a conic / radial gradient). */
98
+ export interface GradientValue {
99
+ stops: GradientStop[];
100
+ interpolation?: GradientInterpolation;
101
+ easing?: GradientEasing;
102
+ }
103
+
104
+ /** Reader passed to a conditional `render` / `disabled` predicate — `get("siblingKey")`. */
105
+ export type Get = (key: string) => unknown;
106
+
107
+ /** Per-control extras that can ride on any object-form value. */
108
+ export interface ControlOptions {
109
+ /** The label shown for this control, in place of the title-cased key; `""` shows no label.
110
+ * Honored by every verbose `{ type }` form (in a plain folder object, `label` is a child). */
111
+ label?: string;
112
+ /** Show/hide this control based on other values. */
113
+ render?: (get: Get) => boolean;
114
+ /** Gray-out + lock this control (static, or derived from other values). */
115
+ disabled?: boolean | ((get: Get) => boolean);
116
+ /** A tooltip revealed by an ⓘ marker beside the label. */
117
+ hint?: string;
118
+ /** `false` leaves this control out of what `opts.persist` saves, so it opens on its schema
119
+ * value every load (presets, undo and `toJSON()` still carry it). On a control, not a
120
+ * plain folder object, where `persist` is a child. */
121
+ persist?: boolean;
122
+ }
123
+
124
+ /** The verbose object forms a schema value can take: `{ type: "…", … }`. */
125
+ export type SchemaObject =
126
+ | {
127
+ type: "slider";
128
+ value?: number;
129
+ min?: number;
130
+ max?: number;
131
+ step?: number;
132
+ soft?: boolean;
133
+ unit?: string;
134
+ /** Draws the readout from the value ("7:30 AM" for minutes of the day), in place of its
135
+ * digits; `unit` still follows it. Display only, like `unit`: the param stays the number. */
136
+ format?: (value: number) => string;
137
+ /** Reads a typed readout back into a value, so the inline editor shows and takes the
138
+ * formatted text (without one, it edits the digits). Text it can't read (it throws, or
139
+ * returns anything but a finite number) leaves the value where it was. */
140
+ parse?: (text: string) => number;
141
+ }
142
+ | { type: "number"; value?: number; min?: number; max?: number; step?: number; soft?: boolean; unit?: string }
143
+ | { type: "checkbox"; value?: boolean }
144
+ | { type: "list"; options: readonly Option[]; value?: string | number }
145
+ | { type: "radiogrid" | "segmented"; options: readonly Option[]; value?: string | number; cols?: number }
146
+ | { type: "color"; value?: string }
147
+ | { type: "text"; value?: string; rows?: number; placeholder?: string }
148
+ | { type: "interval"; value?: readonly [number, number]; min?: number; max?: number; step?: number }
149
+ | {
150
+ type: "spring";
151
+ mode?: "time" | "physics";
152
+ visualDuration?: number;
153
+ bounce?: number;
154
+ stiffness?: number;
155
+ damping?: number;
156
+ mass?: number;
157
+ value?: {
158
+ mode?: "time" | "physics";
159
+ visualDuration?: number;
160
+ bounce?: number;
161
+ stiffness?: number;
162
+ damping?: number;
163
+ mass?: number;
164
+ };
165
+ }
166
+ | { type: "cubicbezier"; value?: readonly [number, number, number, number] }
167
+ | ({ type: "shadow"; value?: ShadowInput | string } & ShadowInput)
168
+ | {
169
+ type: "sides";
170
+ value?: SidesInput;
171
+ corners?: boolean;
172
+ unit?: string;
173
+ step?: number;
174
+ min?: number;
175
+ max?: number;
176
+ top?: number;
177
+ right?: number;
178
+ bottom?: number;
179
+ left?: number;
180
+ }
181
+ | ({ type: "motion"; value?: MotionInput } & MotionInput)
182
+ | {
183
+ type: "point";
184
+ components: ReadonlyArray<{
185
+ key: string;
186
+ label?: string;
187
+ value?: number;
188
+ min?: number;
189
+ max?: number;
190
+ step?: number;
191
+ }>;
192
+ value?: { readonly [key: string]: number };
193
+ pad?: boolean;
194
+ invertY?: boolean;
195
+ }
196
+ // the value names its components
197
+ | {
198
+ type: "point";
199
+ value: { readonly [key: string]: number };
200
+ components?: undefined;
201
+ pad?: boolean;
202
+ invertY?: boolean;
203
+ }
204
+ | {
205
+ type: "gradient";
206
+ value?: GradientValue | ReadonlyArray<GradientStop | readonly [string, number]>;
207
+ stops?: ReadonlyArray<GradientStop | readonly [string, number]>;
208
+ interpolation?: GradientInterpolation;
209
+ easing?: GradientEasing;
210
+ }
211
+ | {
212
+ type: "plot";
213
+ expr?: string;
214
+ fn?: (x: number) => number;
215
+ xMin?: number;
216
+ xMax?: number;
217
+ yMin?: number;
218
+ yMax?: number;
219
+ samples?: number;
220
+ editable?: boolean;
221
+ min?: number;
222
+ max?: number;
223
+ }
224
+ | { type: "fpsgraph" }
225
+ | {
226
+ type: "monitor";
227
+ get?: () => number | string;
228
+ value?: number | string;
229
+ graph?: boolean;
230
+ view?: "graph" | "text";
231
+ min?: number;
232
+ max?: number;
233
+ interval?: number;
234
+ rows?: number;
235
+ decimals?: number;
236
+ }
237
+ | {
238
+ type: "image";
239
+ value?: string;
240
+ /** Supplied images to pick from, as URLs or `{ value, label }`. The control becomes a row naming the chosen
241
+ * image that opens a pane of tiles with the upload below them, and the param is the chosen URL or the upload's
242
+ * data URL. */
243
+ options?: ReadonlyArray<string | { value: string; label?: string }>;
244
+ /** Tiles per row in the gallery, 1 to 6 (by default one per option for two or three, 2 for one or four, then
245
+ * 3). */
246
+ cols?: number;
247
+ /** The largest upload taken, in bytes (10 MB unless set); a bigger file is refused with a note. */
248
+ maxSize?: number;
249
+ /** An upload longer than this on its long side, in px, is drawn down to it before it becomes the param, and one
250
+ * heavier than 1.5 MB is redrawn at its size, so a phone photo can't fill storage (2560 unless set; `false`
251
+ * takes uploads as they are). A JPEG comes out as a JPEG at
252
+ * `quality`; anything else as a PNG when that is 1.5 MB or less (a screenshot, not a photo), else as a JPEG, or
253
+ * a WebP when it has transparency. A GIF, an SVG, an animated WebP or APNG, a light picture within `fit`, or one
254
+ * the redraw wouldn't make smaller is kept as it is. */
255
+ fit?: number | false;
256
+ /** The JPEG or WebP quality of a shrunk upload, 0 to 1 (0.85 unless set). */
257
+ quality?: number;
258
+ /** `false` leaves the upload button out of a gallery: the options are the whole choice. */
259
+ upload?: boolean;
260
+ /** `false` leaves Clear out of a gallery, and Delete no longer empties it: for a choice that must keep a
261
+ * value. */
262
+ clear?: boolean;
263
+ }
264
+ | { type: "button"; action: () => void; label?: string }
265
+ | { type: "buttongroup"; buttons: Record<string, () => void> | ReadonlyArray<{ label: string; action: () => void }> }
266
+ | { type: "separator" }
267
+ | { type: "tabs"; pages: { readonly [title: string]: Schema } };
268
+
269
+ /** A schema value — a shorthand, a verbose `{ type }` object, or a nested folder. */
270
+ export type SchemaValue = SchemaValueOf<SchemaNest>;
271
+ // `F` is the nested-folder branch: strict for a schema annotated `Schema`, loose for what tweaks() infers from
272
+ // (HeldSchema).
273
+ type SchemaValueOf<F> =
274
+ // bare number → slider (0–1 for a value ≤ 1, else 0–3×value; a negative spans that either side of zero)
275
+ | number
276
+ | boolean // → checkbox
277
+ | string // → text input; a hex or color-function string → color picker
278
+ // [value, min?, max?, step?] → slider
279
+ | readonly [number]
280
+ | readonly [number, number]
281
+ | readonly [number, number, number]
282
+ | readonly [number, number, number, number]
283
+ // [[lo,hi], min, max, step?] → interval
284
+ | readonly [readonly [number, number], number, number]
285
+ | readonly [readonly [number, number], number, number, number]
286
+ // either of the above, widened by a schema held in a variable
287
+ | readonly number[]
288
+ | readonly (number | readonly number[])[]
289
+ | readonly Option[] // a list of options → list (dropdown)
290
+ | { options: readonly Option[]; value?: string | number } // a list with a chosen default
291
+ | { action: () => void; label?: string } // → button
292
+ | (SchemaObject & ControlOptions) // verbose form (+ render/disabled/hint)
293
+ | F // nested object → folder
294
+ | SchemaFolder; // a nested folder with its own render / disabled predicate
295
+
296
+ /** The object you hand to `tweaks(name, schema, opts?)`. No value is a function, so a verbose
297
+ * form's callbacks (`format`, `fn`) take their parameter types from that form alone. */
298
+ export interface Schema {
299
+ readonly [key: string]: SchemaValue;
300
+ }
301
+ // A folder's string `type` names a control (a known name with none of its fields is still a folder), so a name no
302
+ // control has can't pass as a folder: `const s: Schema = { a: { type: "slidr", value: 1 } }` compiled that way.
303
+ type SchemaNest = Schema & { readonly type?: SchemaObject["type"] | Exclude<SchemaValue, string> };
304
+ /** What tweaks() and mountControl() infer from: `Schema` with a folder's `type` left open, since a schema held in a
305
+ * variable widens a verbose form's `type` to `string`; their own check then rules on the names it can still see. */
306
+ export interface HeldSchema {
307
+ readonly [key: string]: HeldValue;
308
+ }
309
+ /** A single value as mountControl() infers it; see `HeldSchema`. */
310
+ export type HeldValue = SchemaValueOf<HeldSchema>;
311
+ /** A nested folder that carries its own `render` / `disabled` predicate or `hint`: chrome, not
312
+ * children. Its index admits the predicate, so it gives up a child named `type`: that sets it
313
+ * apart from a verbose form, whose callbacks would otherwise be typed against the predicate too. */
314
+ export interface SchemaFolder {
315
+ readonly type?: undefined;
316
+ readonly render?: (get: Get) => boolean;
317
+ readonly disabled?: boolean | ((get: Get) => boolean);
318
+ readonly hint?: string;
319
+ readonly [key: string]: SchemaValue | ((get: Get) => boolean) | undefined;
320
+ }
321
+
322
+ // ── Params inferred from a schema ──
323
+ // tweaks() takes its schema as a const type parameter, so each value keeps its literal shape and
324
+ // these types read the control it builds the way schema.ts does, in the same order.
325
+
326
+ /** A spring control's value: the physics, plus the time pair and mode when tuned by feel. */
327
+ export interface SpringValue {
328
+ stiffness: number;
329
+ damping: number;
330
+ mass: number;
331
+ visualDuration?: number;
332
+ bounce?: number;
333
+ mode?: "time" | "physics";
334
+ }
335
+
336
+ type OptionValue<O> = O extends string | number
337
+ ? O
338
+ : O extends { readonly value: infer V extends string | number }
339
+ ? V
340
+ : never;
341
+ // A list's param: one of its options, or a `value` off the list, which the control keeps as given until a pick.
342
+ type ChoiceValue<V> =
343
+ | (V extends { readonly options: readonly (infer O)[] } ? OptionValue<O> : never)
344
+ | (V extends { readonly value: infer D extends string | number } ? D : never);
345
+ type Alnum =
346
+ | "a"
347
+ | "b"
348
+ | "c"
349
+ | "d"
350
+ | "e"
351
+ | "f"
352
+ | "g"
353
+ | "h"
354
+ | "i"
355
+ | "j"
356
+ | "k"
357
+ | "l"
358
+ | "m"
359
+ | "n"
360
+ | "o"
361
+ | "p"
362
+ | "q"
363
+ | "r"
364
+ | "s"
365
+ | "t"
366
+ | "u"
367
+ | "v"
368
+ | "w"
369
+ | "x"
370
+ | "y"
371
+ | "z"
372
+ | "0"
373
+ | "1"
374
+ | "2"
375
+ | "3"
376
+ | "4"
377
+ | "5"
378
+ | "6"
379
+ | "7"
380
+ | "8"
381
+ | "9";
382
+ type Dashed<S extends string, Acc extends string = "", Run = false> = S extends `${infer C}${infer R}`
383
+ ? C extends Alnum
384
+ ? Dashed<R, `${Acc}${C}`, false>
385
+ : Run extends true
386
+ ? Dashed<R, Acc, true>
387
+ : Dashed<R, `${Acc}-`, true>
388
+ : Acc;
389
+ type TrimDash<S extends string> = S extends `-${infer R}` ? TrimDash<R> : S extends `${infer R}-` ? TrimDash<R> : S;
390
+ /** A tab title's key under `params`: lower-case, each run of other characters a `-`, trimmed
391
+ * (`"Layout & Grid"` → `"layout-grid"`). Two titles that slug alike are told apart at runtime only. */
392
+ export type TabKey<T extends string> = string extends T
393
+ ? string
394
+ : TrimDash<Dashed<Lowercase<T>>> extends infer K extends string
395
+ ? K extends ""
396
+ ? "tab"
397
+ : K
398
+ : never;
399
+ // True when two page titles slug to one key: the runtime suffixes the later one (`a`, `a-2`) in an
400
+ // order a type can't follow, so those pages read as the loose Params rather than merge into one.
401
+ type TabClash<P> = true extends {
402
+ [T in keyof P & string]: TabKey<T> extends { [U in Exclude<keyof P & string, T>]: TabKey<U> }[Exclude<
403
+ keyof P & string,
404
+ T
405
+ >]
406
+ ? true
407
+ : false;
408
+ }[keyof P & string]
409
+ ? true
410
+ : false;
411
+
412
+ type Valueless = "button" | "buttongroup" | "separator" | "fpsgraph" | "monitor";
413
+ // The field each falsy-capable form hinges on (schema.ts REQUIRED), or a `value`: without either the object isn't
414
+ // that control but a shorthand or a folder holding a child named `type`.
415
+ type Hinge = {
416
+ list: "options" | "value";
417
+ radiogrid: "options" | "value";
418
+ segmented: "options" | "value";
419
+ point: "components" | "value";
420
+ tabs: "pages" | "value";
421
+ };
422
+ // A hinge field there but unusable skips the control at runtime, so it puts nothing on params.
423
+ type TypedValue<V> = V extends { readonly type: "slider" | "number" }
424
+ ? number
425
+ : V extends { readonly type: "checkbox" }
426
+ ? boolean
427
+ : V extends { readonly type: "list" | "radiogrid" | "segmented" }
428
+ ? V extends { readonly options: readonly unknown[] }
429
+ ? ChoiceValue<V>
430
+ : never
431
+ : V extends { readonly type: "color" | "text" | "image" | "plot" }
432
+ ? string
433
+ : V extends { readonly type: "interval" }
434
+ ? [number, number]
435
+ : V extends { readonly type: "cubicbezier" }
436
+ ? [number, number, number, number]
437
+ : V extends { readonly type: "spring" }
438
+ ? SpringValue
439
+ : V extends { readonly type: "motion" }
440
+ ? MotionValue
441
+ : V extends { readonly type: "shadow" }
442
+ ? ShadowValue
443
+ : V extends { readonly type: "sides" }
444
+ ? SidesValue
445
+ : V extends { readonly type: "gradient" }
446
+ ? GradientValue
447
+ : V extends { readonly type: "point" }
448
+ ? V extends { readonly components: readonly [] }
449
+ ? never
450
+ : V extends { readonly components: readonly (infer C)[] }
451
+ ? { [K in C extends { readonly key: infer Key extends string } ? Key : never]: number }
452
+ : V extends { readonly value: infer P extends object }
453
+ ? keyof P extends never
454
+ ? never
455
+ : { -readonly [K in keyof P & string]: number }
456
+ : never
457
+ : V extends { readonly type: "tabs" }
458
+ ? V extends { readonly pages: infer P extends object }
459
+ ? keyof P extends never
460
+ ? never
461
+ : TabClash<P> extends true
462
+ ? Record<string, Params>
463
+ : { [T in keyof P & string as TabKey<T>]: ParamsOf<P[T]> }
464
+ : never
465
+ : unknown;
466
+ /** The value a schema entry puts on `params`. */
467
+ export type ValueOf<V> = V extends { readonly type: infer T }
468
+ ? // a `type` widened by a schema held in a variable: the control can't be known
469
+ string extends T
470
+ ? any
471
+ : T extends SchemaObject["type"]
472
+ ? T extends keyof Hinge
473
+ ? [Extract<Hinge[T], keyof V>] extends [never]
474
+ ? ShorthandValue<V>
475
+ : VerboseValue<V, T>
476
+ : VerboseValue<V, T>
477
+ : // A name no control has: with a field only a control reads, the runtime skips it; without one, it's a folder.
478
+ T extends string
479
+ ? [Extract<"value" | "options" | "action", keyof V>] extends [never]
480
+ ? ShorthandValue<V>
481
+ : never
482
+ : ShorthandValue<V>
483
+ : ShorthandValue<V>;
484
+ type VerboseValue<V, T> = T extends Valueless ? never : TypedValue<V>;
485
+ type ShorthandValue<V> = V extends readonly [readonly [number, number], ...unknown[]]
486
+ ? [number, number]
487
+ : V extends
488
+ | readonly [number]
489
+ | readonly [number, number]
490
+ | readonly [number, number, number]
491
+ | readonly [number, number, number, number]
492
+ ? number
493
+ : V extends readonly [number, ...number[]]
494
+ ? V[number] // five numbers or more are a list of them
495
+ : V extends readonly []
496
+ ? undefined // an empty list, with no choice to hold
497
+ : // a tuple widened by a schema held in a variable: a slider, or a list of numbers
498
+ V extends readonly number[]
499
+ ? number
500
+ : V extends readonly (number | readonly number[])[]
501
+ ? [number, number]
502
+ : V extends number
503
+ ? number
504
+ : V extends boolean
505
+ ? boolean
506
+ : V extends string
507
+ ? string
508
+ : V extends readonly (infer O)[]
509
+ ? OptionValue<O>
510
+ : V extends { readonly action: (...a: any[]) => any }
511
+ ? never
512
+ : V extends { readonly options: readonly unknown[] }
513
+ ? ChoiceValue<V>
514
+ : V extends object
515
+ ? Entries<V, true>
516
+ : unknown;
517
+ // Keys the runtime skips at every level, since they would write through to Object.prototype.
518
+ type Reserved = "__proto__" | "constructor" | "prototype";
519
+ // A nested folder's render / disabled / hint are its chrome, not children; at the root and on a
520
+ // tab page they are controls like any other key.
521
+ type FolderChrome<V, K> = K extends "disabled" | "hint"
522
+ ? true
523
+ : K extends "render"
524
+ ? V extends (...a: any[]) => any
525
+ ? true
526
+ : false
527
+ : false;
528
+ type Entries<S, Nested extends boolean> = string extends keyof S
529
+ ? Params
530
+ : {
531
+ -readonly [
532
+ K in keyof S & string as K extends Reserved
533
+ ? never
534
+ : ValueOf<S[K]> extends never
535
+ ? never
536
+ : Nested extends true
537
+ ? FolderChrome<S[K], K> extends true
538
+ ? never
539
+ : K
540
+ : K
541
+ ]: ValueOf<S[K]>;
542
+ };
543
+ /** The live values a schema builds, nested like the schema; a plain `Schema` gives the loose `Params`. */
544
+ export type ParamsOf<S> = Entries<S, false>;
545
+
546
+ // ── Type names checked at the call ──
547
+ // Schema's folder branch takes any object, so `{ type: "slidr", value: 1 }` fit it and compiled. tweaks() and
548
+ // mountControl() intersect their argument with these, which keep a known name, a widened `string` (a schema held in a
549
+ // variable: the runtime decides), and every shorthand as they are, and turn an unknown name into the list of known
550
+ // ones, so it fails right where it's written. A known name without its hinge field is a folder at runtime and isn't
551
+ // checked further; with it, or for a name with no hinge, the runtime builds that control, so its fields must fit the
552
+ // form: `{ type: "image", options: [1] }` read as a folder of a text and a slider otherwise, and compiled.
553
+ type CheckValue<V> = V extends { readonly type: infer T }
554
+ ? string extends T
555
+ ? V
556
+ : T extends SchemaObject["type"]
557
+ ? V extends { readonly type: "tabs"; readonly pages: infer P }
558
+ ? { readonly pages: { [K in keyof P]: KnownTypes<P[K]> } }
559
+ : T extends keyof Hinge
560
+ ? [Extract<Hinge[T], keyof V>] extends [never]
561
+ ? unknown
562
+ : FitsForm<V, T>
563
+ : FitsForm<V, T>
564
+ : { readonly type: NotAControlType }
565
+ : V extends readonly unknown[] | ((...a: any[]) => any)
566
+ ? unknown
567
+ : V extends object
568
+ ? KnownTypes<V>
569
+ : unknown;
570
+ // 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.
571
+ type FormOf<O, T> = O extends { readonly type: infer N } ? (T extends N ? O & ControlOptions : never) : never;
572
+ type FitsForm<V, T> = V extends FormOf<SchemaObject, T> ? unknown : FormOf<SchemaObject, T>;
573
+ // A brand, not the list of names: a literal intersected with the list collapses the whole entry to `never`, and the
574
+ // error read "not assignable to type 'never'" on every key. The brand keeps the entry and puts its text in the error.
575
+ type NotAControlType = { readonly "names no control: see the type list in AGENTS.md": true };
576
+ /** A schema whose every `type` names a control (`tweaks()` checks it; `{ type: "slidr" }` is an error). */
577
+ export type KnownTypes<S> = { [K in keyof S]: CheckValue<S[K]> };
578
+ /** One schema value whose `type` names a control (`mountControl()` checks it). */
579
+ export type KnownType<V> = CheckValue<V>;
580
+
581
+ /** A theme — token overrides applied over the default monochrome look. Every key is
582
+ * optional, so a partial theme only moves what it names; unset tokens keep their default.
583
+ * Keys are the `--twk-*` custom properties themselves. Apply at build via
584
+ * `tweaks(name, schema, { theme })`, or live via `panel.setTheme(theme)`. The kit's whole
585
+ * appearance runs on these custom properties — this is the public theming surface. */
586
+ export interface Theme {
587
+ /** Brand accent — slider fills, focus rings, active highlights (default: neutral, no accent). */
588
+ "--twk-accent"?: string;
589
+ /** Text drawn on the accent (active segment / radio label); the primary text tone unless set, so set it with a light
590
+ * accent. */
591
+ "--twk-on-accent"?: string;
592
+ /** Panel background, reused for recessed wells. */
593
+ "--twk-base"?: string;
594
+ /** Popover / dropdown background. */
595
+ "--twk-dropdown-bg"?: string;
596
+ /** Control surface and its hover / active steps. */
597
+ "--twk-surface"?: string;
598
+ "--twk-surface-hover"?: string;
599
+ "--twk-surface-active"?: string;
600
+ /** Hairline border and its hover step. */
601
+ "--twk-border"?: string;
602
+ "--twk-border-hover"?: string;
603
+ /** Text-selection highlight wash. */
604
+ "--twk-selection"?: string;
605
+ /** Text tones — panel title, section heading, primary value text, control label, and the muted / faint / focus
606
+ * tones. */
607
+ "--twk-text-root"?: string;
608
+ "--twk-text-section"?: string;
609
+ "--twk-text-primary"?: string;
610
+ "--twk-text-label"?: string;
611
+ "--twk-text-secondary"?: string;
612
+ "--twk-text-tertiary"?: string;
613
+ "--twk-text-focus"?: string;
614
+ /** Copy-confirmation accent, and the invalid-state accent (the plot's bad-expression cue). */
615
+ "--twk-success"?: string;
616
+ "--twk-danger"?: string;
617
+ /** Popover shadow, the panel's container elevation, and its lifted (floating / dragging) variant. */
618
+ "--twk-shadow-dropdown"?: string;
619
+ "--twk-shadow-panel"?: string;
620
+ "--twk-shadow-panel-lifted"?: string;
621
+ /** Font stack, and the monospace stack (the plot's expression field). */
622
+ "--twk-font-sans"?: string;
623
+ "--twk-font-mono"?: string;
624
+ /** Control corner radius, row height and the panel's width (256px by default), as CSS lengths. */
625
+ "--twk-radius"?: string;
626
+ "--twk-row-height"?: string;
627
+ "--twk-width"?: string;
628
+ /** Any other token. */
629
+ [token: `--twk-${string}`]: string | undefined;
630
+ }
631
+
632
+ /** Options for a programmatic write — `set()`, `setMany()`, `fromJSON()`. */
633
+ export interface SetOptions {
634
+ /** Apply without calling your `on()` listeners: for mirroring a value the host already
635
+ * acted on (another tab, its own state), so the change doesn't echo back. Conditional
636
+ * controls, persistence and undo still follow it: it's an undo step like any other. */
637
+ silent?: boolean;
638
+ }
639
+
640
+ /** Where a floating panel starts: `x` / `y` are its insets in px (16 by default) from the
641
+ * viewport's top-left. The far edge keeps a gap of `y` when the height caps; CSS
642
+ * `--twk-float-gap` overrides it. */
643
+ export interface FloatingPlacement {
644
+ x?: number;
645
+ y?: number;
646
+ }
647
+
648
+ /** The words a panel shows and says, by key, for the `strings` option (each line notes the English default). A key
649
+ * with a `{placeholder}` takes a string that uses it, or a function given the values, for a language whose plurals
650
+ * need more than one form. Units (px, ms, %), axis letters and color notations stay as they are. */
651
+ export type Word<V = never> = [V] extends [never] ? string : string | ((v: V) => string);
652
+ export interface Strings {
653
+ // The panel and its toolbar
654
+ /** "Tweaks" — the panel's landmark name when its own is blank */ panel?: string;
655
+ /** "Presets" — the toolbar button */ presets?: string;
656
+ /** "Copy values (⇧: changed only)" — the toolbar button */ copy?: string;
657
+ /** "Reset" — the toolbar button */ reset?: string;
658
+ /** "Double-click or hold to reset" — a row label's tooltip */ resetHint?: string;
659
+ /** "Values copied" */ copied?: string;
660
+ /** "{n} changed values copied" — 1 changed value copied for one */ copiedChanged?: Word<{ n: number }>;
661
+ /** "Nothing has changed" */ nothingChanged?: string;
662
+ /** "Copy failed" */ copyFailed?: string;
663
+ /** "{label} copied" — ⌘C on a row */ rowCopied?: Word<{ label: string }>;
664
+ /** "Not saved" — a value storage refused */ notSaved?: string;
665
+ /** "{n} values pasted" — 1 value pasted for one */ pasted?: Word<{ n: number }>;
666
+ /** "Nothing to paste" */ nothingToPaste?: string;
667
+ /** "Couldn't read the clipboard" */ clipboardUnread?: string;
668
+ // The presets menu
669
+ /** "Preset name…" — the field's placeholder */ presetName?: string;
670
+ /** "New preset name" — the field's label */ newPreset?: string;
671
+ /** "Save" */ save?: string;
672
+ /** "No presets yet" */ noPresets?: string;
673
+ /** "Loaded preset {name}" */ presetLoaded?: Word<{ name: string }>;
674
+ /** "Saved preset {name}" */ presetSaved?: Word<{ name: string }>;
675
+ /** "Preset {name} not saved: storage is full" */ presetFull?: Word<{ name: string }>;
676
+ /** "Delete preset {name}" — a row's delete button */ presetDelete?: Word<{ name: string }>;
677
+ // Controls
678
+ /** "Off" — a checkbox */ off?: string;
679
+ /** "On" — a checkbox */ on?: string;
680
+ /** "Button" — a group button with no label */ button?: string;
681
+ /** "Tab" — a tab page whose title is blank */ tab?: string;
682
+ /** "Folder" — a markup folder with no data-label */ folder?: string;
683
+ /** "Drag to adjust" — a number field's grip */ dragToAdjust?: string;
684
+ /** "{label} minimum" */ intervalMin?: Word<{ label: string }>;
685
+ /** "{label} maximum" */ intervalMax?: Word<{ label: string }>;
686
+ /** "Monitor" — a monitor with no label */ monitor?: string;
687
+ /** "FPS" — an fpsgraph with no label */ fps?: string;
688
+ /** "Plot" — a plot with no label */ plot?: string;
689
+ /** "{label} — expression in x" — a plot's field */ expression?: Word<{ label: string }>;
690
+ /** "Point" — a point with no label */ point?: string;
691
+ /** "{label} pad: {x} and {y}" */ pointPad?: Word<{ label: string; x: string | number; y: string | number }>;
692
+ /** "Control point {n}" — a cubic bézier handle */ controlPoint?: Word<{ n: number }>;
693
+ /** "Control point {n}, x {x}, y {y}" */ controlPointAt?: Word<{ n: number; x: string | number; y: string | number }>;
694
+ // Color
695
+ /** "Color" — a color row with no label, and the shadow's color row */ color?: string;
696
+ /** "Color plane: chroma across, lightness up" */ colorPlane?: string;
697
+ /** "Color mode" */ colorMode?: string;
698
+ /** "Hex color" */ hex?: string;
699
+ /** "Hue" */ hue?: string;
700
+ /** "Alpha" */ alpha?: string;
701
+ /** "lightness {l}%, chroma {c}" — the color plane's description, said after a run of keys */ colorReadout?: Word<{
702
+ l: number;
703
+ c: number;
704
+ }>;
705
+ /** "wide" — the gamut badge past P3 (sRGB and P3 are names) */ wide?: string;
706
+ // Gradient
707
+ /** "Gradient" — a gradient with no label */ gradient?: string;
708
+ /** "{n} stops" — 1 stop for one */ stops?: Word<{ n: number }>;
709
+ /** "Add a stop after the selected one" — the add button's tooltip */ addStop?: string;
710
+ /** "Add a color stop after the selected one" — the add button's label */ addStopAria?: string;
711
+ /** "Color stop at {pos}%" */ stopAt?: Word<{ pos: number }>;
712
+ /** "Color stop at {pos}%, selected" */ stopAtSelected?: Word<{ pos: number }>;
713
+ /** "Easing between stops" — the easing menu's label */ stopEasing?: string;
714
+ /** "How the ramp blends between stops" — its tooltip */ stopEasingHint?: string;
715
+ /** "Linear" */ easeLinear?: string;
716
+ /** "Ease" */ ease?: string;
717
+ /** "Ease in" */ easeIn?: string;
718
+ /** "Ease out" */ easeOut?: string;
719
+ /** "Ease in-out" */ easeInOut?: string;
720
+ /** "Custom" — an easing the menu doesn't list */ custom?: string;
721
+ // Image
722
+ /** "Upload image" — the gallery's upload button */ upload?: string;
723
+ /** "{label}: upload an image" */ uploadImage?: Word<{ label: string }>;
724
+ /** "{label}: choose an image" */ chooseImage?: Word<{ label: string }>;
725
+ /** "Clear" */ clear?: string;
726
+ /** "{label}: clear the image" */ clearImage?: Word<{ label: string }>;
727
+ /** "Replace" */ replace?: string;
728
+ /** "Drop or choose" */ dropOrChoose?: string;
729
+ /** "None" — no image chosen */ none?: string;
730
+ /** "Image {n}" — a gallery option with no label */ imageN?: Word<{ n: number }>;
731
+ /** "Uploaded image" */ uploadedImage?: string;
732
+ /** "Custom image" — a value off the gallery's list */ customImage?: string;
733
+ /** "Not an image" */ notAnImage?: string;
734
+ /** "Couldn't read the file" */ unreadable?: string;
735
+ /** "{size}, max {max}" — a file over maxSize */ tooLarge?: Word<{ size: string; max: string }>;
736
+ // Motion and spring
737
+ /** "Motion" — a motion row with no label */ motion?: string;
738
+ /** "Motion type" */ motionType?: string;
739
+ /** "Type" — the motion pane's row label beside that switch */ type?: string;
740
+ /** "Easing" */ easing?: string;
741
+ /** "Spring" */ spring?: string;
742
+ /** "Duration" */ duration?: string;
743
+ /** "Replay the motion" */ replay?: string;
744
+ /** "Easing · {ms} ms" */ easingSummary?: Word<{ ms: number }>;
745
+ /** "Spring · {ms} ms" */ springSummary?: Word<{ ms: number }>;
746
+ /** "Spring mode" */ springMode?: string;
747
+ /** "Mode" */ mode?: string;
748
+ /** "Time" */ time?: string;
749
+ /** "Physics" */ physics?: string;
750
+ /** "Stiffness" */ stiffness?: string;
751
+ /** "Damping" */ damping?: string;
752
+ /** "Mass" */ mass?: string;
753
+ /** "Bounce" */ bounce?: string;
754
+ // Shadow
755
+ /** "Shadow" — a shadow with no label */ shadow?: string;
756
+ /** "Shadow position" */ shadowPosition?: string;
757
+ /** "in" — an inset shadow's summary, before its numbers */ inset?: string;
758
+ /** "Position" */ position?: string;
759
+ /** "Outer" */ outer?: string;
760
+ /** "Inner" */ inner?: string;
761
+ /** "Blur" */ blur?: string;
762
+ /** "Spread" */ spread?: string;
763
+ // Sides and corners
764
+ /** "Top" */ top?: string;
765
+ /** "Right" */ right?: string;
766
+ /** "Bottom" */ bottom?: string;
767
+ /** "Left" */ left?: string;
768
+ /** "Top-left" */ topLeft?: string;
769
+ /** "Top-right" */ topRight?: string;
770
+ /** "Bottom-right" */ bottomRight?: string;
771
+ /** "Bottom-left" */ bottomLeft?: string;
772
+ /** "Independent sides" */ splitSides?: string;
773
+ /** "Independent corners" */ splitCorners?: string;
774
+ }
775
+
776
+ /** Panel options (third arg to `tweaks()`). */
777
+ export interface TweaksOptions {
778
+ /** Theme overrides as `--twk-*` tokens; `null` / omitted = default monochrome. */
779
+ theme?: Theme | null;
780
+ /** Persist values to localStorage + enable presets — a key, or `true` to key by name. Uploaded images go to
781
+ * IndexedDB under the same key, and come back by `ready`. */
782
+ persist?: string | boolean;
783
+ /** With `persist`, `false` keeps saving values but leaves the Presets menu out of the
784
+ * toolbar (the presets API still works). On by default. */
785
+ presets?: boolean;
786
+ /** Start the panel already floated (fixed-positioned): `true` → 16px from the top-left, or a
787
+ * placement `{ x, y }`. Its height caps at the window and it scrolls, header and all. */
788
+ floating?: boolean | FloatingPlacement;
789
+ /** Drag the header to reposition the panel — on by default. An inline panel lifts into a
790
+ * floating layer on first drag; set `false` to pin it in place. */
791
+ draggable?: boolean;
792
+ /** Set `false` for a bare panel (no copy/reset/presets toolbar). */
793
+ toolbar?: boolean;
794
+ /** Enable debounced undo/redo (⌘Z / ⇧⌘Z while the panel is hovered/focused). */
795
+ undo?: boolean;
796
+ /** Custom reset handler (e.g. restore real app defaults + rebuild). */
797
+ onReset?: () => void;
798
+ /** Mount the panel in a shadow root, so page CSS can't restyle it (`#app button { … }`, a `.prose` wrapper). `el` is
799
+ * then its host, a `<twk-host>`: theme it with `--twk-*` on the host itself (`.twk-host { … }`), or with `theme`.
800
+ * The styles come from the page's `tweakit/css` when that's loaded, else from `href` (by default `tweaks.css` beside
801
+ * the module). */
802
+ shadow?: boolean | { href?: string };
803
+ /** The words the panel and its controls show and say, over the English defaults: translate them, or rename the chrome
804
+ * (`{ presets: "Looks" }`). A key left out keeps its default. */
805
+ strings?: Strings;
806
+ }
807
+
808
+ /** Options for `enhance()`: the words its markup controls show and say. */
809
+ export interface EnhanceOptions {
810
+ strings?: Strings;
811
+ }
812
+
813
+ /** The live values bag, loosely typed — `params[key]` for each control. A schema literal passed
814
+ * to `tweaks()` gets its exact shape instead (`ParamsOf`). */
815
+ export type Params = Record<string, any>;
816
+ /** The keys `set()` and `setMany()` take: a typed panel's controls by dotted path (`size`, `card.size`), so a typo is a
817
+ * compile error; any string for a panel built from a plain `Schema`. An object-valued control's own fields read as
818
+ * paths too (`drop.blur`): the types can't tell its value from a folder, so they allow what the runtime refuses. Past
819
+ * ten folders deep the rest of a path is any string, since spelling every one out overran tsc's depth limit. */
820
+ export type PanelPath<P> = PathsOf<P, []>;
821
+ type PathsOf<P, D extends unknown[]> = string extends keyof P
822
+ ? string
823
+ : D["length"] extends 10
824
+ ? string
825
+ : {
826
+ [K in keyof P & string]:
827
+ | K
828
+ | (P[K] extends readonly unknown[]
829
+ ? never
830
+ : P[K] extends object
831
+ ? `${K}.${PathsOf<P[K], [...D, 0]>}`
832
+ : never);
833
+ }[keyof P & string];
834
+
835
+ /** The whole-panel state `toJSON()` returns / `fromJSON()` accepts: every control value
836
+ * (nested to mirror folders/tabs) plus UI state — which folders are collapsed and which
837
+ * tab is active. JSON-safe; persist it however you like. Both `values` and `ui` are
838
+ * optional on the way in, so `fromJSON({ values })` restores values only and
839
+ * `fromJSON({ ui })` restores chrome only. (Panel collapse + floating position are not
840
+ * captured — position has its own `opts.persist` channel.) */
841
+ export interface PanelState {
842
+ /** Every control value, **nested** to mirror folders/tabs (e.g. `{ shadow: { radius: 16 } }`)
843
+ * — not the dotted form `set()`/`setMany()` take. `fromJSON` matches by that nested path and
844
+ * skips keys with no matching control. Normally you pass back what `toJSON()` produced. */
845
+ values?: Record<string, any>;
846
+ ui?: {
847
+ /** Folder collapse state → `true` when collapsed. Keys are the folder's path with each
848
+ * segment JSON-pointer-escaped (`.`→`~0`, `~`→`~1`) then joined by `.` — injective, so a
849
+ * literal dot in a key can't collide with the nesting separator. For plain keys that's
850
+ * the dotted path, so `fromJSON({ ui: { folders: { shadow: true } } })` starts one closed. */
851
+ folders?: Record<string, boolean>;
852
+ /** Active tab → the active page's key, under the tabs control's path (escaped as above). */
853
+ tabs?: Record<string, string | null>;
854
+ };
855
+ }
856
+
857
+ /** The panel object `tweaks()` returns; `P` is its values' shape. A bare `Panel` takes any panel, typed or not (its
858
+ * default is `any`, not `Params`: `set()`'s typed paths would make a typed panel unassignable to `Panel<Params>`). */
859
+ export interface Panel<P = any> {
860
+ /** The panel root element — append it to the DOM. With `shadow`, the host the panel lives in. */
861
+ el: HTMLElement;
862
+ /** Live values, updated in place as controls change. */
863
+ params: P;
864
+ /** Resolves once any lazily-loaded controls have hydrated and, for a panel that persists an image control, its
865
+ * stored images have loaded (resolves immediately when there's neither). */
866
+ ready: Promise<Panel<P>>;
867
+ /** Subscribe to changes; `last` is the changed control's dotted path (`size`, `card.size`), as `set()` and
868
+ * `changes()`
869
+ * name it, or undefined when many changed at once (a restore, `reset()`, `fromJSON`, `setMany`). */
870
+ on(fn: (params: P, last?: string) => void): () => void;
871
+ /** Set a control's value programmatically. Nested controls take a dotted path
872
+ * ("folder.child", "tabs.page.child"); at runtime a bare key also reaches a nested control
873
+ * when it's unambiguous, though a typed panel's types take the full path. A key matching
874
+ * no control, or a folder or tab, is refused with a console warning. */
875
+ set(key: PanelPath<P>, value: unknown, options?: SetOptions): void;
876
+ /** Set several controls at once — a flat map of keys (dotted for nested controls), e.g.
877
+ * `setMany({ "shadow.radius": 28, blur: 48 })`. Resolves each key like `set()`, then fires
878
+ * listeners + persists ONCE for the whole batch, where N separate `set()` calls would fire
879
+ * N times. Unknown / ambiguous / reserved keys warn and are skipped. */
880
+ setMany(values: { readonly [K in PanelPath<P>]?: unknown }, options?: SetOptions): void;
881
+ /** Reset every control to its default (or run `opts.onReset`). */
882
+ reset(): void;
883
+ /** Serialize the whole panel — every control value plus UI state (collapsed folders,
884
+ * active tabs) — to a plain JSON-safe object, independent of localStorage/presets. Hand
885
+ * it to your own storage: a file, a query string / share link, a server. `JSON.stringify`
886
+ * on the panel calls this too. Most meaningful once `ready` (lazy controls have built). */
887
+ toJSON(): PanelState;
888
+ /** The values that differ from their defaults, keyed by dotted path — `setMany()`'s shape
889
+ * (`{ "shadow.radius": 28, blur: 48 }`), the hand-off for baking tuned values into source.
890
+ * ⇧-click on the toolbar's copy button copies this instead of the full snapshot. Empty
891
+ * before `ready`. */
892
+ changes(): Record<string, unknown>;
893
+ /** Restore a panel from a `toJSON()` object: values are applied where their path still
894
+ * exists (missing ones skipped, like loading a preset), then folder/tab UI state. Fires
895
+ * listeners once. Both halves are optional — pass `{ values }` to restore values only, or
896
+ * `{ ui }` to restore chrome only. The value restore is undoable (one history step); the
897
+ * UI restore is intentionally outside undo, so a Cmd-Z reverts values without thrashing
898
+ * folders/tabs. */
899
+ fromJSON(state: PanelState, options?: SetOptions): void;
900
+ /** Re-theme the panel live; `null` reverts to the default. */
901
+ setTheme(theme?: Theme | null): void;
902
+ /** Retitle the panel live — the header follows. The name it was built
903
+ * with stays its storage key (`persist: true`), so saved values and presets keep their
904
+ * home across a rename. */
905
+ setName(name: string): void;
906
+ /** Save the current values as a named preset (needs `opts.persist`). False when there is nowhere to save it, or
907
+ * storage refused it. */
908
+ savePreset(name: string): boolean;
909
+ /** Load a named preset. */
910
+ loadPreset(name: string): boolean;
911
+ /** Delete a named preset. */
912
+ deletePreset(name: string): void;
913
+ /** List saved preset names. */
914
+ presets(): string[];
915
+ /** Undo / redo (need `opts.undo`). */
916
+ undo(): void;
917
+ redo(): void;
918
+ /** Tear the panel down — remove it from the DOM, release every global listener, close any open popover; the API goes
919
+ * inert. */
920
+ destroy(): void;
921
+ }
922
+
923
+ /** A built control's handle (internal-ish, but used across modules). */
924
+ export interface Control {
925
+ el: HTMLElement;
926
+ set(value: unknown): void;
927
+ get(): unknown;
928
+ /** Release any loop the control owns (the monitor's poll, the FPS graph's rAF). Only
929
+ * the live controls supply it; the panel calls it from `destroy()`. */
930
+ destroy?(): void;
931
+ /** Show a short message on the control for a moment. Only the image control supplies it; the panel calls it when
932
+ * storage refuses the control's value. */
933
+ note?(message: string): void;
934
+ /** The hard range set() fits a number into, as the control resolved it (derived bounds too); a point's per component.
935
+ * Only the numeric controls supply it; the panel's clamp notice reads it. */
936
+ range?: [number, number] | Record<string, [number, number]>;
937
+ }
938
+
939
+ /** Options for `mountControl(host, value, opts?)` — one control, no panel. */
940
+ export interface MountOptions {
941
+ /** The key passed to `onChange` and used to derive the label when none is given (default `"value"`). */
942
+ key?: string;
943
+ /** The label shown on the control; `""` shows none (the host page already has one). Wins over
944
+ * the title-cased key and over a verbose form's own `label`. */
945
+ label?: string;
946
+ /** Fired with the new value whenever the control changes — by hand or through `set()`. */
947
+ onChange?: (value: unknown, key: string) => void;
948
+ /** Mount it in a shadow root, as `tweaks()`'s `shadow` does a panel; `el` is then the host. */
949
+ shadow?: boolean | { href?: string };
950
+ /** Its words, as `tweaks()`'s `strings`. */
951
+ strings?: Strings;
952
+ }
953
+
954
+ /** The handle `mountControl()` returns. */
955
+ export interface MountedControl {
956
+ /** The wrapper (`.twk-control`) the control lives in, already appended to the host and
957
+ * stable from creation — it carries the kit's tokens, so the control renders outside a
958
+ * panel. On the code-split build a lazily-loaded control fills it in once `ready` resolves. With `shadow`, the host
959
+ * the wrapper lives in. */
960
+ el: HTMLElement;
961
+ /** Apply a value — the control updates AND `onChange` fires (when the value actually
962
+ * changed). Before `ready`, the value is queued and applied once the control builds. */
963
+ set(value: unknown): void;
964
+ /** The current value (the queued or schema value before `ready`; `undefined` for a
965
+ * valueless control such as a button). */
966
+ get(): unknown;
967
+ /** Remove the control from the host and release anything it owns. */
968
+ destroy(): void;
969
+ /** Resolves with this handle once the control has built — immediately on the single-file build. */
970
+ ready: Promise<MountedControl>;
971
+ }
972
+
973
+ /** The notations the color picker edits in (its mode dropdown). The emitted value follows
974
+ * the mode: `hex` gives `#RRGGBB`, the wide-gamut modes give a CSS color like `oklch(…)`. */
975
+ export type ColorMode = "hex" | "srgb" | "css" | "hsl" | "hwb" | "oklch" | "oklab" | "lch" | "lab" | "p3" | "rec2020";
976
+
977
+ /** Options for `createColorPicker(opts?)` — the picker body alone, no trigger row. */
978
+ export interface ColorPickerOptions {
979
+ /** The color it opens on — any CSS color string (default `#7c5cff`). */
980
+ value?: string;
981
+ /** The notation it opens in (default `oklch`). */
982
+ mode?: ColorMode;
983
+ /** Fired with the new color string whenever it changes — by hand or through `set()`. */
984
+ onChange?: (value: string) => void;
985
+ /** Build it in a shadow root, as `tweaks()`'s `shadow` does a panel; `el` is then the host. */
986
+ shadow?: boolean | { href?: string };
987
+ /** Its words, as `tweaks()`'s `strings`. */
988
+ strings?: Strings;
989
+ }
990
+
991
+ /** The handle `createColorPicker()` returns. Bring your own trigger: append `el` to a popover
992
+ * or a panel of your own; the kit owns the editor surface and the gamut math. */
993
+ export interface ColorPicker {
994
+ /** The picker's root (`.twk-color-picker`), stable from creation; it carries the kit's tokens,
995
+ * so the picker renders anywhere. On the code-split build the editor fills it in once the
996
+ * color chunk has loaded. With `shadow`, the host the root lives in. */
997
+ el: HTMLElement;
998
+ /** Apply a color — the picker updates AND `onChange` fires (when the color actually changed). */
999
+ set(value: string): void;
1000
+ /** The current color, serialized in the picker's mode. */
1001
+ get(): string;
1002
+ /** The current notation. */
1003
+ mode(): ColorMode;
1004
+ /** Switch notation — formatting only, nothing fires. */
1005
+ setMode(mode: ColorMode): void;
1006
+ /** Re-rasterize and re-place the thumbs. It reflows itself a frame after creation; call
1007
+ * this when you show it later (a popover opening) or after it resizes. */
1008
+ reflow(): void;
1009
+ /** Remove the picker from the DOM. */
1010
+ destroy(): void;
1011
+ /** Resolves with this handle once the editor has built — immediately on the single-file build. */
1012
+ ready: Promise<ColorPicker>;
1013
+ }
1014
+
1015
+ /** `detail` of the `twk:change` event a `[data-twk]` host dispatches (bubbling) whenever its
1016
+ * control changes — `key` is the host's `data-key` (or its label), `value` the new value,
1017
+ * also written to the host's `data-value`. */
1018
+ export interface TwChangeDetail {
1019
+ key: string;
1020
+ value: unknown;
1021
+ }
1022
+
1023
+ declare global {
1024
+ interface HTMLElementEventMap {
1025
+ "twk:change": CustomEvent<TwChangeDetail>;
1026
+ }
1027
+ }