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.
- package/AGENTS.md +32 -30
- package/README.md +55 -29
- package/THIRD-PARTY-NOTICES.md +0 -1
- package/dist/tweaks/auto.js +2 -0
- package/dist/tweaks/auto.js.map +6 -0
- package/dist/tweaks/bezier-S2BOXQBM.js +2 -0
- package/dist/tweaks/bezier-S2BOXQBM.js.map +6 -0
- package/dist/tweaks/chunk-3MZAAMC2.js +2 -0
- package/dist/tweaks/chunk-3MZAAMC2.js.map +6 -0
- package/dist/tweaks/chunk-ETB35KFV.js +2 -0
- package/dist/tweaks/chunk-ETB35KFV.js.map +6 -0
- package/dist/tweaks/chunk-YB5GKWZI.js +2 -0
- package/dist/tweaks/chunk-YB5GKWZI.js.map +6 -0
- package/dist/tweaks/clip-A3VI53BK.js +2 -0
- package/dist/tweaks/clip-A3VI53BK.js.map +6 -0
- package/dist/tweaks/color-UGYMAF3W.js +2 -0
- package/dist/tweaks/color-UGYMAF3W.js.map +6 -0
- package/dist/tweaks/core.js +2 -1
- package/dist/tweaks/core.js.map +6 -0
- package/dist/tweaks/fold-GY47HIQA.js +2 -0
- package/dist/tweaks/fold-GY47HIQA.js.map +6 -0
- package/dist/tweaks/gradient-BTX25WSH.js +2 -0
- package/dist/tweaks/gradient-BTX25WSH.js.map +6 -0
- package/dist/tweaks/heavy-NPFIGHRM.js +2 -0
- package/dist/tweaks/heavy-NPFIGHRM.js.map +6 -0
- package/dist/tweaks/image-HTQQ4WX6.js +4 -0
- package/dist/tweaks/image-HTQQ4WX6.js.map +6 -0
- package/dist/tweaks/image-store-QD5SMZXG.js +2 -0
- package/dist/tweaks/image-store-QD5SMZXG.js.map +6 -0
- package/dist/tweaks/interval-BHPI4LRN.js +2 -0
- package/dist/tweaks/interval-BHPI4LRN.js.map +6 -0
- package/dist/tweaks/markup-QM53TQJZ.js +2 -0
- package/dist/tweaks/markup-QM53TQJZ.js.map +6 -0
- package/dist/tweaks/monitor-722WIYGW.js +3 -0
- package/dist/tweaks/monitor-722WIYGW.js.map +6 -0
- package/dist/tweaks/motion-HMCZK6HJ.js +2 -0
- package/dist/tweaks/motion-HMCZK6HJ.js.map +6 -0
- package/dist/tweaks/plot-D3NQ6XNN.js +3 -0
- package/dist/tweaks/plot-D3NQ6XNN.js.map +6 -0
- package/dist/tweaks/point-XDXKMDXH.js +2 -0
- package/dist/tweaks/point-XDXKMDXH.js.map +6 -0
- package/dist/tweaks/presets-EUWNSCUW.js +2 -0
- package/dist/tweaks/presets-EUWNSCUW.js.map +6 -0
- package/dist/tweaks/shadow-AM7VSRNS.js +2 -0
- package/dist/tweaks/shadow-AM7VSRNS.js.map +6 -0
- package/dist/tweaks/shadow-root-RYUV3AGS.js +3 -0
- package/dist/tweaks/shadow-root-RYUV3AGS.js.map +6 -0
- package/dist/tweaks/sides-JWIURDA4.js +2 -0
- package/dist/tweaks/sides-JWIURDA4.js.map +6 -0
- package/dist/tweaks/spring-BF5GBTPM.js +2 -0
- package/dist/tweaks/spring-BF5GBTPM.js.map +6 -0
- package/dist/tweaks/tabs-PRVUUZXZ.js +2 -0
- package/dist/tweaks/tabs-PRVUUZXZ.js.map +6 -0
- package/dist/tweaks.css +1 -1
- package/dist/tweaks.js +7 -3
- package/dist/tweaks.js.map +6 -0
- package/dist/types/tweaks/easing.d.ts +8 -11
- package/dist/types/tweaks/enhance.d.ts +3 -2
- package/dist/types/tweaks/panel.d.ts +6 -4
- package/dist/types/tweaks/schema.d.ts +4 -8
- package/dist/types/tweaks/standalone.d.ts +3 -3
- package/dist/types/tweaks/types.d.ts +499 -116
- package/package.json +30 -5
- package/src/tweaks/auto.ts +16 -0
- package/src/tweaks/clip.ts +156 -0
- package/src/tweaks/controls/basic.ts +727 -0
- package/src/tweaks/controls/bezier.ts +204 -0
- package/src/tweaks/controls/color.ts +861 -0
- package/src/tweaks/controls/gradient.ts +419 -0
- package/src/tweaks/controls/image.ts +555 -0
- package/src/tweaks/controls/interval.ts +189 -0
- package/src/tweaks/controls/monitor.ts +211 -0
- package/src/tweaks/controls/motion.ts +316 -0
- package/src/tweaks/controls/plot.ts +440 -0
- package/src/tweaks/controls/point.ts +208 -0
- package/src/tweaks/controls/shadow.ts +156 -0
- package/src/tweaks/controls/sides.ts +196 -0
- package/src/tweaks/controls/spring.ts +308 -0
- package/src/tweaks/controls/tabs.ts +68 -0
- package/src/tweaks/core.ts +9 -0
- package/src/tweaks/easing.ts +118 -0
- package/src/tweaks/enhance.ts +35 -0
- package/src/tweaks/feedback.ts +218 -0
- package/src/tweaks/fold.ts +33 -0
- package/src/tweaks/heavy.ts +626 -0
- package/src/tweaks/icons.ts +26 -0
- package/src/tweaks/image-store.ts +189 -0
- package/src/tweaks/lazy.ts +63 -0
- package/src/tweaks/markup.ts +317 -0
- package/src/tweaks/panel.ts +1693 -0
- package/src/tweaks/presets.ts +85 -0
- package/src/tweaks/schema.ts +351 -0
- package/src/tweaks/shadow-root.ts +178 -0
- package/src/tweaks/shared.ts +1137 -0
- package/src/tweaks/single.ts +22 -0
- package/src/tweaks/standalone.ts +179 -0
- package/src/tweaks/types.ts +1027 -0
- package/src/tweaks/undo.ts +104 -0
- package/src/tweaks.css +965 -0
- package/src/wide-gamut.ts +658 -0
- package/dist/tweaks/bezier-2RABC6LD.js +0 -1
- package/dist/tweaks/chunk-4ATI34PB.js +0 -1
- package/dist/tweaks/chunk-7OSCMZ2J.js +0 -1
- package/dist/tweaks/chunk-W3JNIJ2F.js +0 -1
- package/dist/tweaks/clip-ECGCECEC.js +0 -1
- package/dist/tweaks/colour-2RE3FQ5E.js +0 -1
- package/dist/tweaks/fold-UGN7TXGX.js +0 -1
- package/dist/tweaks/gradient-DB6XTDHO.js +0 -1
- package/dist/tweaks/heavy-PKOMHB3C.js +0 -1
- package/dist/tweaks/image-7MCLBOZ3.js +0 -1
- package/dist/tweaks/interval-L2E4L3AZ.js +0 -1
- package/dist/tweaks/markup-ZJJ6CCWM.js +0 -1
- package/dist/tweaks/monitor-IEK742PG.js +0 -2
- package/dist/tweaks/motion-N5UNVR55.js +0 -1
- package/dist/tweaks/plot-VVTOY3RA.js +0 -2
- package/dist/tweaks/point-YMMHSLO7.js +0 -1
- package/dist/tweaks/shadow-IKYOV3VN.js +0 -1
- package/dist/tweaks/sides-FH227Z54.js +0 -1
- package/dist/tweaks/spring-3PKAFIH7.js +0 -1
- package/dist/tweaks/tabs-BKK5NIIY.js +0 -1
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tweakit",
|
|
3
|
-
"version": "0.
|
|
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
|
|
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
|
-
"
|
|
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
|
+
}
|