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
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "tweakit",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
- "main": "./dist/tweaks.js",
5
+ "main": "./dist/tweaks/core.js",
6
6
  "types": "./dist/types/tweaks/core.d.ts",
7
- "description": "A dependency-free, code-split real-time parameter panel — sliders, toggles, a wide-gamut OKLCH colour picker, curve editors and more, from a plain schema.",
7
+ "description": "A dependency-free, code-split real-time parameter panel — sliders, toggles, a wide-gamut OKLCH color picker, curve editors and more, from a plain schema.",
8
8
  "license": "MIT",
9
9
  "author": "Ryan Kiley",
10
10
  "repository": {
@@ -28,12 +28,21 @@
28
28
  ],
29
29
  "scripts": {
30
30
  "build": "node build.mjs",
31
- "test": "node build.mjs && node --test \"test/*.test.mjs\"",
31
+ "test": "node build.mjs && node --enable-source-maps --test \"test/*.test.mjs\"",
32
32
  "prepublishOnly": "npm test",
33
- "serve": "node build.mjs && python3 -m http.server 4330 --directory dist"
33
+ "prepack": "node build.mjs",
34
+ "serve": "node build.mjs && python3 -m http.server 4330 --directory dist",
35
+ "test:browser": "node build.mjs && node --test \"test/browser/*.test.mjs\"",
36
+ "format": "prettier --write .",
37
+ "format:check": "prettier --check ."
34
38
  },
35
39
  "exports": {
36
40
  ".": {
41
+ "types": "./dist/types/tweaks/core.d.ts",
42
+ "import": "./dist/tweaks/core.js",
43
+ "default": "./dist/tweaks/core.js"
44
+ },
45
+ "./single": {
37
46
  "types": "./dist/types/tweaks/core.d.ts",
38
47
  "import": "./dist/tweaks.js",
39
48
  "default": "./dist/tweaks.js"
@@ -43,24 +52,40 @@
43
52
  "import": "./dist/tweaks/core.js",
44
53
  "default": "./dist/tweaks/core.js"
45
54
  },
55
+ "./auto": {
56
+ "types": "./dist/types/tweaks/core.d.ts",
57
+ "import": "./dist/tweaks/auto.js",
58
+ "default": "./dist/tweaks/auto.js"
59
+ },
46
60
  "./css": {
47
61
  "types": "./dist/tweaks.css.d.ts",
48
62
  "default": "./dist/tweaks.css"
49
63
  },
50
64
  "./package.json": "./package.json"
51
65
  },
66
+ "sideEffects": [
67
+ "./dist/tweaks.css",
68
+ "./dist/tweaks/auto.js",
69
+ "./dist/tweaks/*-*.js",
70
+ "./src/**"
71
+ ],
52
72
  "files": [
53
73
  "/AGENTS.md",
54
74
  "dist/tweaks",
55
75
  "dist/tweaks.js",
76
+ "dist/tweaks.js.map",
56
77
  "dist/tweaks.css",
57
78
  "dist/tweaks.css.d.ts",
58
79
  "dist/types",
80
+ "src",
59
81
  "THIRD-PARTY-NOTICES.md"
60
82
  ],
61
83
  "devDependencies": {
62
84
  "esbuild": "^0.28.0",
85
+ "fake-indexeddb": "^6.2.6",
63
86
  "jsdom": "^30.1.2",
87
+ "playwright": "^1.64.1",
88
+ "prettier": "3.9.10",
64
89
  "typescript": "^7.0.2"
65
90
  }
66
91
  }
