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.
Files changed (111) hide show
  1. package/AGENTS.md +31 -29
  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-JFEZMCLP.js +2 -0
  7. package/dist/tweaks/bezier-JFEZMCLP.js.map +6 -0
  8. package/dist/tweaks/chunk-6UO4DHBN.js +2 -0
  9. package/dist/tweaks/chunk-6UO4DHBN.js.map +6 -0
  10. package/dist/tweaks/chunk-DIQMLBAS.js +2 -0
  11. package/dist/tweaks/chunk-DIQMLBAS.js.map +6 -0
  12. package/dist/tweaks/chunk-XE6OUJBG.js +2 -0
  13. package/dist/tweaks/chunk-XE6OUJBG.js.map +6 -0
  14. package/dist/tweaks/clip-WW7JYO4X.js +2 -0
  15. package/dist/tweaks/clip-WW7JYO4X.js.map +6 -0
  16. package/dist/tweaks/color-YMMFURP6.js +2 -0
  17. package/dist/tweaks/color-YMMFURP6.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-UGN7TXGX.js → fold-U6YRZLCJ.js} +2 -1
  21. package/dist/tweaks/fold-U6YRZLCJ.js.map +6 -0
  22. package/dist/tweaks/gradient-JER7PW4D.js +2 -0
  23. package/dist/tweaks/gradient-JER7PW4D.js.map +6 -0
  24. package/dist/tweaks/heavy-CLD7UAVI.js +2 -0
  25. package/dist/tweaks/heavy-CLD7UAVI.js.map +6 -0
  26. package/dist/tweaks/image-H6QSLGO3.js +4 -0
  27. package/dist/tweaks/image-H6QSLGO3.js.map +6 -0
  28. package/dist/tweaks/image-store-WRKY2NPU.js +2 -0
  29. package/dist/tweaks/image-store-WRKY2NPU.js.map +6 -0
  30. package/dist/tweaks/interval-TEUHQGZN.js +2 -0
  31. package/dist/tweaks/interval-TEUHQGZN.js.map +6 -0
  32. package/dist/tweaks/markup-NIBJD6E2.js +2 -0
  33. package/dist/tweaks/markup-NIBJD6E2.js.map +6 -0
  34. package/dist/tweaks/monitor-3BY7YCGX.js +3 -0
  35. package/dist/tweaks/monitor-3BY7YCGX.js.map +6 -0
  36. package/dist/tweaks/motion-FQPUBKKM.js +2 -0
  37. package/dist/tweaks/motion-FQPUBKKM.js.map +6 -0
  38. package/dist/tweaks/plot-2HBNBZSG.js +3 -0
  39. package/dist/tweaks/plot-2HBNBZSG.js.map +6 -0
  40. package/dist/tweaks/point-VV6XV3I6.js +2 -0
  41. package/dist/tweaks/point-VV6XV3I6.js.map +6 -0
  42. package/dist/tweaks/shadow-5E4E55I2.js +2 -0
  43. package/dist/tweaks/shadow-5E4E55I2.js.map +6 -0
  44. package/dist/tweaks/sides-P3STEVYY.js +2 -0
  45. package/dist/tweaks/sides-P3STEVYY.js.map +6 -0
  46. package/dist/tweaks/spring-HUVSXNVN.js +2 -0
  47. package/dist/tweaks/spring-HUVSXNVN.js.map +6 -0
  48. package/dist/tweaks/tabs-76FW7LQ7.js +2 -0
  49. package/dist/tweaks/tabs-76FW7LQ7.js.map +6 -0
  50. package/dist/tweaks.css +1 -1
  51. package/dist/tweaks.js +6 -3
  52. package/dist/tweaks.js.map +6 -0
  53. package/dist/types/tweaks/easing.d.ts +8 -11
  54. package/dist/types/tweaks/panel.d.ts +6 -4
  55. package/dist/types/tweaks/schema.d.ts +4 -8
  56. package/dist/types/tweaks/standalone.d.ts +3 -3
  57. package/dist/types/tweaks/types.d.ts +278 -101
  58. package/package.json +27 -5
  59. package/src/tweaks/auto.ts +12 -0
  60. package/src/tweaks/clip.ts +94 -0
  61. package/src/tweaks/controls/basic.ts +403 -0
  62. package/src/tweaks/controls/bezier.ts +99 -0
  63. package/src/tweaks/controls/color.ts +412 -0
  64. package/src/tweaks/controls/gradient.ts +240 -0
  65. package/src/tweaks/controls/image.ts +299 -0
  66. package/src/tweaks/controls/interval.ts +104 -0
  67. package/src/tweaks/controls/monitor.ts +105 -0
  68. package/src/tweaks/controls/motion.ts +141 -0
  69. package/src/tweaks/controls/plot.ts +220 -0
  70. package/src/tweaks/controls/point.ts +119 -0
  71. package/src/tweaks/controls/shadow.ts +83 -0
  72. package/src/tweaks/controls/sides.ts +86 -0
  73. package/src/tweaks/controls/spring.ts +148 -0
  74. package/src/tweaks/controls/tabs.ts +47 -0
  75. package/src/tweaks/core.ts +9 -0
  76. package/src/tweaks/easing.ts +93 -0
  77. package/src/tweaks/enhance.ts +20 -0
  78. package/src/tweaks/feedback.ts +119 -0
  79. package/src/tweaks/fold.ts +21 -0
  80. package/src/tweaks/heavy.ts +302 -0
  81. package/src/tweaks/icons.ts +19 -0
  82. package/src/tweaks/image-store.ts +101 -0
  83. package/src/tweaks/lazy.ts +46 -0
  84. package/src/tweaks/markup.ts +154 -0
  85. package/src/tweaks/panel.ts +869 -0
  86. package/src/tweaks/schema.ts +189 -0
  87. package/src/tweaks/shared.ts +525 -0
  88. package/src/tweaks/single.ts +22 -0
  89. package/src/tweaks/standalone.ts +89 -0
  90. package/src/tweaks/types.ts +526 -0
  91. package/src/tweaks.css +959 -0
  92. package/src/wide-gamut.ts +310 -0
  93. package/dist/tweaks/bezier-2RABC6LD.js +0 -1
  94. package/dist/tweaks/chunk-4ATI34PB.js +0 -1
  95. package/dist/tweaks/chunk-7OSCMZ2J.js +0 -1
  96. package/dist/tweaks/chunk-W3JNIJ2F.js +0 -1
  97. package/dist/tweaks/clip-ECGCECEC.js +0 -1
  98. package/dist/tweaks/colour-2RE3FQ5E.js +0 -1
  99. package/dist/tweaks/gradient-DB6XTDHO.js +0 -1
  100. package/dist/tweaks/heavy-PKOMHB3C.js +0 -1
  101. package/dist/tweaks/image-7MCLBOZ3.js +0 -1
  102. package/dist/tweaks/interval-L2E4L3AZ.js +0 -1
  103. package/dist/tweaks/markup-ZJJ6CCWM.js +0 -1
  104. package/dist/tweaks/monitor-IEK742PG.js +0 -2
  105. package/dist/tweaks/motion-N5UNVR55.js +0 -1
  106. package/dist/tweaks/plot-VVTOY3RA.js +0 -2
  107. package/dist/tweaks/point-YMMHSLO7.js +0 -1
  108. package/dist/tweaks/shadow-IKYOV3VN.js +0 -1
  109. package/dist/tweaks/sides-FH227Z54.js +0 -1
  110. package/dist/tweaks/spring-3PKAFIH7.js +0 -1
  111. package/dist/tweaks/tabs-BKK5NIIY.js +0 -1
