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
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
/* Tweaks — what only the lazy controls use: the numeric field engine and its scrub, the
|
|
2
|
+
* press-drag gesture, the SVG factory, canvas sizing and the modal-trigger row, kept out of
|
|
3
|
+
* the shared chunk. It also registers the Number control, which no shorthand infers. */
|
|
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";
|
|
22
|
+
import type { Built, NumSpec, NumField, OnChange } from "./shared.js";
|
|
23
|
+
import type { Meta } from "./schema.js";
|
|
24
|
+
|
|
25
|
+
const svgNS = "http://www.w3.org/2000/svg";
|
|
26
|
+
// The SVG twin of shared.ts's el().
|
|
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
|
+
};
|
|
32
|
+
// A token reads back as its light-dark(light, dark) pair, which a canvas can't parse: keep the arm the node's
|
|
33
|
+
// color-scheme paints with ("light dark" follows the OS), innermost pairs included, as CSS would.
|
|
34
|
+
const pickScheme = (v: string, cs: CSSStyleDeclaration) => {
|
|
35
|
+
if (!/light-dark\(/i.test(v)) return v;
|
|
36
|
+
const s = cs.colorScheme || "",
|
|
37
|
+
dark = /\bdark\b/.test(s) && (!/\blight\b/.test(s) || matchMedia("(prefers-color-scheme: dark)").matches);
|
|
38
|
+
for (let i; (i = v.search(/light-dark\(/i)) !== -1;) {
|
|
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
|
+
}
|
|
50
|
+
if (comma < 0 || j >= v.length) return ""; // malformed: the caller's fallback color
|
|
51
|
+
v = v.slice(0, i) + (dark ? v.slice(comma + 1, j) : v.slice(i + 11, comma)).trim() + v.slice(j + 1);
|
|
52
|
+
}
|
|
53
|
+
return v;
|
|
54
|
+
};
|
|
55
|
+
const cssVar = (node: Element, name: string) => {
|
|
56
|
+
const r = getComputedStyle(node);
|
|
57
|
+
return pickScheme(r.getPropertyValue(name).trim(), r);
|
|
58
|
+
};
|
|
59
|
+
// One getComputedStyle for both reads — this runs in the FPS draw loop.
|
|
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
|
+
};
|
|
65
|
+
// Select the whole value on focus (the hex field: you type a new color, never edit a digit).
|
|
66
|
+
// Browsers collapse a focus-time selection on the click's own mouseup, so the first primary
|
|
67
|
+
// mouseup after a pointer-initiated focus is swallowed while the whole value is still selected.
|
|
68
|
+
const selectAllOnFocus = (input: HTMLInputElement) => {
|
|
69
|
+
let swallowUp = false;
|
|
70
|
+
input.addEventListener("pointerdown", (e) => {
|
|
71
|
+
swallowUp = e.button === 0 && activeIn(input) !== input;
|
|
72
|
+
});
|
|
73
|
+
input.addEventListener("focus", () => input.select());
|
|
74
|
+
input.addEventListener("mouseup", (e) => {
|
|
75
|
+
if (!swallowUp) return;
|
|
76
|
+
swallowUp = false;
|
|
77
|
+
if (
|
|
78
|
+
e.button === 0 &&
|
|
79
|
+
activeIn(input) === input &&
|
|
80
|
+
input.selectionStart === 0 &&
|
|
81
|
+
input.selectionEnd === input.value.length
|
|
82
|
+
)
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
// Escape in an edited field restores `kept` and stops; otherwise it passes on and closes the pane. Without this,
|
|
87
|
+
// closing blurred the field and committed the half-typed value.
|
|
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
|
+
});
|
|
154
|
+
// Press-drag on a node: onDown on pointerdown (pointer captured), onMove per move, onEnd on
|
|
155
|
+
// up/cancel/lost capture or when the button releases off the node (buttons===0).
|
|
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
|
+
) {
|
|
164
|
+
let activeId: number | null = null; // one captured pointer; a second finger can't fork the drag
|
|
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);
|
|
185
|
+
node.addEventListener("lostpointercapture", end); // capture lost mid-drag (popover unmounting) ends it too
|
|
186
|
+
}
|
|
187
|
+
// Press-drag flagging .is-grabbing on the surface for the gesture's run (the thumb-lift CSS keys off it).
|
|
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
|
+
});
|
|
197
|
+
// Pointer position inside a box as [x, y] fractions in 0–1.
|
|
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
|
+
};
|
|
202
|
+
// An arrow key as a [dx, dy] step, ↑ positive as on screen; [0, 0] for any other key.
|
|
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
|
+
];
|
|
207
|
+
// A 2D surface's readout rides on aria-description, which a screen reader speaks on focus and never again: the returned
|
|
208
|
+
// function says it once a run of keys stops.
|
|
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
|
+
};
|
|
218
|
+
// Size a canvas to its CSS box × devicePixelRatio (capped) and scale the context to CSS pixels; returns [cssW, cssH].
|
|
219
|
+
const fitCanvas = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, maxDpr = Infinity): [number, number] => {
|
|
220
|
+
const r = canvas.getBoundingClientRect();
|
|
221
|
+
const dpr = Math.min(window.devicePixelRatio || 1, maxDpr);
|
|
222
|
+
canvas.width = r.width * dpr;
|
|
223
|
+
canvas.height = r.height * dpr;
|
|
224
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
225
|
+
return [r.width, r.height];
|
|
226
|
+
};
|
|
227
|
+
// The modal-trigger row (color, gradient, point): a row button, label left, a preview cluster (`right`) the caller
|
|
228
|
+
// fills.
|
|
229
|
+
const triggerRow = (cls: string, label: string) => {
|
|
230
|
+
const root = el("div", cls);
|
|
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);
|
|
235
|
+
root.append(trigger);
|
|
236
|
+
return { root, trigger, right };
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// A 2-bar drag handle (the icon sets' grips are dots).
|
|
240
|
+
const ICON_GRIP = icon('<path d="M6 4v8M10 4v8"/>', "", 2, 16); // 1.5px bars at the 12px it is drawn at
|
|
241
|
+
|
|
242
|
+
// Grab guide — a dotted line from the grab point to the cursor plus a value bubble, portaled
|
|
243
|
+
// to <body> for a drag. Its four spans (line, dot, arrow, bubble) are addressed by index.
|
|
244
|
+
interface GuideEl extends HTMLDivElement {
|
|
245
|
+
readonly children: HTMLCollectionOf<HTMLSpanElement>;
|
|
246
|
+
}
|
|
247
|
+
function makeGrabGuide() {
|
|
248
|
+
let g: GuideEl | null = null,
|
|
249
|
+
y = 0,
|
|
250
|
+
x0 = 0,
|
|
251
|
+
bx = 0;
|
|
252
|
+
return {
|
|
253
|
+
show(x: number, atY: number, bubbleX: number | undefined, anchor: Element) {
|
|
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
|
|
259
|
+
g.children[1].style.cssText = `left:${x}px;top:${y}px`;
|
|
260
|
+
carrySkin(g, anchor); // the anchor panel's theme + scheme, like every portal
|
|
261
|
+
portalTo(g, anchor);
|
|
262
|
+
},
|
|
263
|
+
move(x: number, text: string) {
|
|
264
|
+
if (!g) return;
|
|
265
|
+
g.children[0].style.cssText = `left:${Math.min(x0, x)}px;top:${y}px;width:${Math.abs(x - x0)}px`;
|
|
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
|
+
}
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Drag-to-scrub on a grab handle: 1px ≈ one step (Shift ×10, Alt ×0.1), re-anchoring on a
|
|
281
|
+
// modifier change so the value never jumps. read() is the live value, text() the bubble label.
|
|
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();
|
|
294
|
+
// Every end path matters: capture lost mid-scrub must still hide the full-screen guide.
|
|
295
|
+
dragGesture(grab, {
|
|
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
|
+
},
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── The labeled numeric field — ONE numeric engine: a sanitized step, min-anchored
|
|
324
|
+
// round-to-step, optional `soft`, a text input committing on change/Enter, and the grab
|
|
325
|
+
// handle. Two chromes: the boxed field, and with `spec.row` the row that IS the Number control. ──
|
|
326
|
+
function numField(spec: NumSpec, onChange?: (v: number) => void): NumField {
|
|
327
|
+
// A 0/negative/non-finite step breaks round-to-step (reachable via a point component's step).
|
|
328
|
+
const step = Number.isFinite(+spec.step) && +spec.step > 0 ? +spec.step : 1;
|
|
329
|
+
const bound = (b: unknown) => (b == null ? NaN : +b); // null → NaN → unbounded (a null max coerced to 0)
|
|
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
|
+
};
|
|
352
|
+
let value = fit(Number.isFinite(+spec.value) ? +spec.value : 0);
|
|
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);
|
|
363
|
+
quietFocus(inp); // click-to-edit stays ringless; Tab rings
|
|
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
|
+
};
|
|
374
|
+
// Typed text is read strictly as a whole number, as the slider's editor reads it (not parseFloat, which takes "1,000"
|
|
375
|
+
// as 1 and "12abc" as 12); the field's unit may be typed along. NaN for anything else.
|
|
376
|
+
const typed = () => Number(inp.value.replace(spec.unit || "", "").trim() || NaN);
|
|
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
|
+
});
|
|
384
|
+
escapeReverts(inp, () => value.toFixed(decimals));
|
|
385
|
+
// ↑/↓ step off the half-typed value (a typed "4" then ↑ reads 5), the keyboard twin of the scrub.
|
|
386
|
+
inp.addEventListener("keydown", (e) => {
|
|
387
|
+
if (e.key === "Enter") return inp.blur();
|
|
388
|
+
const d = e.key === "ArrowUp" ? 1 : e.key === "ArrowDown" ? -1 : 0;
|
|
389
|
+
if (!d) return;
|
|
390
|
+
e.preventDefault();
|
|
391
|
+
const n = typed();
|
|
392
|
+
set((Number.isFinite(n) ? n : value) + d * step * (e.shiftKey ? 10 : 1));
|
|
393
|
+
});
|
|
394
|
+
attachScrub(
|
|
395
|
+
grab,
|
|
396
|
+
wrap,
|
|
397
|
+
step,
|
|
398
|
+
() => value,
|
|
399
|
+
set,
|
|
400
|
+
() => inp.value,
|
|
401
|
+
);
|
|
402
|
+
return { el: root, set: (val: number | string) => set(val, false), get: () => value, range: [lo, hi] };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── Number — numField in its row chrome. ──
|
|
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.
|
|
412
|
+
const decimalStep = (v: unknown) => {
|
|
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++;
|
|
420
|
+
return 10 ** k;
|
|
421
|
+
};
|
|
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
|
|
423
|
+
// lazy chunk, so the basic panel carries none of it.
|
|
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
|
+
);
|
|
430
|
+
|
|
431
|
+
// ── Spring step response — shared by the spring and motion controls. The window runs to the
|
|
432
|
+
// settle, capped at 2.2 s. ──
|
|
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.
|
|
437
|
+
const CRIT = (2 * Math.PI) / 1.2;
|
|
438
|
+
// The visualDuration (s) the spring editor shows and resolves: down to 10 ms, up to the 10 s a spring's linear() runs.
|
|
439
|
+
const SPRING_DUR = [0.01, 10] as const;
|
|
440
|
+
const bounceRatio = (b: number) => (b < 0 ? 1 / Math.max(1 + b, 0.05) : Math.max(1 - b, 0.05));
|
|
441
|
+
const overdampedScale = (z: number) => {
|
|
442
|
+
if (!(z > 1)) return 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);
|
|
447
|
+
let x = CRIT;
|
|
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));
|
|
451
|
+
return x / CRIT;
|
|
452
|
+
};
|
|
453
|
+
const springFromTime = (visualDuration: number, bounce: number) => {
|
|
454
|
+
const z = bounceRatio(bounce),
|
|
455
|
+
root = (CRIT / visualDuration) * overdampedScale(z),
|
|
456
|
+
k = root * root;
|
|
457
|
+
return { stiffness: k, damping: 2 * z * Math.sqrt(k), mass: 1 };
|
|
458
|
+
};
|
|
459
|
+
// Its inverse, for physics keys handed to a time-mode editor: the visualDuration + bounce Motion would resolve to them.
|
|
460
|
+
const timeFromSpring = (k: number, d: number, m = 1) => {
|
|
461
|
+
const z = d / (2 * Math.sqrt(k * m));
|
|
462
|
+
return { visualDuration: (CRIT * overdampedScale(z)) / Math.sqrt(k / m), bounce: z > 1 ? 1 / z - 1 : 1 - z };
|
|
463
|
+
};
|
|
464
|
+
// How fast the slowest mode dies out: ζω₀, or an overdamped spring's slow root (ζω₀ is its fast one, so the preview
|
|
465
|
+
// window shrank to milliseconds and drew a near-flat line).
|
|
466
|
+
const decayRate = (w0: number, z: number) => (z > 1 ? w0 * (z - Math.sqrt(z * z - 1)) : z * w0);
|
|
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));
|
|
469
|
+
// Closed-form step response of a damped harmonic oscillator (under/critical/over): position at time t.
|
|
470
|
+
const springAt = (k: number, d: number, m: number) => {
|
|
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
|
+
}
|
|
477
|
+
if (z <= 1 + 1e-4) return (t: number) => 1 - Math.exp(-w0 * t) * (1 + w0 * t);
|
|
478
|
+
const s = Math.sqrt(z * z - 1),
|
|
479
|
+
a = w0 * (z + s),
|
|
480
|
+
b = w0 * (z - s);
|
|
481
|
+
return (t: number) => 1 - (a * Math.exp(-b * t) - b * Math.exp(-a * t)) / (a - b);
|
|
482
|
+
};
|
|
483
|
+
// N samples over the span, for drawing.
|
|
484
|
+
function springCurve(k: number, d: number, m: number, N = 64, T = springSpan(k, d, m)) {
|
|
485
|
+
const x = springAt(k, d, m),
|
|
486
|
+
out: number[] = [];
|
|
487
|
+
for (let i = 0; i < N; i++) out.push(x((i / (N - 1)) * T));
|
|
488
|
+
return out;
|
|
489
|
+
}
|
|
490
|
+
// The run as a CSS transition: how long, and a linear() easing. It lasts until within e⁻⁷ of rest, by the slowest mode
|
|
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.
|
|
494
|
+
const springRun = (k: number, d: number, m: number) => {
|
|
495
|
+
const w0 = Math.sqrt(k / m),
|
|
496
|
+
z = d / (2 * Math.sqrt(k * m)),
|
|
497
|
+
x = springAt(k, d, m);
|
|
498
|
+
const span = Math.min(7 / Math.max(decayRate(w0, z), 1e-3), 10);
|
|
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);
|
|
501
|
+
const ts = Array.from({ length: n }, (_, i) => (i / (n - 1)) * span);
|
|
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);
|
|
504
|
+
ts.sort((a, b) => a - b);
|
|
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)}%`);
|
|
508
|
+
return { duration: Math.round(span * 1000), easing: `linear(${stops.join(", ")})` };
|
|
509
|
+
};
|
|
510
|
+
|
|
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
|
+
});
|
|
530
|
+
const nearest = (s: string, names: string[]) => {
|
|
531
|
+
const a = s.toLowerCase(); // case-blind, so "textmuted" finds "textMuted"
|
|
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
|
+
});
|
|
543
|
+
};
|
|
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
|
+
);
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const warned = new Set<string>();
|
|
558
|
+
const badGradient = (fn: string, value: unknown, all: unknown, n: number) => {
|
|
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
|
+
}
|
|
568
|
+
};
|
|
569
|
+
// The words for the core's notices (shared.ts notice()): what went wrong and the way out, downloaded only when one
|
|
570
|
+
// fires.
|
|
571
|
+
const NOTICES: Record<string, (...a: any[]) => string> = {
|
|
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`,
|
|
576
|
+
pointValue: (key, comps) => `"${key}": a point value that isn't a number starts at 0 (${comps})`,
|
|
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`,
|
|
582
|
+
noPath: (key) => `set("${key}") — no control at that path`,
|
|
583
|
+
noValue: (key, type) => `set("${key}") ignored — a ${type} holds no value to set`,
|
|
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)}`);
|
|
598
|
+
};
|
|
599
|
+
sayNow(notice);
|
|
600
|
+
|
|
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
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* Inline SVG icons for the kit's core chrome (toolbar, folder chevron, select, hint marker),
|
|
2
|
+
* kept in one module so the license notice below covers them all. */
|
|
3
|
+
// ── Icons for the toolbar ──
|
|
4
|
+
// Adapted from Hugeicons' free set (MIT; per-icon origins in ../../THIRD-PARTY-NOTICES.md), path
|
|
5
|
+
// data compacted. The icon color is translucent, so strokes that cross double-composite into a
|
|
6
|
+
// bright hotspot: each icon is one path. Copy and presets keep separate parts so the hover can
|
|
7
|
+
// move them (tweaks.css), drawn to abut rather than overlap.
|
|
8
|
+
import { icon } from "./shared.js";
|
|
9
|
+
const ICON_COPY = icon(
|
|
10
|
+
'<path class="twk-copy-back" d="m5.5 16.5-.7-.2q-1.5-.6-2.1-2.1-.2-.7-.2-2.7v-2c0-3.3 0-5 1-6s2.7-1 6-1h2c1.4 0 2 0 2.7.2q1.6.5 2.1 2.1l.2.7"/><path class="twk-copy-front" d="M7.5 14.5c0-3.3 0-5 1-6s2.7-1 6-1 5 0 6 1 1 2.7 1 6 0 5-1 6-2.7 1-6 1-5 0-6-1-1-2.7-1-6"/>',
|
|
11
|
+
"twk-toolbar-btn__copy",
|
|
12
|
+
);
|
|
13
|
+
const ICON_CHECK = icon('<path d="m5 14 3.5 3.5L19 6.5"/>', "twk-toolbar-btn__check", 2.4);
|
|
14
|
+
// Reset is Hugeicons' undo, a near-full circle, so the spin on click reads as a turn about the center.
|
|
15
|
+
const ICON_RESET = icon('<path d="M3 12a9 9 0 1 0 9-9 9 9 0 0 0-7.6 4.2M3.3 3l.2 2q0 2.2.6 2.7t2.7.2l2-.2"/>');
|
|
16
|
+
const chevronIcon = (cls: string) => icon('<path d="M18 9s-4.4 6-6 6-6-6-6-6"/>', cls, 2.5);
|
|
17
|
+
const ICON_CHEVRON = chevronIcon("twk-chevron");
|
|
18
|
+
const ICON_PRESETS = icon(
|
|
19
|
+
'<path d="M20.2 13.5q1.8.8 1.8 1.5 0 .8-2.7 2l-5 2.2Q13 20 12 20q-.7 0-2.4-.8l-4.9-2.3Q2 16 2 15t1.8-1.5"/><path class="twk-layer-top" d="M9.6 4.8Q11.3 4 12 4t2.4.8l4.9 2.3Q22 8 22 9c0 .7-.9 1.1-2.7 2l-5 2.2Q13 14 12 14q-.7 0-2.4-.8l-4.9-2.3Q2 10 2 9c0-.7.9-1.1 2.7-2z"/>',
|
|
20
|
+
);
|
|
21
|
+
const ICON_X = icon('<path d="M18 6 6 18m12 0L6 6"/>', "", 2.2);
|
|
22
|
+
const ICON_INFO = icon(
|
|
23
|
+
'<circle cx="12" cy="12" r="10"/><path d="M12 16v-4m-.25-3.75a.25.25 0 0 0 .5 0 .25.25 0 0 0-.5 0"/>',
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export { ICON_COPY, ICON_CHECK, ICON_RESET, ICON_CHEVRON, ICON_PRESETS, ICON_X, ICON_INFO, chevronIcon };
|