@@ -0,0 +1,16 @@
1
+ /* tweakit/auto: the code-split API, plus enhance(document) once the page has parsed. The bare import binds nothing, so
2
+ * a
3
+ * page of [data-twk] markup with no script of its own imports this entry instead. Built on its own, unbundled, so it
4
+ * stays a few lines over ./core.js rather than reshaping the split build's chunks. */
5
+ import { enhance } from "./core.js";
6
+ export * from "./core.js";
7
+
8
+ // A page with nothing to bind doesn't fetch the markup chunk: no [data-twk] host, and no static showcase panel
9
+ // (enhance() wraps one).
10
+ if (typeof document !== "undefined") {
11
+ const run = () => {
12
+ if (document.querySelector('[data-twk], .twk-panel[data-mode="inline"] > .twk-controls')) enhance(document);
13
+ };
14
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", run);
15
+ else run();
16
+ }
@@ -0,0 +1,156 @@
1
+ /* The paste's parse, checks and report: a lazy chunk the first paste fetches, so none of it rides in a basic
2
+ * panel's download. A runtime import here of a module only core reaches (schema.ts, feedback.ts) would split
3
+ * that module into a shared chunk of its own, so the caller lends what it needs (Report, ClipCtx); shared.ts is
4
+ * already shared with the lazy controls. */
5
+ import type { Entry, Snapshot } from "./panel.js";
6
+ import type { PanelState } from "./types.js";
7
+ import type { Meta } from "./schema.js";
8
+ import type { announce } from "./feedback.js";
9
+ import { isObj, isColorStr, optValue, hasOwn, isReservedKey, words } from "./shared.js";
10
+ import type { Tr } from "./shared.js";
11
+
12
+ /** Where a pasted value can land: a control's path, its current value, its meta and its row (a disabled one takes
13
+ * nothing). */
14
+ export interface Slot {
15
+ path: string[];
16
+ get: () => unknown;
17
+ m: Meta;
18
+ el?: Element;
19
+ }
20
+ /** How a paste reports: the words a screen reader hears, and whether its panel is still there. */
21
+ export interface Report {
22
+ announce: typeof announce;
23
+ alive: () => boolean;
24
+ tr?: Tr;
25
+ }
26
+ /** What a panel lends the paste. */
27
+ export interface ClipCtx extends Report {
28
+ panel: HTMLElement;
29
+ entries: Entry[];
30
+ fromJSON: (state: PanelState) => void;
31
+ }
32
+ /** What a scoped ⌘C / ⌘V acts on: one row, or every row under a folder's path. */
33
+ export type Scope = { kind: "row"; entry: Entry; label: string } | { kind: "folder"; path: string[]; label: string };
34
+
35
+ const atPath = (obj: unknown, path: string[]): unknown =>
36
+ path.reduce<any>((o, k) => (isObj(o) && hasOwn(o, k) ? o[k] : undefined), obj);
37
+
38
+ /** ⇧-copy's output is a flat map by dotted path; nested, "folder.child" lands where set() would put it. A reserved
39
+ * segment is dropped, so a pasted "__proto__.x" can't write onto Object.prototype. */
40
+ const unflatten = (flat: Record<string, unknown>): Snapshot => {
41
+ const out: Snapshot = {};
42
+ for (const k of Object.keys(flat)) {
43
+ const path = k.split(".");
44
+ if (path.some(isReservedKey)) continue;
45
+ let o = out;
46
+ for (const s of path.slice(0, -1)) o = isObj(o[s]) ? o[s] : (o[s] = {});
47
+ o[path[path.length - 1]] = flat[k];
48
+ }
49
+ return out;
50
+ };
51
+
52
+ // A pasted value fits where it's going when it has the current value's shape: the same JSON type all the way down, a
53
+ // finite number, a tuple (an interval, a curve) of the same length, a list (a gradient's stops) of 2 to 64 items each
54
+ // shaped like the first, a color the browser can parse wherever a color stands (a color or gradient control), and
55
+ // for a list control one of its options. Anything else would be coerced or ignored by set() — null to 0, a stray string
56
+ // to black, a short tuple dropped — so it's skipped, and counted as not pasted; so is anything for a disabled row.
57
+ const isCssColor = (v: unknown) =>
58
+ isColorStr(v) && (typeof CSS === "undefined" || !CSS.supports || CSS.supports("color", v as string));
59
+ const sameShape = (cur: unknown, v: unknown, col: boolean): boolean => {
60
+ if (Array.isArray(cur))
61
+ return (
62
+ Array.isArray(v) &&
63
+ (cur.every((x) => !isObj(x))
64
+ ? v.length === cur.length && v.every((x, i) => sameShape(cur[i], x, col))
65
+ : v.length >= 2 && v.length <= 64 && v.every((x) => sameShape(cur[0], x, col)))
66
+ );
67
+ if (isObj(cur)) return isObj(v) && !Array.isArray(v) && Object.keys(cur).every((k) => sameShape(cur[k], v[k], col));
68
+ return (
69
+ typeof v === typeof cur &&
70
+ (typeof v !== "number" || Number.isFinite(v)) &&
71
+ (!col || !isColorStr(cur) || isCssColor(v))
72
+ );
73
+ };
74
+ const fits = (s: Slot, v: unknown) =>
75
+ v != null &&
76
+ !s.el?.closest(".is-disabled") &&
77
+ sameShape(s.get(), v, s.m.type === "color" || s.m.type === "gradient") &&
78
+ (!s.m.options || s.m.options.some((o) => JSON.stringify(optValue(o)) === JSON.stringify(v)));
79
+
80
+ /** The values in `values` that fit their slots, each slot read at its path below `base`. */
81
+ export const pick = <S extends Slot>(slots: S[], values: unknown, base: string[] = []): Array<[S, unknown]> =>
82
+ slots.map((s): [S, unknown] => [s, atPath(values, s.path.slice(base.length))]).filter(([s, v]) => fits(s, v));
83
+
84
+ // Only what fits is written, through fromJSON: a bag key or a value set() would coerce stays as it is.
85
+ const write = (ctx: ClipCtx, hits: Array<[Entry, unknown]>) => {
86
+ const out: Snapshot = {};
87
+ for (const [e, v] of hits) {
88
+ let o = out;
89
+ for (const k of e.path.slice(0, -1)) o = o[k] ||= {};
90
+ o[e.path[e.path.length - 1]] = v;
91
+ }
92
+ ctx.fromJSON({ values: out });
93
+ };
94
+
95
+ /** ⌘V on a row or folder. A row takes a bare value or its own one-key object. A folder takes its values object, or any
96
+ * copy that holds the folder at its own path (a whole-panel copy, a changed-only map), only into its own rows: each
97
+ * reading is tried, as it stands and unflattened, at the folder's path and as the folder's own, and the one that lands
98
+ * most wins (a folder whose subfolder shares its name reads its own copy right). */
99
+ export function pasteScope(ctx: ClipCtx, s: Scope, text: string): number | false {
100
+ const data = JSON.parse(text);
101
+ let hits: Array<[Entry, unknown]>;
102
+ if (s.kind === "row") {
103
+ const e = s.entry,
104
+ v =
105
+ isObj(data) && !Array.isArray(data) && Object.keys(data).length === 1 && hasOwn(data, e.key)
106
+ ? data[e.key]
107
+ : data;
108
+ hits = fits(e, v) ? [[e, v]] : [];
109
+ } else {
110
+ if (!isObj(data) || Array.isArray(data)) return false;
111
+ const rows = ctx.entries.filter((e) => e.path.length > s.path.length && s.path.every((k, i) => e.path[i] === k));
112
+ hits = [];
113
+ for (const d of [data, unflatten(data)])
114
+ for (const v of [atPath(d, s.path), d]) {
115
+ const h = isObj(v) ? pick(rows, v, s.path) : [];
116
+ if (h.length > hits.length) hits = h;
117
+ }
118
+ }
119
+ if (!hits.length) return false;
120
+ write(ctx, hits);
121
+ return hits.length;
122
+ }
123
+
124
+ const flashes = new WeakMap<Element, ReturnType<typeof setTimeout>>();
125
+ /** Every paste ends here. `text` is what the browser's paste event carried (null: none); `apply` returns how many
126
+ * values it took, or false for nothing usable (a throw counts the same). A screen reader hears which, and a picked
127
+ * `band` flashes it; the result is whether anything landed, or undefined when the panel went while this chunk
128
+ * loaded. */
129
+ export function pasteInto(text: string | null, apply: (text: string) => number | false, r: Report, band?: Element) {
130
+ if (!r.alive()) return;
131
+ let n: number | false = false;
132
+ if (text !== null)
133
+ try {
134
+ n = apply(text);
135
+ } catch {}
136
+ const tr = r.tr || words();
137
+ r.announce(
138
+ !n
139
+ ? text === null
140
+ ? tr("clipboardUnread", "Couldn't read the clipboard")
141
+ : tr("nothingToPaste", "Nothing to paste")
142
+ : tr("pasted", (v) => `${v.n} value${v.n === 1 ? "" : "s"} pasted`, { n }),
143
+ );
144
+ if (band) {
145
+ // a quick second paste on the same row restarts the flash: the first one's timer mustn't cut it short
146
+ const c = n ? "is-pasted" : "is-refused";
147
+ band.classList.remove("is-pasted", "is-refused");
148
+ band.classList.add(c);
149
+ clearTimeout(flashes.get(band));
150
+ flashes.set(
151
+ band,
152
+ setTimeout(() => band.classList.remove(c), 450),
153
+ );
154
+ }
155
+ return !!n;
156
+ }