tweakit 0.6.0 → 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 +5 -5
- package/README.md +3 -3
- package/dist/tweaks/auto.js +1 -1
- package/dist/tweaks/auto.js.map +1 -1
- 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/core.js +1 -1
- package/dist/tweaks/core.js.map +3 -3
- 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-CLD7UAVI.js → heavy-NPFIGHRM.js} +2 -2
- package/dist/tweaks/image-HTQQ4WX6.js +4 -0
- package/dist/tweaks/image-HTQQ4WX6.js.map +6 -0
- package/dist/tweaks/{image-store-WRKY2NPU.js → image-store-QD5SMZXG.js} +1 -1
- 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 +5 -4
- package/dist/tweaks.js.map +3 -3
- package/dist/types/tweaks/enhance.d.ts +3 -2
- package/dist/types/tweaks/types.d.ts +278 -72
- package/package.json +5 -2
- package/src/tweaks/auto.ts +9 -5
- package/src/tweaks/clip.ts +84 -22
- package/src/tweaks/controls/basic.ts +463 -139
- package/src/tweaks/controls/bezier.ts +152 -47
- package/src/tweaks/controls/color.ts +610 -161
- package/src/tweaks/controls/gradient.ts +256 -77
- package/src/tweaks/controls/image.ts +388 -132
- package/src/tweaks/controls/interval.ts +126 -41
- package/src/tweaks/controls/monitor.ts +142 -36
- package/src/tweaks/controls/motion.ts +231 -56
- package/src/tweaks/controls/plot.ts +321 -101
- package/src/tweaks/controls/point.ts +134 -45
- package/src/tweaks/controls/shadow.ts +103 -30
- package/src/tweaks/controls/sides.ts +150 -40
- package/src/tweaks/controls/spring.ts +220 -60
- package/src/tweaks/controls/tabs.ts +39 -18
- package/src/tweaks/core.ts +1 -1
- package/src/tweaks/easing.ts +45 -20
- package/src/tweaks/enhance.ts +21 -6
- package/src/tweaks/feedback.ts +133 -34
- package/src/tweaks/fold.ts +22 -10
- package/src/tweaks/heavy.ts +461 -137
- package/src/tweaks/icons.ts +12 -5
- package/src/tweaks/image-store.ts +131 -43
- package/src/tweaks/lazy.ts +36 -19
- package/src/tweaks/markup.ts +255 -92
- package/src/tweaks/panel.ts +1262 -438
- package/src/tweaks/presets.ts +85 -0
- package/src/tweaks/schema.ts +213 -51
- package/src/tweaks/shadow-root.ts +178 -0
- package/src/tweaks/shared.ts +802 -190
- package/src/tweaks/standalone.ts +120 -30
- package/src/tweaks/types.ts +644 -143
- package/src/tweaks/undo.ts +104 -0
- package/src/tweaks.css +556 -550
- package/src/wide-gamut.ts +473 -125
- package/dist/tweaks/bezier-JFEZMCLP.js +0 -2
- package/dist/tweaks/bezier-JFEZMCLP.js.map +0 -6
- package/dist/tweaks/chunk-6UO4DHBN.js +0 -2
- package/dist/tweaks/chunk-6UO4DHBN.js.map +0 -6
- package/dist/tweaks/chunk-DIQMLBAS.js +0 -2
- package/dist/tweaks/chunk-DIQMLBAS.js.map +0 -6
- package/dist/tweaks/chunk-XE6OUJBG.js +0 -2
- package/dist/tweaks/chunk-XE6OUJBG.js.map +0 -6
- package/dist/tweaks/clip-WW7JYO4X.js +0 -2
- package/dist/tweaks/clip-WW7JYO4X.js.map +0 -6
- package/dist/tweaks/color-YMMFURP6.js +0 -2
- package/dist/tweaks/fold-U6YRZLCJ.js +0 -2
- package/dist/tweaks/fold-U6YRZLCJ.js.map +0 -6
- package/dist/tweaks/gradient-JER7PW4D.js +0 -2
- package/dist/tweaks/gradient-JER7PW4D.js.map +0 -6
- package/dist/tweaks/image-H6QSLGO3.js +0 -4
- package/dist/tweaks/image-H6QSLGO3.js.map +0 -6
- package/dist/tweaks/image-store-WRKY2NPU.js.map +0 -6
- package/dist/tweaks/interval-TEUHQGZN.js +0 -2
- package/dist/tweaks/interval-TEUHQGZN.js.map +0 -6
- package/dist/tweaks/markup-NIBJD6E2.js +0 -2
- package/dist/tweaks/markup-NIBJD6E2.js.map +0 -6
- package/dist/tweaks/monitor-3BY7YCGX.js +0 -3
- package/dist/tweaks/monitor-3BY7YCGX.js.map +0 -6
- package/dist/tweaks/motion-FQPUBKKM.js +0 -2
- package/dist/tweaks/motion-FQPUBKKM.js.map +0 -6
- package/dist/tweaks/plot-2HBNBZSG.js +0 -3
- package/dist/tweaks/plot-2HBNBZSG.js.map +0 -6
- package/dist/tweaks/point-VV6XV3I6.js +0 -2
- package/dist/tweaks/point-VV6XV3I6.js.map +0 -6
- package/dist/tweaks/shadow-5E4E55I2.js +0 -2
- package/dist/tweaks/shadow-5E4E55I2.js.map +0 -6
- package/dist/tweaks/sides-P3STEVYY.js +0 -2
- package/dist/tweaks/sides-P3STEVYY.js.map +0 -6
- package/dist/tweaks/spring-HUVSXNVN.js +0 -2
- package/dist/tweaks/spring-HUVSXNVN.js.map +0 -6
- package/dist/tweaks/tabs-76FW7LQ7.js +0 -2
- package/dist/tweaks/tabs-76FW7LQ7.js.map +0 -6
- /package/dist/tweaks/{color-YMMFURP6.js.map → color-UGYMAF3W.js.map} +0 -0
- /package/dist/tweaks/{heavy-CLD7UAVI.js.map → heavy-NPFIGHRM.js.map} +0 -0
package/src/tweaks/heavy.ts
CHANGED
|
@@ -1,113 +1,237 @@
|
|
|
1
1
|
/* Tweaks — what only the lazy controls use: the numeric field engine and its scrub, the
|
|
2
2
|
* press-drag gesture, the SVG factory, canvas sizing and the modal-trigger row, kept out of
|
|
3
3
|
* the shared chunk. It also registers the Number control, which no shorthand infers. */
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
el,
|
|
6
|
+
btn,
|
|
7
|
+
txt,
|
|
8
|
+
clamp,
|
|
9
|
+
carrySkin,
|
|
10
|
+
portalTo,
|
|
11
|
+
activeIn,
|
|
12
|
+
words,
|
|
13
|
+
quietFocus,
|
|
14
|
+
stepPrecision,
|
|
15
|
+
gridEnds,
|
|
16
|
+
roundToStep,
|
|
17
|
+
icon,
|
|
18
|
+
registerControl,
|
|
19
|
+
announce,
|
|
20
|
+
sayNow,
|
|
21
|
+
} from "./shared.js";
|
|
5
22
|
import type { Built, NumSpec, NumField, OnChange } from "./shared.js";
|
|
6
23
|
import type { Meta } from "./schema.js";
|
|
7
24
|
|
|
8
25
|
const svgNS = "http://www.w3.org/2000/svg";
|
|
9
26
|
// The SVG twin of shared.ts's el().
|
|
10
|
-
const svgEl = <K extends keyof SVGElementTagNameMap>(tag: K, cls?: string): Built<SVGElementTagNameMap[K]> => {
|
|
27
|
+
const svgEl = <K extends keyof SVGElementTagNameMap>(tag: K, cls?: string): Built<SVGElementTagNameMap[K]> => {
|
|
28
|
+
const n = document.createElementNS(svgNS, tag);
|
|
29
|
+
if (cls) n.setAttribute("class", cls);
|
|
30
|
+
return n;
|
|
31
|
+
};
|
|
11
32
|
// A token reads back as its light-dark(light, dark) pair, which a canvas can't parse: keep the arm the node's
|
|
12
33
|
// color-scheme paints with ("light dark" follows the OS), innermost pairs included, as CSS would.
|
|
13
34
|
const pickScheme = (v: string, cs: CSSStyleDeclaration) => {
|
|
14
35
|
if (!/light-dark\(/i.test(v)) return v;
|
|
15
|
-
const s = cs.colorScheme || "",
|
|
36
|
+
const s = cs.colorScheme || "",
|
|
37
|
+
dark = /\bdark\b/.test(s) && (!/\blight\b/.test(s) || matchMedia("(prefers-color-scheme: dark)").matches);
|
|
16
38
|
for (let i; (i = v.search(/light-dark\(/i)) !== -1;) {
|
|
17
|
-
let depth = 0,
|
|
18
|
-
|
|
39
|
+
let depth = 0,
|
|
40
|
+
comma = -1,
|
|
41
|
+
j = i + 11;
|
|
42
|
+
for (; j < v.length; j++) {
|
|
43
|
+
const c = v[j];
|
|
44
|
+
if (c === "(") depth++;
|
|
45
|
+
else if (c === ")") {
|
|
46
|
+
if (!depth) break;
|
|
47
|
+
depth--;
|
|
48
|
+
} else if (c === "," && !depth && comma < 0) comma = j;
|
|
49
|
+
}
|
|
19
50
|
if (comma < 0 || j >= v.length) return ""; // malformed: the caller's fallback color
|
|
20
51
|
v = v.slice(0, i) + (dark ? v.slice(comma + 1, j) : v.slice(i + 11, comma)).trim() + v.slice(j + 1);
|
|
21
52
|
}
|
|
22
53
|
return v;
|
|
23
54
|
};
|
|
24
|
-
const cssVar = (node: Element, name: string) => {
|
|
55
|
+
const cssVar = (node: Element, name: string) => {
|
|
56
|
+
const r = getComputedStyle(node);
|
|
57
|
+
return pickScheme(r.getPropertyValue(name).trim(), r);
|
|
58
|
+
};
|
|
25
59
|
// One getComputedStyle for both reads — this runs in the FPS draw loop.
|
|
26
|
-
const accentColor = (node: Element) => {
|
|
60
|
+
const accentColor = (node: Element) => {
|
|
61
|
+
const r = getComputedStyle(node);
|
|
62
|
+
const v = (n: string) => pickScheme(r.getPropertyValue(n).trim(), r);
|
|
63
|
+
return v("--twk-accent") || v("--twk-text-primary") || "#fff";
|
|
64
|
+
};
|
|
27
65
|
// Select the whole value on focus (the hex field: you type a new color, never edit a digit).
|
|
28
66
|
// Browsers collapse a focus-time selection on the click's own mouseup, so the first primary
|
|
29
67
|
// mouseup after a pointer-initiated focus is swallowed while the whole value is still selected.
|
|
30
68
|
const selectAllOnFocus = (input: HTMLInputElement) => {
|
|
31
69
|
let swallowUp = false;
|
|
32
|
-
input.addEventListener("pointerdown", (e) => {
|
|
70
|
+
input.addEventListener("pointerdown", (e) => {
|
|
71
|
+
swallowUp = e.button === 0 && activeIn(input) !== input;
|
|
72
|
+
});
|
|
33
73
|
input.addEventListener("focus", () => input.select());
|
|
34
74
|
input.addEventListener("mouseup", (e) => {
|
|
35
75
|
if (!swallowUp) return;
|
|
36
76
|
swallowUp = false;
|
|
37
|
-
if (
|
|
77
|
+
if (
|
|
78
|
+
e.button === 0 &&
|
|
79
|
+
activeIn(input) === input &&
|
|
80
|
+
input.selectionStart === 0 &&
|
|
81
|
+
input.selectionEnd === input.value.length
|
|
82
|
+
)
|
|
83
|
+
e.preventDefault();
|
|
38
84
|
});
|
|
39
85
|
};
|
|
40
86
|
// Escape in an edited field restores `kept` and stops; otherwise it passes on and closes the pane. Without this,
|
|
41
87
|
// closing blurred the field and committed the half-typed value.
|
|
42
|
-
const escapeReverts = (input: HTMLInputElement, kept: () => string) =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
88
|
+
const escapeReverts = (input: HTMLInputElement, kept: () => string) =>
|
|
89
|
+
input.addEventListener("keydown", (e: KeyboardEvent) => {
|
|
90
|
+
if (e.key !== "Escape" || input.value === kept()) return;
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
e.stopPropagation();
|
|
93
|
+
input.value = kept();
|
|
94
|
+
});
|
|
95
|
+
// Drag a pane by its edge (padding, or a gap between parts) to uncover what's beneath; it keeps that offset from its
|
|
96
|
+
// row while open (st.moved), and the next open resets it. Bound by popover() on a movable pane's first open.
|
|
97
|
+
const dragPane = (pop: HTMLElement, st: { open: boolean; moved: boolean }, place: () => void) =>
|
|
98
|
+
pop.addEventListener("pointerdown", (e: PointerEvent) => {
|
|
99
|
+
if (e.target !== pop || e.button !== 0 || !st.open) return;
|
|
100
|
+
e.preventDefault(); // no text selection, and focus stays where it was
|
|
101
|
+
const id = e.pointerId,
|
|
102
|
+
sx = e.clientX,
|
|
103
|
+
sy = e.clientY,
|
|
104
|
+
ox = parseFloat(pop.style.left),
|
|
105
|
+
oy = parseFloat(pop.style.top),
|
|
106
|
+
wasMoved = st.moved;
|
|
107
|
+
let going = false;
|
|
108
|
+
try {
|
|
109
|
+
pop.setPointerCapture(id);
|
|
110
|
+
} catch {}
|
|
111
|
+
const move = (m: PointerEvent) => {
|
|
112
|
+
if (m.pointerId !== id) return;
|
|
113
|
+
if (!going) {
|
|
114
|
+
if (Math.hypot(m.clientX - sx, m.clientY - sy) < 4) return;
|
|
115
|
+
going = st.moved = true;
|
|
116
|
+
pop.classList.add("is-moving");
|
|
117
|
+
}
|
|
118
|
+
pop.style.left = ox + m.clientX - sx + "px";
|
|
119
|
+
pop.style.top = oy + m.clientY - sy + "px";
|
|
120
|
+
place();
|
|
121
|
+
};
|
|
122
|
+
const end = (m: PointerEvent) => {
|
|
123
|
+
if (m.pointerId === id) stop();
|
|
124
|
+
};
|
|
125
|
+
// Escape mid-move puts it back and keeps it open. Capture phase, so it beats the pane's own Escape.
|
|
126
|
+
const key = (k: KeyboardEvent) => {
|
|
127
|
+
if (k.key !== "Escape" || !going) return;
|
|
128
|
+
k.preventDefault();
|
|
129
|
+
k.stopPropagation();
|
|
130
|
+
pop.style.left = ox + "px";
|
|
131
|
+
pop.style.top = oy + "px";
|
|
132
|
+
st.moved = wasMoved;
|
|
133
|
+
place();
|
|
134
|
+
stop();
|
|
135
|
+
try {
|
|
136
|
+
pop.releasePointerCapture(id);
|
|
137
|
+
} catch {}
|
|
138
|
+
};
|
|
139
|
+
const stop = () => {
|
|
140
|
+
pop.removeEventListener("pointermove", move);
|
|
141
|
+
pop.removeEventListener("pointerup", end);
|
|
142
|
+
pop.removeEventListener("pointercancel", end);
|
|
143
|
+
pop.removeEventListener("lostpointercapture", end);
|
|
144
|
+
document.removeEventListener("keydown", key, true);
|
|
145
|
+
pop.classList.remove("is-moving");
|
|
146
|
+
};
|
|
147
|
+
// lost: the node left mid-press
|
|
148
|
+
pop.addEventListener("pointermove", move);
|
|
149
|
+
pop.addEventListener("pointerup", end);
|
|
150
|
+
pop.addEventListener("pointercancel", end);
|
|
151
|
+
pop.addEventListener("lostpointercapture", end);
|
|
152
|
+
document.addEventListener("keydown", key, true);
|
|
153
|
+
});
|
|
74
154
|
// Press-drag on a node: onDown on pointerdown (pointer captured), onMove per move, onEnd on
|
|
75
155
|
// up/cancel/lost capture or when the button releases off the node (buttons===0).
|
|
76
|
-
function dragGesture(
|
|
156
|
+
function dragGesture(
|
|
157
|
+
node: HTMLElement,
|
|
158
|
+
{
|
|
159
|
+
onDown,
|
|
160
|
+
onMove,
|
|
161
|
+
onEnd,
|
|
162
|
+
}: { onDown?: (e: PointerEvent) => void; onMove?: (e: PointerEvent) => void; onEnd?: (e: PointerEvent) => void } = {},
|
|
163
|
+
) {
|
|
77
164
|
let activeId: number | null = null; // one captured pointer; a second finger can't fork the drag
|
|
78
|
-
const end = (e: PointerEvent) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
165
|
+
const end = (e: PointerEvent) => {
|
|
166
|
+
if (activeId === null || e.pointerId !== activeId) return;
|
|
167
|
+
activeId = null;
|
|
168
|
+
onEnd && onEnd(e);
|
|
169
|
+
};
|
|
170
|
+
node.addEventListener("pointerdown", (e) => {
|
|
171
|
+
if (e.button !== 0 || activeId !== null) return;
|
|
172
|
+
activeId = e.pointerId;
|
|
173
|
+
try {
|
|
174
|
+
node.setPointerCapture(e.pointerId);
|
|
175
|
+
} catch {}
|
|
176
|
+
onDown && onDown(e);
|
|
177
|
+
});
|
|
178
|
+
node.addEventListener("pointermove", (e) => {
|
|
179
|
+
if (e.pointerId !== activeId) return;
|
|
180
|
+
if (e.buttons === 0) return end(e);
|
|
181
|
+
onMove && onMove(e);
|
|
182
|
+
});
|
|
183
|
+
node.addEventListener("pointerup", end);
|
|
184
|
+
node.addEventListener("pointercancel", end);
|
|
82
185
|
node.addEventListener("lostpointercapture", end); // capture lost mid-drag (popover unmounting) ends it too
|
|
83
186
|
}
|
|
84
187
|
// Press-drag flagging .is-grabbing on the surface for the gesture's run (the thumb-lift CSS keys off it).
|
|
85
|
-
const grabSurface = (surface: HTMLElement, set: (e: PointerEvent) => void) =>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
188
|
+
const grabSurface = (surface: HTMLElement, set: (e: PointerEvent) => void) =>
|
|
189
|
+
dragGesture(surface, {
|
|
190
|
+
onDown: (e) => {
|
|
191
|
+
surface.classList.add("is-grabbing");
|
|
192
|
+
set(e);
|
|
193
|
+
},
|
|
194
|
+
onMove: set,
|
|
195
|
+
onEnd: () => surface.classList.remove("is-grabbing"),
|
|
196
|
+
});
|
|
90
197
|
// Pointer position inside a box as [x, y] fractions in 0–1.
|
|
91
|
-
const boxFrac = (e: MouseEvent, box: Element): [number, number] => {
|
|
198
|
+
const boxFrac = (e: MouseEvent, box: Element): [number, number] => {
|
|
199
|
+
const r = box.getBoundingClientRect();
|
|
200
|
+
return [clamp((e.clientX - r.left) / r.width, 0, 1), clamp((e.clientY - r.top) / r.height, 0, 1)];
|
|
201
|
+
};
|
|
92
202
|
// An arrow key as a [dx, dy] step, ↑ positive as on screen; [0, 0] for any other key.
|
|
93
|
-
const arrowStep = (e: KeyboardEvent): [number, number] => [
|
|
203
|
+
const arrowStep = (e: KeyboardEvent): [number, number] => [
|
|
204
|
+
e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0,
|
|
205
|
+
e.key === "ArrowUp" ? 1 : e.key === "ArrowDown" ? -1 : 0,
|
|
206
|
+
];
|
|
94
207
|
// A 2D surface's readout rides on aria-description, which a screen reader speaks on focus and never again: the returned
|
|
95
208
|
// function says it once a run of keys stops.
|
|
96
|
-
const sayWhenKeysStop = (node: HTMLElement) => {
|
|
209
|
+
const sayWhenKeysStop = (node: HTMLElement) => {
|
|
210
|
+
let t = 0;
|
|
211
|
+
return () => {
|
|
212
|
+
clearTimeout(t);
|
|
213
|
+
t = setTimeout(() => {
|
|
214
|
+
if (node.isConnected) announce(node.getAttribute("aria-description") || "");
|
|
215
|
+
}, 250);
|
|
216
|
+
};
|
|
217
|
+
};
|
|
97
218
|
// Size a canvas to its CSS box × devicePixelRatio (capped) and scale the context to CSS pixels; returns [cssW, cssH].
|
|
98
219
|
const fitCanvas = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, maxDpr = Infinity): [number, number] => {
|
|
99
220
|
const r = canvas.getBoundingClientRect();
|
|
100
221
|
const dpr = Math.min(window.devicePixelRatio || 1, maxDpr);
|
|
101
|
-
canvas.width = r.width * dpr;
|
|
222
|
+
canvas.width = r.width * dpr;
|
|
223
|
+
canvas.height = r.height * dpr;
|
|
102
224
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
103
225
|
return [r.width, r.height];
|
|
104
226
|
};
|
|
105
|
-
// The modal-trigger row (color, gradient, point): a row button, label left, a preview cluster (`right`) the caller
|
|
227
|
+
// The modal-trigger row (color, gradient, point): a row button, label left, a preview cluster (`right`) the caller
|
|
228
|
+
// fills.
|
|
106
229
|
const triggerRow = (cls: string, label: string) => {
|
|
107
230
|
const root = el("div", cls);
|
|
108
|
-
const trigger = btn("
|
|
109
|
-
|
|
110
|
-
|
|
231
|
+
const trigger = btn("twk-trigger");
|
|
232
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
233
|
+
const right = el("span", "twk-trigger-right");
|
|
234
|
+
trigger.append(txt("span", "twk-trigger-label", label), right);
|
|
111
235
|
root.append(trigger);
|
|
112
236
|
return { root, trigger, right };
|
|
113
237
|
};
|
|
@@ -117,37 +241,82 @@ const ICON_GRIP = icon('<path d="M6 4v8M10 4v8"/>', "", 2, 16); // 1.5px bars at
|
|
|
117
241
|
|
|
118
242
|
// Grab guide — a dotted line from the grab point to the cursor plus a value bubble, portaled
|
|
119
243
|
// to <body> for a drag. Its four spans (line, dot, arrow, bubble) are addressed by index.
|
|
120
|
-
interface GuideEl extends HTMLDivElement {
|
|
244
|
+
interface GuideEl extends HTMLDivElement {
|
|
245
|
+
readonly children: HTMLCollectionOf<HTMLSpanElement>;
|
|
246
|
+
}
|
|
121
247
|
function makeGrabGuide() {
|
|
122
|
-
let g: GuideEl | null = null,
|
|
248
|
+
let g: GuideEl | null = null,
|
|
249
|
+
y = 0,
|
|
250
|
+
x0 = 0,
|
|
251
|
+
bx = 0;
|
|
123
252
|
return {
|
|
124
253
|
show(x: number, atY: number, bubbleX: number | undefined, anchor: Element) {
|
|
125
|
-
g = el("div", "
|
|
126
|
-
g.innerHTML = `<span class="
|
|
127
|
-
y = atY;
|
|
254
|
+
g = el("div", "twk-grab-guide twk-portal") as GuideEl;
|
|
255
|
+
g.innerHTML = `<span class="twk-grab-line"></span><span class="twk-grab-dot"></span><span class="twk-grab-arrow"></span><span class="twk-grab-bubble"></span>`;
|
|
256
|
+
y = atY;
|
|
257
|
+
x0 = x;
|
|
258
|
+
bx = bubbleX ?? x; // bubble anchors over the field center, not the cursor
|
|
128
259
|
g.children[1].style.cssText = `left:${x}px;top:${y}px`;
|
|
129
260
|
carrySkin(g, anchor); // the anchor panel's theme + scheme, like every portal
|
|
130
|
-
|
|
261
|
+
portalTo(g, anchor);
|
|
131
262
|
},
|
|
132
263
|
move(x: number, text: string) {
|
|
133
264
|
if (!g) return;
|
|
134
265
|
g.children[0].style.cssText = `left:${Math.min(x0, x)}px;top:${y}px;width:${Math.abs(x - x0)}px`;
|
|
135
|
-
|
|
136
|
-
g.children[
|
|
266
|
+
// arrowhead at the cursor, pointing in the drag direction
|
|
267
|
+
g.children[2].style.cssText = `left:${x}px;top:${y}px;transform:translate(-50%,-50%) scaleX(${x >= x0 ? 1 : -1})`;
|
|
268
|
+
g.children[3].style.cssText = `left:${bx}px;top:${y - 16}px`;
|
|
269
|
+
g.children[3].textContent = text;
|
|
270
|
+
},
|
|
271
|
+
hide() {
|
|
272
|
+
if (g) {
|
|
273
|
+
g.remove();
|
|
274
|
+
g = null;
|
|
275
|
+
}
|
|
137
276
|
},
|
|
138
|
-
hide() { if (g) { g.remove(); g = null; } },
|
|
139
277
|
};
|
|
140
278
|
}
|
|
141
279
|
|
|
142
280
|
// Drag-to-scrub on a grab handle: 1px ≈ one step (Shift ×10, Alt ×0.1), re-anchoring on a
|
|
143
281
|
// modifier change so the value never jumps. read() is the live value, text() the bubble label.
|
|
144
|
-
function attachScrub(
|
|
145
|
-
|
|
282
|
+
function attachScrub(
|
|
283
|
+
grab: HTMLElement,
|
|
284
|
+
wrap: HTMLElement,
|
|
285
|
+
step: number,
|
|
286
|
+
read: () => number,
|
|
287
|
+
apply: (v: number) => void,
|
|
288
|
+
text: () => string,
|
|
289
|
+
) {
|
|
290
|
+
let downX = 0,
|
|
291
|
+
downV = 0,
|
|
292
|
+
curK = 1;
|
|
293
|
+
const gd = makeGrabGuide();
|
|
146
294
|
// Every end path matters: capture lost mid-scrub must still hide the full-screen guide.
|
|
147
295
|
dragGesture(grab, {
|
|
148
|
-
onDown: (e) => {
|
|
149
|
-
|
|
150
|
-
|
|
296
|
+
onDown: (e) => {
|
|
297
|
+
e.preventDefault();
|
|
298
|
+
downX = e.clientX;
|
|
299
|
+
downV = read();
|
|
300
|
+
curK = 1;
|
|
301
|
+
grab.classList.add("is-dragging");
|
|
302
|
+
const br = wrap.getBoundingClientRect();
|
|
303
|
+
gd.show(e.clientX, br.top + br.height / 2, br.left + br.width / 2, wrap);
|
|
304
|
+
gd.move(e.clientX, text());
|
|
305
|
+
},
|
|
306
|
+
onMove: (e) => {
|
|
307
|
+
const k = e.shiftKey ? 10 : e.altKey ? 0.1 : 1;
|
|
308
|
+
if (k !== curK) {
|
|
309
|
+
curK = k;
|
|
310
|
+
downX = e.clientX;
|
|
311
|
+
downV = read();
|
|
312
|
+
}
|
|
313
|
+
apply(downV + (e.clientX - downX) * step * k);
|
|
314
|
+
gd.move(e.clientX, text());
|
|
315
|
+
},
|
|
316
|
+
onEnd: () => {
|
|
317
|
+
grab.classList.remove("is-dragging");
|
|
318
|
+
gd.hide();
|
|
319
|
+
},
|
|
151
320
|
});
|
|
152
321
|
}
|
|
153
322
|
|
|
@@ -158,71 +327,133 @@ function numField(spec: NumSpec, onChange?: (v: number) => void): NumField {
|
|
|
158
327
|
// A 0/negative/non-finite step breaks round-to-step (reachable via a point component's step).
|
|
159
328
|
const step = Number.isFinite(+spec.step) && +spec.step > 0 ? +spec.step : 1;
|
|
160
329
|
const bound = (b: unknown) => (b == null ? NaN : +b); // null → NaN → unbounded (a null max coerced to 0)
|
|
161
|
-
let min = bound(spec.min),
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
330
|
+
let min = bound(spec.min),
|
|
331
|
+
max = bound(spec.max);
|
|
332
|
+
// an inverted pair would clamp every value to one end
|
|
333
|
+
if (Number.isFinite(min) && Number.isFinite(max) && max < min) {
|
|
334
|
+
notice("swap", [spec.label ?? "number", min, max]);
|
|
335
|
+
const t = min;
|
|
336
|
+
min = max;
|
|
337
|
+
max = t;
|
|
338
|
+
}
|
|
339
|
+
const anchor = Number.isFinite(min) ? min : 0,
|
|
340
|
+
decimals = stepPrecision(step); // min-anchored, like the slider
|
|
341
|
+
// on-grid ends, so fit(fit(x)) === fit(x)
|
|
342
|
+
const [lo, hi] = gridEnds(
|
|
343
|
+
Number.isFinite(min) ? min : -Infinity,
|
|
344
|
+
Number.isFinite(max) ? max : Infinity,
|
|
345
|
+
step,
|
|
346
|
+
anchor,
|
|
347
|
+
);
|
|
348
|
+
const fit = (val: number) => {
|
|
349
|
+
const n = roundToStep(val, anchor, step);
|
|
350
|
+
return spec.soft ? n : clamp(n, lo, hi);
|
|
351
|
+
};
|
|
166
352
|
let value = fit(Number.isFinite(+spec.value) ? +spec.value : 0);
|
|
167
|
-
const root = el("div", spec.row ? "
|
|
168
|
-
const wrap = el("div", "
|
|
169
|
-
const grab = el("span", "
|
|
170
|
-
|
|
353
|
+
const root = el("div", spec.row ? "twk-row" : "twk-field");
|
|
354
|
+
const wrap = el("div", "twk-num-wrap");
|
|
355
|
+
const grab = el("span", "twk-num-grab", ICON_GRIP);
|
|
356
|
+
grab.setAttribute("aria-hidden", "true");
|
|
357
|
+
grab.title = words()("dragToAdjust", "Drag to adjust");
|
|
358
|
+
const inp = el("input", "twk-num");
|
|
359
|
+
inp.type = "text";
|
|
360
|
+
inp.inputMode = "decimal";
|
|
361
|
+
inp.setAttribute("aria-label", spec.unit ? `${spec.label} (${spec.unit})` : spec.label);
|
|
362
|
+
inp.value = value.toFixed(decimals);
|
|
171
363
|
quietFocus(inp); // click-to-edit stays ringless; Tab rings
|
|
172
|
-
wrap.append(grab, inp);
|
|
173
|
-
|
|
364
|
+
wrap.append(grab, inp);
|
|
365
|
+
if (spec.unit) wrap.append(txt("span", "twk-num-unit", spec.unit));
|
|
366
|
+
root.append(txt("span", spec.row ? "twk-row-label" : "twk-field-label", spec.label), wrap);
|
|
367
|
+
const set = (val: number | string, fire = true) => {
|
|
368
|
+
val = +val;
|
|
369
|
+
if (!Number.isFinite(val)) return;
|
|
370
|
+
value = fit(val);
|
|
371
|
+
inp.value = value.toFixed(decimals);
|
|
372
|
+
if (fire && onChange) onChange(value);
|
|
373
|
+
};
|
|
174
374
|
// Typed text is read strictly as a whole number, as the slider's editor reads it (not parseFloat, which takes "1,000"
|
|
175
375
|
// as 1 and "12abc" as 12); the field's unit may be typed along. NaN for anything else.
|
|
176
376
|
const typed = () => Number(inp.value.replace(spec.unit || "", "").trim() || NaN);
|
|
177
|
-
// Only a readable number that lands on a new value commits; otherwise the field shows the kept value again and fires
|
|
178
|
-
|
|
377
|
+
// Only a readable number that lands on a new value commits; otherwise the field shows the kept value again and fires
|
|
378
|
+
// nothing.
|
|
379
|
+
inp.addEventListener("change", () => {
|
|
380
|
+
const n = typed();
|
|
381
|
+
if (Number.isFinite(n) && fit(n) !== value) set(n);
|
|
382
|
+
else inp.value = value.toFixed(decimals);
|
|
383
|
+
});
|
|
179
384
|
escapeReverts(inp, () => value.toFixed(decimals));
|
|
180
385
|
// ↑/↓ step off the half-typed value (a typed "4" then ↑ reads 5), the keyboard twin of the scrub.
|
|
181
386
|
inp.addEventListener("keydown", (e) => {
|
|
182
387
|
if (e.key === "Enter") return inp.blur();
|
|
183
|
-
const d = e.key === "ArrowUp" ? 1 : e.key === "ArrowDown" ? -1 : 0;
|
|
388
|
+
const d = e.key === "ArrowUp" ? 1 : e.key === "ArrowDown" ? -1 : 0;
|
|
389
|
+
if (!d) return;
|
|
184
390
|
e.preventDefault();
|
|
185
391
|
const n = typed();
|
|
186
392
|
set((Number.isFinite(n) ? n : value) + d * step * (e.shiftKey ? 10 : 1));
|
|
187
393
|
});
|
|
188
|
-
attachScrub(
|
|
394
|
+
attachScrub(
|
|
395
|
+
grab,
|
|
396
|
+
wrap,
|
|
397
|
+
step,
|
|
398
|
+
() => value,
|
|
399
|
+
set,
|
|
400
|
+
() => inp.value,
|
|
401
|
+
);
|
|
189
402
|
return { el: root, set: (val: number | string) => set(val, false), get: () => value, range: [lo, hi] };
|
|
190
403
|
}
|
|
191
404
|
|
|
192
405
|
// ── Number — numField in its row chrome. ──
|
|
193
|
-
// The coarsest power of ten a number sits on (0.5 → 0.1, 3.14159 → 0.00001, 1230 → 10), read at 12 significant digits
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
406
|
+
// The coarsest power of ten a number sits on (0.5 → 0.1, 3.14159 → 0.00001, 1230 → 10), read at 12 significant digits
|
|
407
|
+
// so
|
|
408
|
+
// 0.1 + 0.2 counts as 0.3; Infinity for 0 or a non-number, which constrains nothing. A step no coarser keeps it
|
|
409
|
+
// exactly.
|
|
410
|
+
// Past eight significant digits a number was computed, not written (1/3, Math.PI): it steps at its fourth digit, as a
|
|
411
|
+
// slider rounds it, so an arrow moves it by 0.0001, not by 1e-12.
|
|
197
412
|
const decimalStep = (v: unknown) => {
|
|
198
|
-
const n = Number(Math.abs(+(v as number)).toPrecision(12));
|
|
199
|
-
|
|
200
|
-
|
|
413
|
+
const n = Number(Math.abs(+(v as number)).toPrecision(12));
|
|
414
|
+
if (!n || !Number.isFinite(n)) return Infinity;
|
|
415
|
+
const d = stepPrecision(n),
|
|
416
|
+
e = Math.floor(Math.log10(n));
|
|
417
|
+
if (d) return Number(`1e${e + d + 1 > 8 ? e - 3 : -d}`);
|
|
418
|
+
let k = 0;
|
|
419
|
+
while (k < 15 && n % 10 ** (k + 1) === 0) k++;
|
|
201
420
|
return 10 ** k;
|
|
202
421
|
};
|
|
203
422
|
// A number with no step takes 1, or finer when its default or min has decimals (0.5 steps by 0.5): decided here, in the
|
|
204
423
|
// lazy chunk, so the basic panel carries none of it.
|
|
205
|
-
registerControl("number", (meta: Meta, onChange?: OnChange) =>
|
|
424
|
+
registerControl("number", (meta: Meta, onChange?: OnChange) =>
|
|
425
|
+
numField(
|
|
426
|
+
{ ...meta, step: meta.step ?? Math.min(1, decimalStep(meta.value), decimalStep(meta.min)), row: true },
|
|
427
|
+
onChange,
|
|
428
|
+
),
|
|
429
|
+
);
|
|
206
430
|
|
|
207
431
|
// ── Spring step response — shared by the spring and motion controls. The window runs to the
|
|
208
432
|
// settle, capped at 2.2 s. ──
|
|
209
|
-
// Motion's visualDuration + bounce, resolved as motion-dom's spring generator does (mass 1): a bounce maps to a damping
|
|
210
|
-
// as SwiftUI's does, floored at 0.05 either way; the period is visualDuration × 1.2; and an overdamped (negative)
|
|
211
|
-
// scales the frequency by a Newton solve so it arrives when the critically damped spring would. Copied value for
|
|
212
|
-
// what's tuned here plays the same in Motion.
|
|
433
|
+
// Motion's visualDuration + bounce, resolved as motion-dom's spring generator does (mass 1): a bounce maps to a damping
|
|
434
|
+
// ratio as SwiftUI's does, floored at 0.05 either way; the period is visualDuration × 1.2; and an overdamped (negative)
|
|
435
|
+
// bounce scales the frequency by a Newton solve so it arrives when the critically damped spring would. Copied value for
|
|
436
|
+
// value, so what's tuned here plays the same in Motion.
|
|
213
437
|
const CRIT = (2 * Math.PI) / 1.2;
|
|
214
438
|
// The visualDuration (s) the spring editor shows and resolves: down to 10 ms, up to the 10 s a spring's linear() runs.
|
|
215
439
|
const SPRING_DUR = [0.01, 10] as const;
|
|
216
440
|
const bounceRatio = (b: number) => (b < 0 ? 1 / Math.max(1 + b, 0.05) : Math.max(1 - b, 0.05));
|
|
217
441
|
const overdampedScale = (z: number) => {
|
|
218
442
|
if (!(z > 1)) return 1;
|
|
219
|
-
const r = Math.sqrt(z * z - 1),
|
|
443
|
+
const r = Math.sqrt(z * z - 1),
|
|
444
|
+
slow = z - r,
|
|
445
|
+
fast = z + r,
|
|
446
|
+
target = 2 * r * Math.exp(-CRIT) * (1 + CRIT);
|
|
220
447
|
let x = CRIT;
|
|
221
|
-
for (let i = 1; i < 12; i++)
|
|
448
|
+
for (let i = 1; i < 12; i++)
|
|
449
|
+
x -=
|
|
450
|
+
(fast * Math.exp(-slow * x) - slow * Math.exp(-fast * x) - target) / (Math.exp(-fast * x) - Math.exp(-slow * x));
|
|
222
451
|
return x / CRIT;
|
|
223
452
|
};
|
|
224
453
|
const springFromTime = (visualDuration: number, bounce: number) => {
|
|
225
|
-
const z = bounceRatio(bounce),
|
|
454
|
+
const z = bounceRatio(bounce),
|
|
455
|
+
root = (CRIT / visualDuration) * overdampedScale(z),
|
|
456
|
+
k = root * root;
|
|
226
457
|
return { stiffness: k, damping: 2 * z * Math.sqrt(k), mass: 1 };
|
|
227
458
|
};
|
|
228
459
|
// Its inverse, for physics keys handed to a time-mode editor: the visualDuration + bounce Motion would resolve to them.
|
|
@@ -233,70 +464,163 @@ const timeFromSpring = (k: number, d: number, m = 1) => {
|
|
|
233
464
|
// How fast the slowest mode dies out: ζω₀, or an overdamped spring's slow root (ζω₀ is its fast one, so the preview
|
|
234
465
|
// window shrank to milliseconds and drew a near-flat line).
|
|
235
466
|
const decayRate = (w0: number, z: number) => (z > 1 ? w0 * (z - Math.sqrt(z * z - 1)) : z * w0);
|
|
236
|
-
const springSpan = (k: number, d: number, m: number) =>
|
|
467
|
+
const springSpan = (k: number, d: number, m: number) =>
|
|
468
|
+
Math.min(2.2, 9 / Math.max(decayRate(Math.sqrt(k / m), d / (2 * Math.sqrt(k * m))), 0.5));
|
|
237
469
|
// Closed-form step response of a damped harmonic oscillator (under/critical/over): position at time t.
|
|
238
470
|
const springAt = (k: number, d: number, m: number) => {
|
|
239
|
-
const w0 = Math.sqrt(k / m),
|
|
240
|
-
|
|
471
|
+
const w0 = Math.sqrt(k / m),
|
|
472
|
+
z = d / (2 * Math.sqrt(k * m));
|
|
473
|
+
if (z < 1 - 1e-4) {
|
|
474
|
+
const wd = w0 * Math.sqrt(1 - z * z);
|
|
475
|
+
return (t: number) => 1 - Math.exp(-z * w0 * t) * (Math.cos(wd * t) + ((z * w0) / wd) * Math.sin(wd * t));
|
|
476
|
+
}
|
|
241
477
|
if (z <= 1 + 1e-4) return (t: number) => 1 - Math.exp(-w0 * t) * (1 + w0 * t);
|
|
242
|
-
const s = Math.sqrt(z * z - 1),
|
|
478
|
+
const s = Math.sqrt(z * z - 1),
|
|
479
|
+
a = w0 * (z + s),
|
|
480
|
+
b = w0 * (z - s);
|
|
243
481
|
return (t: number) => 1 - (a * Math.exp(-b * t) - b * Math.exp(-a * t)) / (a - b);
|
|
244
482
|
};
|
|
245
483
|
// N samples over the span, for drawing.
|
|
246
484
|
function springCurve(k: number, d: number, m: number, N = 64, T = springSpan(k, d, m)) {
|
|
247
|
-
const x = springAt(k, d, m),
|
|
485
|
+
const x = springAt(k, d, m),
|
|
486
|
+
out: number[] = [];
|
|
248
487
|
for (let i = 0; i < N; i++) out.push(x((i / (N - 1)) * T));
|
|
249
488
|
return out;
|
|
250
489
|
}
|
|
251
490
|
// The run as a CSS transition: how long, and a linear() easing. It lasts until within e⁻⁷ of rest, by the slowest mode
|
|
252
|
-
// (an overdamped spring's slow root, not ζω₀), capped at 10 s, where the last stop is 1 so it ends at rest.
|
|
253
|
-
//
|
|
254
|
-
//
|
|
491
|
+
// (an overdamped spring's slow root, not ζω₀), capped at 10 s, where the last stop is 1 so it ends at rest. There are
|
|
492
|
+
// at least 24 stops a wobble plus one on every peak and trough, each at its own percentage, so the overshoots land
|
|
493
|
+
// where they are: stops spaced by duration alone give a bouncy spring three a wobble and miss its peaks by a third.
|
|
255
494
|
const springRun = (k: number, d: number, m: number) => {
|
|
256
|
-
const w0 = Math.sqrt(k / m),
|
|
495
|
+
const w0 = Math.sqrt(k / m),
|
|
496
|
+
z = d / (2 * Math.sqrt(k * m)),
|
|
497
|
+
x = springAt(k, d, m);
|
|
257
498
|
const span = Math.min(7 / Math.max(decayRate(w0, z), 1e-3), 10);
|
|
258
|
-
const wd = z < 1 ? w0 * Math.sqrt(1 - z * z) : 0,
|
|
499
|
+
const wd = z < 1 ? w0 * Math.sqrt(1 - z * z) : 0,
|
|
500
|
+
n = Math.min(Math.max(48, Math.ceil(wd ? (24 * span * wd) / (2 * Math.PI) : span * 40)), 800);
|
|
259
501
|
const ts = Array.from({ length: n }, (_, i) => (i / (n - 1)) * span);
|
|
260
|
-
|
|
502
|
+
// every extremum of the underdamped response
|
|
503
|
+
if (wd) for (let t = Math.PI / wd; t < span; t += Math.PI / wd) ts.push(t);
|
|
261
504
|
ts.sort((a, b) => a - b);
|
|
262
|
-
const stops = ts
|
|
505
|
+
const stops = ts
|
|
506
|
+
.filter((t, i) => i === 0 || t - ts[i - 1] > 1e-6)
|
|
507
|
+
.map((t, i, a) => `${i === a.length - 1 ? 1 : +x(t).toFixed(3)} ${+((t / span) * 100).toFixed(2)}%`);
|
|
263
508
|
return { duration: Math.round(span * 1000), easing: `linear(${stops.join(", ")})` };
|
|
264
509
|
};
|
|
265
510
|
|
|
266
|
-
// The name a typo is a single edit from (a letter added, dropped or changed)
|
|
267
|
-
// only a mistake needs them. unknownType: a schema or markup
|
|
268
|
-
//
|
|
511
|
+
// The name a typo is a single edit from (a letter added, dropped or changed), or that another library or an older
|
|
512
|
+
// tweakit calls it by. It and the notices below are lazy, as only a mistake needs them. unknownType: a schema or markup
|
|
513
|
+
// `type` no control answers to. Its list leaves out the color module's picker body, registered as a control but not a
|
|
514
|
+
// type to write.
|
|
515
|
+
const SYNONYMS: Record<string, string> = Object.assign(Object.create(null), {
|
|
516
|
+
bezier: "cubicbezier",
|
|
517
|
+
"cubic-bezier": "cubicbezier",
|
|
518
|
+
easing: "motion",
|
|
519
|
+
toggle: "checkbox",
|
|
520
|
+
switch: "checkbox",
|
|
521
|
+
bool: "checkbox",
|
|
522
|
+
boolean: "checkbox",
|
|
523
|
+
dropdown: "list",
|
|
524
|
+
select: "list",
|
|
525
|
+
string: "text",
|
|
526
|
+
textarea: "text",
|
|
527
|
+
range: "slider",
|
|
528
|
+
fps: "fpsgraph",
|
|
529
|
+
});
|
|
269
530
|
const nearest = (s: string, names: string[]) => {
|
|
270
531
|
const a = s.toLowerCase(); // case-blind, so "textmuted" finds "textMuted"
|
|
271
|
-
|
|
532
|
+
if (names.includes(SYNONYMS[a])) return SYNONYMS[a];
|
|
533
|
+
return names.find((n) => {
|
|
534
|
+
const k = n.toLowerCase();
|
|
535
|
+
let i = 0;
|
|
536
|
+
while (i < a.length && a[i] === k[i]) i++;
|
|
537
|
+
return [
|
|
538
|
+
[1, 1],
|
|
539
|
+
[0, 1],
|
|
540
|
+
[1, 0],
|
|
541
|
+
].some(([x, y]) => a.slice(i + x) === k.slice(i + y));
|
|
542
|
+
});
|
|
272
543
|
};
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
544
|
+
const THEN: Record<string, string> = {
|
|
545
|
+
control: "is skipped, as an object with value, options or action is read as a control",
|
|
546
|
+
folder: "builds a folder",
|
|
547
|
+
markup: "is skipped",
|
|
548
|
+
};
|
|
549
|
+
const unknownType = (where: string, type: string, known: string[], then: "control" | "folder" | "markup") => {
|
|
550
|
+
const names = [...new Set(known)].filter((t) => t !== "folder" && t !== "colorpicker").sort(),
|
|
551
|
+
near = nearest(type, names);
|
|
552
|
+
console.warn(
|
|
553
|
+
`[tweaks] ${where}: "${type}" isn't a control type${near ? ` — did you mean "${near}"?` : "."} It ${THEN[then]}. Types: ${names.join(", ")}`,
|
|
554
|
+
);
|
|
276
555
|
};
|
|
277
556
|
|
|
278
557
|
const warned = new Set<string>();
|
|
279
558
|
const badGradient = (fn: string, value: unknown, all: unknown, n: number) => {
|
|
280
|
-
const why = !Array.isArray(all)
|
|
281
|
-
|
|
282
|
-
|
|
559
|
+
const why = !Array.isArray(all)
|
|
560
|
+
? `expected { stops: [{ color, pos }] }, got ${value === null ? "null" : typeof value === "object" ? "an object without a stops array" : typeof value}`
|
|
561
|
+
: n < all.length
|
|
562
|
+
? `skipped ${all.length - n} of ${all.length} stops without a CSS color and a numeric pos`
|
|
563
|
+
: "the gradient has no stops";
|
|
564
|
+
if (!warned.has(fn + why)) {
|
|
565
|
+
warned.add(fn + why);
|
|
566
|
+
console.warn(`[tweaks] ${fn}(): ${why}`);
|
|
567
|
+
}
|
|
283
568
|
};
|
|
284
|
-
// The words for the core's notices (shared.ts notice()): what went wrong and the way out, downloaded only when one
|
|
569
|
+
// The words for the core's notices (shared.ts notice()): what went wrong and the way out, downloaded only when one
|
|
570
|
+
// fires.
|
|
285
571
|
const NOTICES: Record<string, (...a: any[]) => string> = {
|
|
286
|
-
clamp: (key, v, min, max, type) =>
|
|
287
|
-
|
|
572
|
+
clamp: (key, v, min, max, type) =>
|
|
573
|
+
`set("${key}", ${JSON.stringify(v)}) clamped to ${min > -Infinity ? min : "−∞"}–${max < Infinity ? max : "∞"}; widen min/max${type === "slider" || type === "number" ? ", or set soft: true to allow it" : ""}`,
|
|
574
|
+
sharedKey: (k) =>
|
|
575
|
+
`persist: another live panel already saves under "${k}", so each overwrites the other's values; give one its own persist key`,
|
|
288
576
|
pointValue: (key, comps) => `"${key}": a point value that isn't a number starts at 0 (${comps})`,
|
|
289
|
-
swap: (label, min, max)
|
|
290
|
-
theme: (key) =>
|
|
291
|
-
|
|
577
|
+
swap: (label, min, max) => `"${label}": min ${min} is above max ${max}, so they were swapped`,
|
|
578
|
+
theme: (key) =>
|
|
579
|
+
`theme: "${key}" isn't a --twk-* token (${key.startsWith("--tw-") ? `renamed "--twk-${key.slice(5)}" in 0.7` : '"--twk-accent", say'}) — skipped`,
|
|
580
|
+
ambiguousPath: (key, n) =>
|
|
581
|
+
`set("${key}") is ambiguous — ${n} controls read as that path (a key holding "." beside a folder); rename one`,
|
|
292
582
|
noPath: (key) => `set("${key}") — no control at that path`,
|
|
293
583
|
noValue: (key, type) => `set("${key}") ignored — a ${type} holds no value to set`,
|
|
294
|
-
ambiguousKey: (key, n, eg) =>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
584
|
+
ambiguousKey: (key, n, eg) =>
|
|
585
|
+
`set("${key}") is ambiguous — ${n} controls share that key; use a dotted path (e.g. "${eg}")`,
|
|
586
|
+
springDur: (key, v, shown) =>
|
|
587
|
+
`"${key}": a spring visualDuration of ${v} s is past the editor's 0.01–10 s, so it uses ${shown} s`,
|
|
588
|
+
springKept: (key, k, d, m) =>
|
|
589
|
+
`"${key}": a spring of stiffness ${k}, damping ${d}, mass ${m} is past what the editor draws (0.01–10 s, bounce ±95%); the value keeps it as written, the editor shows the nearest`,
|
|
590
|
+
noControl: (key, group) =>
|
|
591
|
+
group
|
|
592
|
+
? `set("${key}") ignored — it's a folder/tabs group; set its children instead`
|
|
593
|
+
: `set("${key}") ignored — no control has that key`,
|
|
594
|
+
};
|
|
595
|
+
const notice = (kind: string, args: unknown[]) => {
|
|
596
|
+
const say = NOTICES[kind];
|
|
597
|
+
if (say) console.warn(`[tweaks] ${say(...args)}`);
|
|
298
598
|
};
|
|
299
|
-
const notice = (kind: string, args: unknown[]) => { const say = NOTICES[kind]; if (say) console.warn(`[tweaks] ${say(...args)}`); };
|
|
300
599
|
sayNow(notice);
|
|
301
600
|
|
|
302
|
-
export {
|
|
601
|
+
export {
|
|
602
|
+
escapeReverts,
|
|
603
|
+
dragPane,
|
|
604
|
+
badGradient,
|
|
605
|
+
notice,
|
|
606
|
+
decimalStep,
|
|
607
|
+
unknownType,
|
|
608
|
+
numField,
|
|
609
|
+
dragGesture,
|
|
610
|
+
svgEl,
|
|
611
|
+
cssVar,
|
|
612
|
+
accentColor,
|
|
613
|
+
selectAllOnFocus,
|
|
614
|
+
grabSurface,
|
|
615
|
+
boxFrac,
|
|
616
|
+
fitCanvas,
|
|
617
|
+
triggerRow,
|
|
618
|
+
springCurve,
|
|
619
|
+
springSpan,
|
|
620
|
+
springRun,
|
|
621
|
+
springFromTime,
|
|
622
|
+
timeFromSpring,
|
|
623
|
+
arrowStep,
|
|
624
|
+
sayWhenKeysStop,
|
|
625
|
+
SPRING_DUR,
|
|
626
|
+
};
|