tweakit 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +31 -29
- package/README.md +55 -29
- package/THIRD-PARTY-NOTICES.md +0 -1
- package/dist/tweaks/auto.js +2 -0
- package/dist/tweaks/auto.js.map +6 -0
- package/dist/tweaks/bezier-JFEZMCLP.js +2 -0
- package/dist/tweaks/bezier-JFEZMCLP.js.map +6 -0
- package/dist/tweaks/chunk-6UO4DHBN.js +2 -0
- package/dist/tweaks/chunk-6UO4DHBN.js.map +6 -0
- package/dist/tweaks/chunk-DIQMLBAS.js +2 -0
- package/dist/tweaks/chunk-DIQMLBAS.js.map +6 -0
- package/dist/tweaks/chunk-XE6OUJBG.js +2 -0
- package/dist/tweaks/chunk-XE6OUJBG.js.map +6 -0
- package/dist/tweaks/clip-WW7JYO4X.js +2 -0
- package/dist/tweaks/clip-WW7JYO4X.js.map +6 -0
- package/dist/tweaks/color-YMMFURP6.js +2 -0
- package/dist/tweaks/color-YMMFURP6.js.map +6 -0
- package/dist/tweaks/core.js +2 -1
- package/dist/tweaks/core.js.map +6 -0
- package/dist/tweaks/{fold-UGN7TXGX.js → fold-U6YRZLCJ.js} +2 -1
- package/dist/tweaks/fold-U6YRZLCJ.js.map +6 -0
- package/dist/tweaks/gradient-JER7PW4D.js +2 -0
- package/dist/tweaks/gradient-JER7PW4D.js.map +6 -0
- package/dist/tweaks/heavy-CLD7UAVI.js +2 -0
- package/dist/tweaks/heavy-CLD7UAVI.js.map +6 -0
- package/dist/tweaks/image-H6QSLGO3.js +4 -0
- package/dist/tweaks/image-H6QSLGO3.js.map +6 -0
- package/dist/tweaks/image-store-WRKY2NPU.js +2 -0
- package/dist/tweaks/image-store-WRKY2NPU.js.map +6 -0
- package/dist/tweaks/interval-TEUHQGZN.js +2 -0
- package/dist/tweaks/interval-TEUHQGZN.js.map +6 -0
- package/dist/tweaks/markup-NIBJD6E2.js +2 -0
- package/dist/tweaks/markup-NIBJD6E2.js.map +6 -0
- package/dist/tweaks/monitor-3BY7YCGX.js +3 -0
- package/dist/tweaks/monitor-3BY7YCGX.js.map +6 -0
- package/dist/tweaks/motion-FQPUBKKM.js +2 -0
- package/dist/tweaks/motion-FQPUBKKM.js.map +6 -0
- package/dist/tweaks/plot-2HBNBZSG.js +3 -0
- package/dist/tweaks/plot-2HBNBZSG.js.map +6 -0
- package/dist/tweaks/point-VV6XV3I6.js +2 -0
- package/dist/tweaks/point-VV6XV3I6.js.map +6 -0
- package/dist/tweaks/shadow-5E4E55I2.js +2 -0
- package/dist/tweaks/shadow-5E4E55I2.js.map +6 -0
- package/dist/tweaks/sides-P3STEVYY.js +2 -0
- package/dist/tweaks/sides-P3STEVYY.js.map +6 -0
- package/dist/tweaks/spring-HUVSXNVN.js +2 -0
- package/dist/tweaks/spring-HUVSXNVN.js.map +6 -0
- package/dist/tweaks/tabs-76FW7LQ7.js +2 -0
- package/dist/tweaks/tabs-76FW7LQ7.js.map +6 -0
- package/dist/tweaks.css +1 -1
- package/dist/tweaks.js +6 -3
- package/dist/tweaks.js.map +6 -0
- package/dist/types/tweaks/easing.d.ts +8 -11
- package/dist/types/tweaks/panel.d.ts +6 -4
- package/dist/types/tweaks/schema.d.ts +4 -8
- package/dist/types/tweaks/standalone.d.ts +3 -3
- package/dist/types/tweaks/types.d.ts +278 -101
- package/package.json +27 -5
- package/src/tweaks/auto.ts +12 -0
- package/src/tweaks/clip.ts +94 -0
- package/src/tweaks/controls/basic.ts +403 -0
- package/src/tweaks/controls/bezier.ts +99 -0
- package/src/tweaks/controls/color.ts +412 -0
- package/src/tweaks/controls/gradient.ts +240 -0
- package/src/tweaks/controls/image.ts +299 -0
- package/src/tweaks/controls/interval.ts +104 -0
- package/src/tweaks/controls/monitor.ts +105 -0
- package/src/tweaks/controls/motion.ts +141 -0
- package/src/tweaks/controls/plot.ts +220 -0
- package/src/tweaks/controls/point.ts +119 -0
- package/src/tweaks/controls/shadow.ts +83 -0
- package/src/tweaks/controls/sides.ts +86 -0
- package/src/tweaks/controls/spring.ts +148 -0
- package/src/tweaks/controls/tabs.ts +47 -0
- package/src/tweaks/core.ts +9 -0
- package/src/tweaks/easing.ts +93 -0
- package/src/tweaks/enhance.ts +20 -0
- package/src/tweaks/feedback.ts +119 -0
- package/src/tweaks/fold.ts +21 -0
- package/src/tweaks/heavy.ts +302 -0
- package/src/tweaks/icons.ts +19 -0
- package/src/tweaks/image-store.ts +101 -0
- package/src/tweaks/lazy.ts +46 -0
- package/src/tweaks/markup.ts +154 -0
- package/src/tweaks/panel.ts +869 -0
- package/src/tweaks/schema.ts +189 -0
- package/src/tweaks/shared.ts +525 -0
- package/src/tweaks/single.ts +22 -0
- package/src/tweaks/standalone.ts +89 -0
- package/src/tweaks/types.ts +526 -0
- package/src/tweaks.css +959 -0
- package/src/wide-gamut.ts +310 -0
- package/dist/tweaks/bezier-2RABC6LD.js +0 -1
- package/dist/tweaks/chunk-4ATI34PB.js +0 -1
- package/dist/tweaks/chunk-7OSCMZ2J.js +0 -1
- package/dist/tweaks/chunk-W3JNIJ2F.js +0 -1
- package/dist/tweaks/clip-ECGCECEC.js +0 -1
- package/dist/tweaks/colour-2RE3FQ5E.js +0 -1
- package/dist/tweaks/gradient-DB6XTDHO.js +0 -1
- package/dist/tweaks/heavy-PKOMHB3C.js +0 -1
- package/dist/tweaks/image-7MCLBOZ3.js +0 -1
- package/dist/tweaks/interval-L2E4L3AZ.js +0 -1
- package/dist/tweaks/markup-ZJJ6CCWM.js +0 -1
- package/dist/tweaks/monitor-IEK742PG.js +0 -2
- package/dist/tweaks/motion-N5UNVR55.js +0 -1
- package/dist/tweaks/plot-VVTOY3RA.js +0 -2
- package/dist/tweaks/point-YMMHSLO7.js +0 -1
- package/dist/tweaks/shadow-IKYOV3VN.js +0 -1
- package/dist/tweaks/sides-FH227Z54.js +0 -1
- package/dist/tweaks/spring-3PKAFIH7.js +0 -1
- package/dist/tweaks/tabs-BKK5NIIY.js +0 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/* Markup-driven enhancement — the work behind enhance(): it turns [data-tw] hosts into live controls, sharing the
|
|
2
|
+
* panel's meta derivation through the kit. A lazy chunk in the code-split build, fetched by the first enhance() on a page
|
|
3
|
+
* with markup (single.ts imports it statically). It imports shared.ts alone: a panel module imported here as well as by
|
|
4
|
+
* core would become a shared chunk every basic panel fetches, so core lends what it needs (enhance.ts builds the kit). */
|
|
5
|
+
import { el, btn, foldBody, getControl, titleCase, setMarkup, controlTypes, hasOwn, valueChanged, restoreDefault, SPRING_KEYS } from "./shared.js";
|
|
6
|
+
import type { metaFor } from "./schema.js";
|
|
7
|
+
import type { ensureForMetas, LAZY_IMPORT } from "./lazy.js";
|
|
8
|
+
import type { createFolder, createControl } from "./controls/basic.js";
|
|
9
|
+
import type { makeCopyBtn, makeResetBtn, addHintMarker } from "./feedback.js";
|
|
10
|
+
import type { Control, SchemaObject } from "./types.js";
|
|
11
|
+
|
|
12
|
+
/** What core lends the markup chunk. */
|
|
13
|
+
export interface MarkupKit {
|
|
14
|
+
metaFor: typeof metaFor;
|
|
15
|
+
ensureForMetas: typeof ensureForMetas; LAZY_IMPORT: typeof LAZY_IMPORT; createFolder: typeof createFolder; createControl: typeof createControl;
|
|
16
|
+
makeCopyBtn: typeof makeCopyBtn; makeResetBtn: typeof makeResetBtn; addHintMarker: typeof addHintMarker;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── The markup forms — each [data-tw] type parses its dataset into the same verbose
|
|
20
|
+
// schema value TYPED_META consumes, then rides metaFor: ONE derivation, so markup defaults can't drift. ──
|
|
21
|
+
const springData = (d: DOMStringMap) => ({ ...Object.fromEntries(SPRING_KEYS.map((k) => [k, num(d[k])])), mode: d.mode }); // the motion control reads the same markup
|
|
22
|
+
const choices = (d: DOMStringMap) => ({ options: splitList(d.options), value: d.value, cols: num(d.cols) }); // the radio grid, by either name
|
|
23
|
+
const DATA_VALUE: Partial<Record<SchemaObject["type"], (d: DOMStringMap, host: HTMLElement, label: string) => object>> = {
|
|
24
|
+
slider: (d) => ({ value: num(d.value), min: num(d.min), max: num(d.max), step: num(d.step), soft: flag(d.soft), unit: d.unit }), // absent bounds derive from the value, like the schema
|
|
25
|
+
checkbox: (d) => (d.options ? { type: "radiogrid", ...choices(d) } : { value: d.checked === "true" }),
|
|
26
|
+
radiogrid: choices,
|
|
27
|
+
segmented: choices,
|
|
28
|
+
list: (d) => ({ options: splitList(d.options), value: d.value }),
|
|
29
|
+
// data-options is a comma list, so it takes URLs, not data URLs (they carry a comma).
|
|
30
|
+
image: (d) => ({ value: d.value, options: d.options ? splitList(d.options) : undefined, cols: num(d.cols), maxSize: num(d.maxSize), fit: d.fit === "false" ? false : num(d.fit), quality: num(d.quality), upload: d.upload === "false" ? false : undefined, clear: d.clear === "false" ? false : undefined }),
|
|
31
|
+
buttongroup: (d) => ({ buttons: splitList(d.buttons).map((lab) => ({ label: lab })) }),
|
|
32
|
+
separator: () => ({}),
|
|
33
|
+
number: (d) => ({ value: num(d.value), min: num(d.min), max: num(d.max), step: num(d.step), unit: d.unit }),
|
|
34
|
+
text: (d) => ({ value: d.value ?? "", placeholder: d.placeholder, rows: num(d.rows) }),
|
|
35
|
+
fpsgraph: (d) => ({ label: d.label ?? "FPS" }),
|
|
36
|
+
interval: (d) => ({ value: d.value ? d.value.split(",").map(Number) : undefined, min: num(d.min), max: num(d.max), step: num(d.step) }),
|
|
37
|
+
sides: (d) => ({ value: d.value ?? d, corners: flag(d.corners), unit: d.unit, step: num(d.step), min: num(d.min), max: num(d.max) }), // "8px 16px", or data-top / -right / -bottom / -left
|
|
38
|
+
shadow: (d) => ({ value: d.value ?? { ...d, inset: flag(d.inset) } }), // a box-shadow string, or data-x / -y / -blur / -color
|
|
39
|
+
spring: springData,
|
|
40
|
+
motion: (d) => ({ ...springData(d), curve: d.curve && d.curve.includes(",") ? d.curve.split(",").map(Number) : d.curve, duration: num(d.duration) }), // plus data-curve and data-duration
|
|
41
|
+
cubicbezier: (d) => ({ value: d.value ? d.value.split(",").map(Number) : undefined }),
|
|
42
|
+
point: (d) => {
|
|
43
|
+
const vals = (d.value || "").split(",").map((s) => parseFloat(s));
|
|
44
|
+
return {
|
|
45
|
+
components: splitList(d.components || "X,Y").map((lab, i) => ({ key: lab.toLowerCase(), label: lab, value: isNaN(vals[i]) ? 0 : vals[i], step: num(d.step), min: num(d.min), max: num(d.max) })), // no step: the control derives a continuous one
|
|
46
|
+
pad: flag(d.pad), invertY: d.invertY === "true",
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
plot: (d) => ({ expr: d.expr, xMin: num(d.xmin), xMax: num(d.xmax), yMin: num(d.ymin), yMax: num(d.ymax), samples: num(d.samples), editable: d.editable !== "false" }),
|
|
50
|
+
};
|
|
51
|
+
const num = (s: string | undefined) => { if (s == null || s === "") return undefined; const n = +s; return Number.isFinite(n) ? n : undefined; }; // non-numeric → undefined, so the default applies
|
|
52
|
+
const flag = (s: string | undefined) => s === "true" || s === ""; // boolean attributes: data-x / data-x="true"
|
|
53
|
+
const splitList = (s: string | undefined) => (s || "").split(",").map((t) => t.trim()).filter(Boolean);
|
|
54
|
+
const dataMeta = (kit: MarkupKit, host: HTMLElement) => {
|
|
55
|
+
const d = host.dataset, type = d.tw;
|
|
56
|
+
// Tabs are panel-only, so a tabs host is left untouched, as the markup page says; built, it fell through to a folder
|
|
57
|
+
// whose missing get() threw out of enhance() and left every later host claimed but empty.
|
|
58
|
+
if (type === "tabs") return null;
|
|
59
|
+
const label = d.label ?? titleCase(d.key || type); // data-label="" means no label, like `label: ""`
|
|
60
|
+
// A lazy type with no parser takes the dataset as its fields.
|
|
61
|
+
const v = hasOwn(DATA_VALUE, type) ? DATA_VALUE[type]!(d, host, label) : (kit.LAZY_IMPORT[type] || getControl(type)) ? { ...d } : null;
|
|
62
|
+
// A typo (`data-tw="slidr"`) rendered nothing and said nothing. The list names what markup builds, so not tabs.
|
|
63
|
+
if (!v) { import("./heavy.js").then((m) => m.unknownType(`data-tw="${type}"`, String(type), [...Object.keys(DATA_VALUE), ...Object.keys(kit.LAZY_IMPORT), ...controlTypes()].filter((t) => t !== "tabs"), "is skipped"), () => {}); return null; }
|
|
64
|
+
// The label rides the verbose value; v spreads after, so a parser's own type or label wins. A form that comes back a
|
|
65
|
+
// folder (a lazy type's verbose form missing the field it hinges on) is no control either.
|
|
66
|
+
const m = kit.metaFor("v", { type, label, ...v });
|
|
67
|
+
return m && m.type !== "folder" ? m : null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
/** The handle enhance() hangs on a bound [data-tw] host: its control, the reset pair and its key. */
|
|
72
|
+
interface HostHandle { ctrl: Control; raw: unknown; def: unknown; key: string; host: HTMLElement; reset: () => void }
|
|
73
|
+
interface HostEl extends HTMLElement { _tw?: HostHandle }
|
|
74
|
+
|
|
75
|
+
// Echo a control's value onto its host's data-value, in the comma form schema.ts parses. A plain
|
|
76
|
+
// object (a point, a spring) would stringify as "[object Object]", so take its values in order.
|
|
77
|
+
const writeDataValue = (host: HTMLElement, v: unknown) => {
|
|
78
|
+
host.dataset.value = v == null ? "" : Array.isArray(v) ? v.join(",") : typeof v === "object" ? Object.values(v).join(",") : String(v);
|
|
79
|
+
};
|
|
80
|
+
// One change path for a markup host: echo onto data-value, then a bubbling tw:change with { key, value }.
|
|
81
|
+
const announce = (host: HTMLElement, key: string, v: unknown) => {
|
|
82
|
+
writeDataValue(host, v);
|
|
83
|
+
host.dispatchEvent(new CustomEvent("tw:change", { bubbles: true, detail: { key, value: v } }));
|
|
84
|
+
};
|
|
85
|
+
// querySelectorAll sees descendants only — a root that is itself a host counts too (a Document has no matches()).
|
|
86
|
+
const select = <T extends Element = HTMLElement>(root: Document | Element, sel: string): T[] => [...(typeof (root as Element).matches === "function" && (root as Element).matches(sel) ? [root as T] : []), ...root.querySelectorAll<T>(sel)];
|
|
87
|
+
/** Bind every unbound [data-tw] host under `root`. */
|
|
88
|
+
async function enhance(root: Document | Element, kit: MarkupKit): Promise<void> {
|
|
89
|
+
const { createFolder, createControl, makeCopyBtn, makeResetBtn, addHintMarker, ensureForMetas } = kit;
|
|
90
|
+
// Static showcase panels collapse like the real one: wrap the controls in a .tw-body, make the title the toggle.
|
|
91
|
+
select(root, '.tw-panel[data-mode="inline"]:not([data-tw-panel-bound])').forEach((panel) => {
|
|
92
|
+
const header = panel.querySelector(":scope > .tw-header");
|
|
93
|
+
const controls = panel.querySelector(":scope > .tw-controls");
|
|
94
|
+
if (!header || !controls) return;
|
|
95
|
+
panel.setAttribute("data-tw-panel-bound", "");
|
|
96
|
+
const body = el("div", "tw-body"); panel.insertBefore(body, controls); body.append(controls);
|
|
97
|
+
let toggle = header.querySelector<HTMLButtonElement>(".tw-header-toggle");
|
|
98
|
+
const title = header.querySelector(".tw-title");
|
|
99
|
+
if (!toggle && title) { toggle = btn("tw-header-toggle"); title.replaceWith(toggle); toggle.append(title); }
|
|
100
|
+
if (!toggle) return;
|
|
101
|
+
toggle.setAttribute("aria-expanded", "true");
|
|
102
|
+
toggle.addEventListener("click", () => foldBody(panel, toggle, body, !panel.classList.contains("is-collapsed")));
|
|
103
|
+
// The same toolbar tweaks() builds, over this panel's own [data-tw] controls (gathered at click time).
|
|
104
|
+
if (!header.querySelector(".tw-toolbar")) {
|
|
105
|
+
const live = () => [...panel.querySelectorAll<HostEl>("[data-tw]")].map((h) => h._tw).filter((t) => t && t.ctrl.get() !== undefined);
|
|
106
|
+
// Two hosts can share a key (a repeated data-key, or the same label): suffix the duplicates.
|
|
107
|
+
const values = () => {
|
|
108
|
+
const vals: Record<string, unknown> = Object.create(null); // null-proto: a "__proto__" data-key is an ordinary key
|
|
109
|
+
for (const t of live()) {
|
|
110
|
+
let k = t.key, n = 2; while (hasOwn(vals, k)) k = `${t.key}-${n++}`;
|
|
111
|
+
vals[k] = t.ctrl.get();
|
|
112
|
+
}
|
|
113
|
+
return JSON.stringify(vals, null, 2);
|
|
114
|
+
};
|
|
115
|
+
const reset = () => { for (const t of live()) t.reset(); };
|
|
116
|
+
const toolbar = el("div", "tw-toolbar");
|
|
117
|
+
toolbar.append(makeCopyBtn(() => [values(), -1]), makeResetBtn(reset)); header.append(toolbar); // no defaults to diff against: the copy is every value
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
// Folders first: move child hosts into the collapsible chrome.
|
|
121
|
+
select(root, '[data-tw="folder"]:not([data-tw-bound])').forEach((host) => {
|
|
122
|
+
host.setAttribute("data-tw-bound", "");
|
|
123
|
+
const f = createFolder({ label: host.dataset.label || "Folder" });
|
|
124
|
+
[...host.children].forEach((c) => f.body.append(c));
|
|
125
|
+
host.append(f.el);
|
|
126
|
+
});
|
|
127
|
+
// Claim each host synchronously, so a re-entrant enhance can't double-bind; then load and build.
|
|
128
|
+
const hosts = select<HostEl>(root, "[data-tw]:not([data-tw-bound])")
|
|
129
|
+
.map((h) => ({ host: h, meta: dataMeta(kit, h) }))
|
|
130
|
+
.filter((x) => x.meta);
|
|
131
|
+
hosts.forEach(({ host }) => host.setAttribute("data-tw-bound", ""));
|
|
132
|
+
const pend = ensureForMetas(hosts.map((x) => x.meta));
|
|
133
|
+
if (pend) await pend.catch(() => {}); // a failed chunk skips its controls instead of rejecting
|
|
134
|
+
// One host that fails to build costs only itself: the rest still bind.
|
|
135
|
+
for (const { host, meta } of hosts) try {
|
|
136
|
+
const key = host.dataset.key || meta.label;
|
|
137
|
+
// Gated the way the panel gates notify(): a same-value emit announces nothing.
|
|
138
|
+
let last: unknown;
|
|
139
|
+
const emit = (v: unknown) => { if (!valueChanged(last, v)) return; last = v; announce(host, key, v); };
|
|
140
|
+
const ctrl = createControl(meta, emit);
|
|
141
|
+
if (!ctrl) continue;
|
|
142
|
+
if (!host.closest(".tw-panel, .tw-portal")) host.classList.add("tw-portal"); // a bare host gets the token scope
|
|
143
|
+
host.append(ctrl.el); if (host.dataset.hint) addHintMarker(ctrl.el, host.dataset.hint);
|
|
144
|
+
// raw = the markup's value, def = the form the control opened on: the panel's reset pair.
|
|
145
|
+
const def = ctrl.get(); last = def;
|
|
146
|
+
// The handle's set() runs the change path too, where the bare control's set() is silent.
|
|
147
|
+
const handle = { ...ctrl, set: (v: unknown) => { ctrl.set(v); emit(ctrl.get()); } };
|
|
148
|
+
host._tw = { ctrl: handle, raw: meta.value, def, key, host, reset: () => { restoreDefault(ctrl, meta.value, def); emit(ctrl.get()); } };
|
|
149
|
+
} catch (err) { console.error(`[tweaks] data-tw="${host.dataset.tw}" could not be built — skipped:`, err); }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setMarkup(enhance);
|
|
153
|
+
|
|
154
|
+
export { DATA_VALUE }; // for the registry sync test
|