@@ -0,0 +1,189 @@
1
+ /* Schema derivation — ONE meta derivation for both entry points: a schema value (shorthand
2
+ * or verbose `{ type }`), and a [data-tw] dataset through markup.ts, parses into the same control meta. A lazy
3
+ * control that reads its own fields needs no table entry. test/registry.test.mjs cross-checks the tables. */
4
+ import { titleCase, isObj, isColorStr, optValue, getControl, controlTypes, hasOwn, isReservedKey, SPRING_KEYS, notice } from "./shared.js";
5
+ import { LAZY_IMPORT } from "./lazy.js";
6
+ import type { SchemaObject, Option, Get, Control } from "./types.js";
7
+
8
+ /** A point control's component spec, as the public verbose form declares it. */
9
+ type PointComponent = Extract<SchemaObject, { type: "point"; components: ReadonlyArray<unknown> }>["components"][number];
10
+ /** One page of a tabs control: its slug key, title and the metas built inside it. */
11
+ interface TabPage { key: string; title: string; children: Meta[] }
12
+ /** The normalized control meta — ONE shape for every control type. The per-type fields are
13
+ * optional; type / key / label are always set. `value` is the authored default in the control's own shape. */
14
+ interface Meta {
15
+ type: string; key: string; label: string;
16
+ value?: any; // host-authored, in the control's own shape
17
+ // Per-control options (ControlOptions), attached by metaFor.
18
+ render?: (get: Get) => boolean; disabled?: boolean | ((get: Get) => boolean); hint?: string; persist?: boolean;
19
+ // Ranges — slider, number, interval, monitor.
20
+ min?: number; max?: number; step?: number; soft?: boolean; unit?: string; corners?: boolean; // corners: the sides control labels its four as corners and floors them at 0
21
+ format?: (value: number) => string; parse?: (text: string) => number; // slider: readout from the value, and a typed one back
22
+ // Single-select — list, radiogrid.
23
+ options?: Option[]; cols?: number;
24
+ // Text.
25
+ rows?: number; placeholder?: string;
26
+ // Spring (also tolerated top-level, beside the normalized `value`).
27
+ mode?: "time" | "physics"; stiffness?: number; damping?: number; mass?: number; visualDuration?: number; bounce?: number;
28
+ // Point.
29
+ components?: PointComponent[]; pad?: boolean; invertY?: boolean;
30
+ // Gradient: the verbose form may say `stops` for `value`.
31
+ stops?: unknown;
32
+ // Plot.
33
+ expr?: string; fn?: ((x: number) => number) | null; xMin?: number; xMax?: number; yMin?: number; yMax?: number; samples?: number; editable?: boolean;
34
+ // Monitor.
35
+ get?: () => number | string; graph?: boolean; view?: "graph" | "text"; interval?: number; decimals?: number;
36
+ // Image (`options` and `cols` above).
37
+ maxSize?: number; fit?: number | false; quality?: number; upload?: boolean; clear?: boolean;
38
+ // Button / button group.
39
+ action?: () => void; buttons?: Record<string, () => void> | Array<{ label: string; action: () => void }>;
40
+ // Folder / tabs.
41
+ children?: Meta[]; pages?: TabPage[];
42
+ }
43
+ /** A verbose handler's result: the control's own fields, or falsy for a malformed shape. */
44
+ type MetaFields = Partial<Meta> | false;
45
+
46
+ // Parse one schema entry → a control meta, or null for an unknown shape. Per-control options
47
+ // (render / disabled / hint / persist) ride on any object-form value, whatever control it infers.
48
+ function metaFor(key: string, value: any, depth = 0): Meta | null {
49
+ if (isReservedKey(key)) return null;
50
+ const meta = baseMetaFor(key, value, depth);
51
+ if (meta && value && typeof value === "object") {
52
+ if (typeof value.render === "function") meta.render = value.render;
53
+ if (value.disabled != null) meta.disabled = value.disabled;
54
+ if (value.hint != null) meta.hint = String(value.hint);
55
+ if (value.persist === false) meta.persist = false;
56
+ }
57
+ return meta;
58
+ }
59
+ // ── Verbose `{ type: "…" }` forms — one handler per control type, returning only the control's
60
+ // own fields; baseMetaFor stamps type / key / label on top, and a handler may override them. A
61
+ // falsy return marks a malformed shape, skipped with a console error like a throw. ──
62
+ // "segmented" is an alias: a single-select from a list renders as the radio grid; the pill is for booleans.
63
+ const radiogridMeta = (v: any): MetaFields => Array.isArray(v.options) && { type: "radiogrid", options: v.options, value: v.value ?? optValue(v.options[0]), cols: v.cols };
64
+ // `??` semantics: an explicit "" (no label) survives where `||` fell back to the key.
65
+ const ownLabel = (v: any, label: string) => (v.label == null ? label : String(v.label));
66
+ const fnOf = (f: unknown) => (typeof f === "function" ? (f as any) : undefined);
67
+ const unitOf = (u: unknown) => (typeof u === "string" && u.trim() ? u.trim().slice(0, 12) : undefined);
68
+ // Typed against the public SchemaObject union, so tsc flags a control type added to types.ts
69
+ // but missing here. "button" has no handler: the `{ action }` shorthand covers its verbose form.
70
+ const TYPED_META: Partial<Record<SchemaObject["type"], (v: any, depth: number, key: string) => MetaFields>> = {
71
+ // Absent min/max derive in the control from the value the range must contain, like the bare-number shorthand:
72
+ // `{ type: "slider", value: 50 }` spans 0–150 like `size: 50`.
73
+ slider: (v) => ({ value: v.value ?? v.min ?? 0, min: v.min, max: v.max, step: v.step, soft: v.soft, unit: unitOf(v.unit), format: fnOf(v.format), parse: fnOf(v.parse) }),
74
+ number: (v) => ({ value: v.value ?? 0, min: v.min, max: v.max, step: v.step, soft: v.soft, unit: unitOf(v.unit) }), // no step: the number control (heavy.ts) fits one to the default
75
+ checkbox: (v) => ({ value: !!v.value }),
76
+ radiogrid: radiogridMeta,
77
+ segmented: radiogridMeta,
78
+ list: (v) => Array.isArray(v.options) && { options: v.options, value: v.value ?? optValue(v.options[0]) },
79
+
80
+ text: (v) => ({ value: v.value ?? "", rows: v.rows, placeholder: v.placeholder }),
81
+ // The config reads off the top level or a nested `value: {…}`, both published forms; the time
82
+ // pair and mode ride along only when present, so the control can infer the mode.
83
+ spring: (v) => {
84
+ const s = isObj(v.value) ? v.value : v;
85
+ const value: any = { stiffness: s.stiffness ?? 300, damping: s.damping ?? 26, mass: s.mass ?? 1 };
86
+ if (Number.isFinite(+s.visualDuration)) value.visualDuration = +s.visualDuration;
87
+ if (Number.isFinite(+s.bounce)) value.bounce = +s.bounce;
88
+ const mode = v.mode ?? s.mode;
89
+ if (mode === "time" || mode === "physics") value.mode = mode; // rides on the value, so a reset restores the mode
90
+ return { value };
91
+ },
92
+
93
+ // Either mode's fields, top-level or nested, or `value` as a bare curve; the control infers
94
+ // the mode (spring keys → spring) and fills the defaults.
95
+ motion: (v) => {
96
+ const s = typeof v.value === "string" || Array.isArray(v.value) ? { ...v, curve: v.value } : isObj(v.value) ? v.value : v; // the array test first: isObj is true of an array
97
+ const springy = SPRING_KEYS.some((k) => s[k] != null);
98
+ // Both sides get a default, so a reset restores the editor you weren't looking at too.
99
+ const value: any = { curve: "ease", duration: 300, ...(springy ? {} : { visualDuration: 0.5, bounce: 0.2 }) };
100
+ for (const k of ["mode", "curve", "duration", ...SPRING_KEYS]) if (s[k] != null) value[k] = s[k];
101
+ if (value.mode !== "easing" && value.mode !== "spring") value.mode = springy ? "spring" : "easing";
102
+ return { value };
103
+ },
104
+ point: (v, _depth, at) => {
105
+ // `value: { x: 0, y: 0 }`, the shape the control hands back, names the components itself; a value beside components sets
106
+ // their defaults. Without either, it's a folder with a child named `type`, as before. A value's component is a finite
107
+ // number or a numeric string; anything else (null, true, "abc") starts at 0 with one notice. A reserved key is skipped.
108
+ const bad: string[] = [];
109
+ const num = (k: string, n: unknown) => { const x = typeof n === "number" ? n : typeof n === "string" && n.trim() ? +n : NaN; if (Number.isFinite(x)) return x; bad.push(k); return 0; };
110
+ const named = isObj(v.value) && !Array.isArray(v.value) ? Object.keys(v.value).filter((k) => !isReservedKey(k)) : [];
111
+ if ((!Array.isArray(v.components) || !v.components.length) && named.length) v = { ...v, components: named.map((key) => ({ key })) };
112
+ if (!Array.isArray(v.components) || !v.components.length) return false;
113
+ // A keyless component takes its label (lower-cased) or index; keys dedupe so two can't share one param.
114
+ const used = new Set<string>();
115
+ const components = v.components.flatMap((c: any, k: number) => {
116
+ const label = c.label ? String(c.label).toLowerCase() : "";
117
+ const base = String(c.key ?? ((isReservedKey(label) ? "" : label) || `c${k}`));
118
+ if (isReservedKey(base)) return [];
119
+ let key = base, n = 2; while (used.has(key)) key = `${base}-${n++}`; used.add(key);
120
+ const own = isObj(v.value) && hasOwn(v.value, key) ? num(key, v.value[key]) : undefined;
121
+ return [{ ...c, key, ...(own !== undefined && { value: own }) }];
122
+ });
123
+ if (bad.length) notice("pointValue", at, bad.join(", "));
124
+ if (!components.length) return false;
125
+ return { components, pad: v.pad, invertY: v.invertY, value: Object.fromEntries(components.map((c: PointComponent) => [c.key, c.value ?? 0])) }; // the default component map, for reset()
126
+ },
127
+ buttongroup: (v) => ({ buttons: v.buttons }),
128
+ separator: () => ({}),
129
+ // Page keys dedupe ("A!" and "A?" both slug to "a") so two pages can't share one params subtree.
130
+ tabs: (v, depth) => {
131
+ if (!v.pages || typeof v.pages !== "object" || !Object.keys(v.pages).length) return false; // no pages is nothing to show, not an empty bar
132
+ const used = new Set<string>();
133
+ return { pages: Object.entries(v.pages).map(([title, schema]: [string, any]) => {
134
+ const base = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tab";
135
+ let k = base, n = 2; while (used.has(k)) k = `${base}-${n++}`; used.add(k);
136
+ return { key: k, title: title.trim() ? title : "Tab", children: Object.entries(schema).map(([ck, sv]) => metaFor(ck, sv, depth + 1)).filter(Boolean) }; // an empty title still gets a visible tab
137
+ }) };
138
+ },
139
+ };
140
+
141
+ // The field each falsy-capable form hinges on: present-but-unusable is malformed, absent is a folder.
142
+ const REQUIRED: Record<string, string> = Object.assign(Object.create(null), { list: "options", radiogrid: "options", segmented: "options", point: "components", tabs: "pages" });
143
+ function baseMetaFor(key: string, value: any, depth = 0): Meta | null {
144
+ if (depth > 64) return null; // a pathological depth is skipped, not a RangeError
145
+ const label = titleCase(key);
146
+ const meta = (type: string, fields: Partial<Meta>, lab = label): Meta => ({ type, key, label: lab, ...fields });
147
+ if (isObj(value) && !Array.isArray(value) && hasOwn(TYPED_META, value.type)) { // own key only: "toString" must miss
148
+ // A handler that throws or returns nothing for a malformed shape skips that control, not
149
+ // the whole panel. The label is resolved here, once, so every verbose form honors `{ label }`.
150
+ let fields: MetaFields;
151
+ try { fields = TYPED_META[value.type as keyof typeof TYPED_META]!(value, depth, key); } // a guard can't narrow a property of an `any`
152
+ catch (e) { console.error(`[tweaks] malformed "${value.type}" schema value for "${key}" — control skipped:`, e); return null; }
153
+ if (fields) return meta(String(value.type), fields, ownLabel(value, label)); // a String object would miss m.type === "tabs" downstream
154
+ // Nothing back with the hinge field present is malformed. Absent, this is a plain folder
155
+ // holding a child named `type` (`marker: { type: "point", size: 3 }`) — fall through.
156
+ if (hasOwn(value, REQUIRED[value.type])) { console.error(`[tweaks] malformed "${value.type}" schema value for "${key}" — control skipped`); return null; }
157
+ }
158
+ // A lazy control with no handler reads its own fields off the verbose form as given.
159
+ else if (isObj(value) && !Array.isArray(value) && typeof value.type === "string" && (LAZY_IMPORT[value.type] || getControl(value.type))) {
160
+ const { type, label: _l, ...fields } = value;
161
+ return meta(type, fields, ownLabel(value, label));
162
+ }
163
+ // A `type` no control answers to: a typo (`slidr`) built a folder of `type` and `value` rows without a word. It may be a
164
+ // folder with a child named `type`, so it still builds one; the console says so, with the near type, from a lazy chunk.
165
+ else if (isObj(value) && !Array.isArray(value) && typeof value.type === "string" && !hasOwn(TYPED_META, value.type))
166
+ import("./heavy.js").then((m) => m.unknownType(`"${key}"`, value.type, [...Object.keys(TYPED_META), ...Object.keys(LAZY_IMPORT), ...controlTypes()], "builds a folder"), () => {});
167
+ // ── Shorthand inference ──
168
+ // Interval / range: [[lo, hi], min, max, step?] — the first entry is a 2-tuple. Missing bounds and step derive in the
169
+ // control, as for `{ type: "interval" }`: [[20, 80]] spans 0–240 (the tuple's own 20–80 pinned both handles).
170
+ if (Array.isArray(value) && Array.isArray(value[0]) && value[0].length === 2 && typeof value[0][0] === "number")
171
+ return meta("interval", { value: value[0].map(Number), min: value[1], max: value[2], step: value[3] });
172
+ // [value, min?, max?, step?]: a short array ([n] = "just a default") takes the bare-number range. The slider derives
173
+ // a missing or non-number bound and infers the step after, so [5, NaN] can't double 5 on a NaN span.
174
+ if (Array.isArray(value) && value.length <= 4 && typeof value[0] === "number") return meta("slider", { value: value[0], min: value[1], max: value[2], step: value[3] });
175
+ if (typeof value === "number") return meta("slider", { value });
176
+ if (typeof value === "boolean") return meta("checkbox", { value });
177
+ if (Array.isArray(value)) return meta("list", { options: value, value: optValue(value[0]) });
178
+ if (isObj(value) && typeof value.action === "function") return meta("button", { action: value.action }, ownLabel(value, label));
179
+ if (isObj(value) && Array.isArray(value.options)) return meta("list", { options: value.options, value: value.value ?? optValue(value.options[0]) });
180
+ if (isColorStr(value)) return meta("color", { value });
181
+ if (typeof value === "string") return meta("text", { value });
182
+ // render / disabled / hint are the folder's chrome, not children.
183
+ if (isObj(value)) return meta("folder", { children: Object.entries(value).filter(([k, v]) => !(k === "render" && typeof v === "function") && !((k === "disabled" || k === "hint") && v != null)).map(([k, v]) => metaFor(k, v, depth + 1)).filter(Boolean) });
184
+ return null;
185
+ }
186
+ // Display/action controls carry no value; the panel build skips entry/reset/persist wiring.
187
+ const VALUELESS = new Set<string>(["button", "fpsgraph", "monitor", "buttongroup", "separator"]);
188
+ export { metaFor, VALUELESS, TYPED_META };
189
+ export type { Meta, TabPage, PointComponent };