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
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
// ── Point — 2D/3D/4D vector. Lazy.
|
|
2
|
-
import { el, clamp, stepPrecision, defaultRange, popover, registerControl } from "../shared.js";
|
|
3
|
-
import {
|
|
2
|
+
import { el, clamp, stepPrecision, defaultRange, popover, registerControl, words } from "../shared.js";
|
|
3
|
+
import {
|
|
4
|
+
numField,
|
|
5
|
+
svgEl,
|
|
6
|
+
grabSurface,
|
|
7
|
+
boxFrac,
|
|
8
|
+
arrowStep,
|
|
9
|
+
sayWhenKeysStop,
|
|
10
|
+
triggerRow,
|
|
11
|
+
decimalStep,
|
|
12
|
+
} from "../heavy.js";
|
|
4
13
|
import type { OnChange, Built } from "../shared.js";
|
|
5
14
|
import type { Meta, PointComponent } from "../schema.js";
|
|
6
15
|
import type { Control } from "../types.js";
|
|
@@ -8,19 +17,21 @@ import type { Control } from "../types.js";
|
|
|
8
17
|
// A trigger row opens the 2D pad over the component fields in a popover. The fields hold the values; readout, thumb
|
|
9
18
|
// and emitted map read off them. `pad: false` opts out.
|
|
10
19
|
function createPoint(meta: Meta, onChange: OnChange): Control {
|
|
20
|
+
const tr = words();
|
|
11
21
|
const comps = meta.components; // [{ key, label, value, step, min, max }]
|
|
12
22
|
|
|
13
23
|
// ── Trigger row — label + value + a mini pad preview. ──
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
24
|
+
// ??: an explicit "" label renders none
|
|
25
|
+
const { root, trigger, right } = triggerRow("twk-point", meta.label ?? tr("point", "Point"));
|
|
26
|
+
const valueEl = el("span", "twk-trigger-value");
|
|
27
|
+
const preview = el("div", "twk-trigger-chip twk-point-preview");
|
|
28
|
+
const previewDot = el("div", "twk-point-preview-dot");
|
|
18
29
|
preview.append(previewDot);
|
|
19
30
|
right.append(preview, valueEl); // chip first, as every chip row
|
|
20
31
|
|
|
21
32
|
// ── Popover — the 2D pad over the component fields. ──
|
|
22
|
-
const pop = el("div", "
|
|
23
|
-
const body = el("div", "
|
|
33
|
+
const pop = el("div", "twk-color-pop twk-point-pop");
|
|
34
|
+
const body = el("div", "twk-point-body");
|
|
24
35
|
pop.append(body);
|
|
25
36
|
|
|
26
37
|
const hasPad = meta.pad !== false && comps.length >= 2;
|
|
@@ -29,78 +40,150 @@ function createPoint(meta: Meta, onChange: OnChange): Control {
|
|
|
29
40
|
// its steps (337 over ±1011 stepped by 10 to 340), as the slider's step fits its default.
|
|
30
41
|
const rangeOf = (c: PointComponent) => {
|
|
31
42
|
const half = defaultRange(Math.abs(Number.isFinite(+c.value) ? +c.value : 0))[1];
|
|
32
|
-
let lo = c.min ?? -half,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
let lo = c.min ?? -half,
|
|
44
|
+
hi = c.max ?? half;
|
|
45
|
+
// A lone bound at or past the other end spans from itself instead, as the slider's does (min 5 over 0 spans 5–35,
|
|
46
|
+
// not 5–1, swapped).
|
|
47
|
+
if ((c.min == null) !== (c.max == null) && !(hi > lo)) {
|
|
48
|
+
const w = 2 * defaultRange(Math.abs(+(c.min ?? c.max)!))[1];
|
|
49
|
+
if (c.min == null) lo = hi - w;
|
|
50
|
+
else hi = lo + w;
|
|
51
|
+
}
|
|
52
|
+
// a degenerate range (hi ≤ lo, NaN) keeps a step of 1
|
|
53
|
+
const fine = 10 ** Math.floor(Math.log10((hi - lo) / 200)),
|
|
54
|
+
nudge = c.step ?? (fine > 0 && Number.isFinite(fine) ? fine : 1);
|
|
55
|
+
// the pad's arrows keep the decade: 3.14159's 0.00001 would crawl
|
|
56
|
+
return { lo, hi, nudge, step: c.step ?? Math.min(nudge, decimalStep(c.value), decimalStep(c.min)) };
|
|
37
57
|
};
|
|
38
58
|
const ranges = comps.map(rangeOf);
|
|
39
|
-
const rx = ranges[0],
|
|
40
|
-
|
|
59
|
+
const rx = ranges[0],
|
|
60
|
+
ry = ranges[1];
|
|
61
|
+
const minX = rx?.lo ?? -1,
|
|
62
|
+
maxX = rx?.hi ?? 1,
|
|
63
|
+
minY = ry?.lo ?? -1,
|
|
64
|
+
maxY = ry?.hi ?? 1;
|
|
41
65
|
const frac = (v: number, lo: number, hi: number) => (hi > lo ? clamp((v - lo) / (hi - lo), 0, 1) : 0.5);
|
|
42
66
|
|
|
43
|
-
let padHost: HTMLDivElement | null = null,
|
|
67
|
+
let padHost: HTMLDivElement | null = null,
|
|
68
|
+
padThumb: HTMLDivElement | null = null,
|
|
69
|
+
padLine: Built<SVGLineElement> | null = null;
|
|
44
70
|
if (hasPad) {
|
|
45
|
-
padHost = el("div", "
|
|
46
|
-
padThumb = el("div", "
|
|
71
|
+
padHost = el("div", "twk-pad");
|
|
72
|
+
padThumb = el("div", "twk-pad-thumb");
|
|
47
73
|
// A tether from the origin to the thumb, so the point reads as a vector; the pad is square so the angle is true.
|
|
48
|
-
const padSvg = svgEl("svg", "
|
|
49
|
-
|
|
74
|
+
const padSvg = svgEl("svg", "twk-pad-line");
|
|
75
|
+
padSvg.setAttribute("viewBox", "0 0 100 100");
|
|
76
|
+
padSvg.setAttribute("preserveAspectRatio", "none");
|
|
77
|
+
padLine = svgEl("line");
|
|
78
|
+
padSvg.append(padLine);
|
|
50
79
|
// The origin is fixed for the control's life: set x1/y1 once.
|
|
51
|
-
const oy0 = frac(0, minY, maxY);
|
|
52
|
-
|
|
80
|
+
const oy0 = frac(0, minY, maxY);
|
|
81
|
+
padLine.setAttribute("x1", frac(0, minX, maxX) * 100);
|
|
82
|
+
padLine.setAttribute("y1", (meta.invertY ? oy0 : 1 - oy0) * 100);
|
|
83
|
+
padHost.append(
|
|
84
|
+
el("div", "twk-pad-axis twk-pad-axis-x"),
|
|
85
|
+
el("div", "twk-pad-axis twk-pad-axis-y"),
|
|
86
|
+
padSvg,
|
|
87
|
+
padThumb,
|
|
88
|
+
);
|
|
53
89
|
body.append(padHost);
|
|
54
90
|
}
|
|
55
91
|
|
|
56
|
-
const fields = el("div", "
|
|
92
|
+
const fields = el("div", "twk-fields");
|
|
57
93
|
body.append(fields);
|
|
58
|
-
const sync = () => {
|
|
94
|
+
const sync = () => {
|
|
95
|
+
positionPad();
|
|
96
|
+
paintValue();
|
|
97
|
+
};
|
|
59
98
|
const emit = () => onChange(read());
|
|
60
99
|
const flds = comps.map((c, k) => {
|
|
61
|
-
const fld = numField(
|
|
62
|
-
|
|
100
|
+
const fld = numField(
|
|
101
|
+
{ label: c.label ?? c.key, value: c.value ?? 0, step: ranges[k].step, min: c.min, max: c.max },
|
|
102
|
+
() => {
|
|
103
|
+
sync();
|
|
104
|
+
emit();
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
fields.append(fld.el);
|
|
108
|
+
return fld;
|
|
63
109
|
});
|
|
64
|
-
|
|
110
|
+
// the emitted map, straight off the fields
|
|
111
|
+
const read = () => Object.fromEntries(comps.map((c, k) => [c.key, flds[k].get()]));
|
|
65
112
|
|
|
66
|
-
// Readout: the components at each step's precision, so a value reads "−1.00, −0.46" (steady columns), not
|
|
113
|
+
// Readout: the components at each step's precision, so a value reads "−1.00, −0.46" (steady columns), not
|
|
114
|
+
// "−1, −0.46".
|
|
67
115
|
const fmt = (v: number, step: number) => (+v).toFixed(stepPrecision(step));
|
|
68
|
-
const paintValue = () => {
|
|
69
|
-
|
|
116
|
+
const paintValue = () => {
|
|
117
|
+
valueEl.textContent = flds.map((f, k) => fmt(f.get(), ranges[k].step)).join(", ");
|
|
118
|
+
};
|
|
119
|
+
let positionPad = () => {
|
|
120
|
+
previewDot.style.left = "50%";
|
|
121
|
+
previewDot.style.top = "50%";
|
|
122
|
+
};
|
|
70
123
|
|
|
71
124
|
if (hasPad) {
|
|
72
125
|
// The mini preview is clipped, so inset the 5px dot: its center travels from +2.5px at 0 to −2.5px at 1.
|
|
73
126
|
const DOT = 5;
|
|
74
127
|
const inset = (f: number) => `calc(${(f * 100).toFixed(2)}% + ${((0.5 - f) * DOT).toFixed(2)}px)`;
|
|
75
128
|
positionPad = () => {
|
|
76
|
-
const fx = frac(flds[0].get(), minX, maxX),
|
|
129
|
+
const fx = frac(flds[0].get(), minX, maxX),
|
|
130
|
+
fyv = frac(flds[1].get(), minY, maxY);
|
|
77
131
|
const tyFrac = meta.invertY ? fyv : 1 - fyv;
|
|
78
|
-
const tx = fx * 100,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
132
|
+
const tx = fx * 100,
|
|
133
|
+
ty = tyFrac * 100;
|
|
134
|
+
padThumb.style.left = tx + "%";
|
|
135
|
+
padThumb.style.top = ty + "%";
|
|
136
|
+
previewDot.style.left = inset(fx);
|
|
137
|
+
previewDot.style.top = inset(tyFrac); // mini pad: dot stays in bounds
|
|
138
|
+
padLine.setAttribute("x2", tx);
|
|
139
|
+
padLine.setAttribute("y2", ty);
|
|
140
|
+
// the pad's live readout for assistive tech
|
|
141
|
+
padHost.setAttribute(
|
|
142
|
+
"aria-description",
|
|
143
|
+
`${comps[0].label ?? comps[0].key} ${fmt(flds[0].get(), rx.step)}, ${comps[1].label ?? comps[1].key} ${fmt(flds[1].get(), ry.step)}`,
|
|
144
|
+
);
|
|
83
145
|
};
|
|
84
146
|
const padSet = (e: PointerEvent) => {
|
|
85
|
-
const [fx, fy] = boxFrac(e, padHost);
|
|
86
|
-
|
|
87
|
-
|
|
147
|
+
const [fx, fy] = boxFrac(e, padHost);
|
|
148
|
+
const yFrac = meta.invertY ? fy : 1 - fy;
|
|
149
|
+
flds[0].set(minX + fx * (maxX - minX));
|
|
150
|
+
flds[1].set(minY + yFrac * (maxY - minY));
|
|
151
|
+
sync();
|
|
152
|
+
emit();
|
|
88
153
|
};
|
|
89
154
|
grabSurface(padHost, padSet);
|
|
90
155
|
// Keyboard: a focusable group (no ARIA role fits a 2D point; the readout rides on aria-description). Arrows move a
|
|
91
|
-
// pad step (⇧ = ten), ↑ always up on screen; Home restores the default. Escape is the popover's — a blur here would
|
|
92
|
-
|
|
93
|
-
padHost.
|
|
156
|
+
// pad step (⇧ = ten), ↑ always up on screen; Home restores the default. Escape is the popover's — a blur here would
|
|
157
|
+
// close it first.
|
|
158
|
+
padHost.tabIndex = 0;
|
|
159
|
+
padHost.setAttribute("role", "group");
|
|
160
|
+
padHost.setAttribute(
|
|
161
|
+
"aria-label",
|
|
162
|
+
tr("pointPad", "{label} pad: {x} and {y}", {
|
|
163
|
+
label: meta.label ?? tr("point", "Point"),
|
|
164
|
+
x: comps[0].label ?? comps[0].key,
|
|
165
|
+
y: comps[1].label ?? comps[1].key,
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
94
168
|
const say = sayWhenKeysStop(padHost);
|
|
95
169
|
padHost.addEventListener("keydown", (e) => {
|
|
96
|
-
if (e.key === "Home") {
|
|
170
|
+
if (e.key === "Home") {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
comps.forEach((c, k) => flds[k].set(c.value ?? 0));
|
|
173
|
+
sync();
|
|
174
|
+
emit();
|
|
175
|
+
say();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
97
178
|
const [dx, dy] = arrowStep(e);
|
|
98
179
|
if (!dx && !dy) return;
|
|
99
180
|
e.preventDefault();
|
|
100
181
|
const k = e.shiftKey ? 10 : 1;
|
|
101
182
|
if (dx) flds[0].set(clamp(flds[0].get() + dx * rx.nudge * k, minX, maxX));
|
|
102
183
|
if (dy) flds[1].set(clamp(flds[1].get() + (meta.invertY ? -dy : dy) * ry.nudge * k, minY, maxY));
|
|
103
|
-
sync();
|
|
184
|
+
sync();
|
|
185
|
+
emit();
|
|
186
|
+
say();
|
|
104
187
|
});
|
|
105
188
|
}
|
|
106
189
|
|
|
@@ -110,7 +193,13 @@ function createPoint(meta: Meta, onChange: OnChange): Control {
|
|
|
110
193
|
|
|
111
194
|
return {
|
|
112
195
|
el: root,
|
|
113
|
-
set: (v: Record<string, number | string>) => {
|
|
196
|
+
set: (v: Record<string, number | string>) => {
|
|
197
|
+
if (v)
|
|
198
|
+
comps.forEach((c, k) => {
|
|
199
|
+
if (v[c.key] != null) flds[k].set(v[c.key]);
|
|
200
|
+
});
|
|
201
|
+
sync();
|
|
202
|
+
},
|
|
114
203
|
get: read,
|
|
115
204
|
range: Object.fromEntries(comps.map((c, k) => [c.key, flds[k].range])),
|
|
116
205
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ── Shadow — a box-shadow editor with the kit's own picker mounted inline (popover() is single-open). The value carries
|
|
2
2
|
// the CSS as `css`, built only from the numbers and a color the picker accepted as one color token (isColor) — a host
|
|
3
3
|
// string reaching a `style` sink could splice in a url(). Lazy. ──
|
|
4
|
-
import { el, txt, popover, createSegmented, registerControl, btn, icon, isObj } from "../shared.js";
|
|
4
|
+
import { el, txt, popover, createSegmented, registerControl, btn, icon, isObj, words } from "../shared.js";
|
|
5
5
|
import { numField, triggerRow } from "../heavy.js";
|
|
6
6
|
import { createPickerBody, isColor } from "./color.js";
|
|
7
7
|
import type { OnChange, NumField } from "../shared.js";
|
|
@@ -16,64 +16,137 @@ const COLOR_RE = /#[0-9a-f]{3,8}\b|(?:oklch|oklab|rgba?|hsla?|hwb|lab|lch|color)
|
|
|
16
16
|
const LEN_RE = /^(-?\d*\.?\d+)(px)?$/;
|
|
17
17
|
// Strict: optional `inset`, two to four px lengths, one color, one layer; anything else is null.
|
|
18
18
|
const parseShadow = (s: string): ShadowInput | null => {
|
|
19
|
-
const m = COLOR_RE.exec(s);
|
|
19
|
+
const m = COLOR_RE.exec(s);
|
|
20
|
+
if (!m || !isColor(m[0])) return null;
|
|
20
21
|
const rest = (s.slice(0, m.index) + " " + s.slice(m.index + m[0].length)).trim().split(/\s+/).filter(Boolean);
|
|
21
|
-
const inset = rest.includes("inset"),
|
|
22
|
+
const inset = rest.includes("inset"),
|
|
23
|
+
lens = rest.filter((t) => t !== "inset");
|
|
22
24
|
if (lens.length < 2 || lens.length > 4 || !lens.every((t) => LEN_RE.test(t))) return null;
|
|
23
25
|
const n = lens.map((t) => +t.replace(/px$/, ""));
|
|
24
26
|
return { inset, x: n[0], y: n[1], blur: n[2] ?? 0, spread: n[3] ?? 0, color: m[0] };
|
|
25
27
|
};
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
// the kit's chevron; importing icons.ts splits a 4th chunk
|
|
30
|
+
const CHEVRON = icon('<path d="M18 9s-4.4 6-6 6-6-6-6-6"/>', "twk-shadow-chevron", 2.5);
|
|
28
31
|
function createShadow(meta: Meta, onChange: OnChange): Control {
|
|
32
|
+
const tr = words();
|
|
29
33
|
const read = (v: unknown): ShadowInput | null => (typeof v === "string" ? parseShadow(v) : isObj(v) ? v : null);
|
|
30
34
|
const init = read(meta.value ?? meta) || {}; // the fields may sit on the meta itself (verbose form)
|
|
31
35
|
let inset = !!init.inset;
|
|
32
36
|
|
|
33
37
|
// ── Row — label, chip, summary. ──
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
38
|
+
// ??: an explicit "" label renders none
|
|
39
|
+
const { root, trigger, right } = triggerRow("twk-shadow", meta.label ?? tr("shadow", "Shadow"));
|
|
40
|
+
const chip = el("span", "twk-trigger-chip twk-shadow-chip"),
|
|
41
|
+
chipTile = el("span", "twk-shadow-chip-tile");
|
|
42
|
+
chip.append(chipTile);
|
|
43
|
+
const valueEl = el("span", "twk-trigger-value");
|
|
37
44
|
right.append(chip, valueEl);
|
|
38
45
|
|
|
39
46
|
// ── Popover — a preview card, the position switch, the four lengths, the picker. ──
|
|
40
|
-
const pop = el("div", "
|
|
41
|
-
const stage = el("div", "
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const pop = el("div", "twk-color-pop twk-shadow-pop");
|
|
48
|
+
const stage = el("div", "twk-shadow-stage"),
|
|
49
|
+
card = el("div", "twk-shadow-card");
|
|
50
|
+
stage.append(card);
|
|
51
|
+
const insetSeg = createSegmented(
|
|
52
|
+
[
|
|
53
|
+
{ value: "outer", label: tr("outer", "Outer") },
|
|
54
|
+
{ value: "inner", label: tr("inner", "Inner") },
|
|
55
|
+
],
|
|
56
|
+
inset ? "inner" : "outer",
|
|
57
|
+
(m: unknown) => {
|
|
58
|
+
if ((m === "inner") !== inset) {
|
|
59
|
+
inset = m === "inner";
|
|
60
|
+
update();
|
|
61
|
+
emit();
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
tr("shadowPosition", "Shadow position"),
|
|
65
|
+
);
|
|
66
|
+
const insetRow = el("div", "twk-row");
|
|
67
|
+
insetRow.append(txt("span", "twk-row-label", tr("position", "Position")), insetSeg.el);
|
|
68
|
+
const fields = el("div", "twk-fields twk-shadow-fields");
|
|
69
|
+
// The four lengths live in their fields, read as the fields fit them: an authored 2.5 shows "3" and emits 3px, and
|
|
70
|
+
// the blur's min clamps a negative one to 0.
|
|
71
|
+
const field = (label: string, value: number, min?: number): NumField => {
|
|
72
|
+
const f = numField({ label, value, step: 1, min, unit: "px" }, () => {
|
|
73
|
+
update();
|
|
74
|
+
emit();
|
|
75
|
+
});
|
|
76
|
+
fields.append(f.el);
|
|
77
|
+
return f;
|
|
78
|
+
};
|
|
79
|
+
const fx = field("X", num(init.x, DEF.x)),
|
|
80
|
+
fy = field("Y", num(init.y, DEF.y)),
|
|
81
|
+
fb = field(tr("blur", "Blur"), num(init.blur, DEF.blur), 0),
|
|
82
|
+
fs = field(tr("spread", "Spread"), num(init.spread, DEF.spread));
|
|
49
83
|
const lengths = () => [fx, fy, fb, fs].map((f) => f.get());
|
|
50
84
|
// The color row discloses the picker, so the pop opens compact.
|
|
51
|
-
const colorRow = btn("
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
85
|
+
const colorRow = btn("twk-trigger twk-shadow-color");
|
|
86
|
+
colorRow.setAttribute("aria-expanded", "false");
|
|
87
|
+
const cSwatch = el("span", "twk-trigger-chip twk-color-swatch"),
|
|
88
|
+
cText = el("span", "twk-trigger-value"),
|
|
89
|
+
cRight = el("span", "twk-trigger-right");
|
|
90
|
+
cRight.append(cSwatch, cText);
|
|
91
|
+
cRight.insertAdjacentHTML("beforeend", CHEVRON);
|
|
92
|
+
colorRow.append(txt("span", "twk-trigger-label", tr("color", "Color")), cRight);
|
|
93
|
+
const body = createPickerBody(
|
|
94
|
+
{ value: typeof init.color === "string" && isColor(init.color) ? init.color : DEF.color, onMode: () => update() },
|
|
95
|
+
() => {
|
|
96
|
+
update();
|
|
97
|
+
emit();
|
|
98
|
+
},
|
|
99
|
+
);
|
|
55
100
|
body.el.hidden = true;
|
|
56
|
-
|
|
101
|
+
// re-place for the new height; the picker lays out now
|
|
102
|
+
colorRow.addEventListener("click", () => {
|
|
103
|
+
const show = body.el.hidden;
|
|
104
|
+
body.el.hidden = !show;
|
|
105
|
+
colorRow.setAttribute("aria-expanded", String(show));
|
|
106
|
+
pop_.reflow();
|
|
107
|
+
});
|
|
57
108
|
pop.append(stage, insetRow, fields, colorRow, body.el);
|
|
58
109
|
root.append(pop);
|
|
59
110
|
|
|
60
|
-
const cssAt = (k: number) =>
|
|
61
|
-
|
|
111
|
+
const cssAt = (k: number) =>
|
|
112
|
+
`${inset ? "inset " : ""}${lengths()
|
|
113
|
+
.map((n) => `${fmt(n * k)}px`)
|
|
114
|
+
.join(" ")} ${body.get()}`;
|
|
115
|
+
const value = (): ShadowValue => {
|
|
116
|
+
const [x, y, blur, spread] = lengths().map((n) => +fmt(n));
|
|
117
|
+
return { inset, x, y, blur, spread, color: body.get(), css: cssAt(1) };
|
|
118
|
+
};
|
|
62
119
|
const emit = () => onChange(value());
|
|
63
120
|
const update = () => {
|
|
64
|
-
card.style.boxShadow = cssAt(1);
|
|
121
|
+
card.style.boxShadow = cssAt(1);
|
|
122
|
+
chipTile.style.boxShadow = cssAt(0.3);
|
|
65
123
|
const [x, y, blur, spread] = lengths();
|
|
66
|
-
valueEl.textContent = `${inset ? "in " : ""}${fmt(x)} ${fmt(y)} ${fmt(blur)}${spread ? " " + fmt(spread) : ""}`;
|
|
67
|
-
cSwatch.style.background = body.swatchCss();
|
|
124
|
+
valueEl.textContent = `${inset ? tr("inset", "in") + " " : ""}${fmt(x)} ${fmt(y)} ${fmt(blur)}${spread ? " " + fmt(spread) : ""}`;
|
|
125
|
+
cSwatch.style.background = body.swatchCss();
|
|
126
|
+
cText.textContent = body.valueText();
|
|
68
127
|
};
|
|
69
128
|
const pop_ = popover(root, trigger, pop, { fallbackH: 300, onOpen: body.reflow, onReflow: body.reflow });
|
|
70
129
|
update();
|
|
71
130
|
|
|
72
|
-
// Set/restore: the object form or a strict box-shadow string; `css` is ignored, and the color goes through the
|
|
131
|
+
// Set/restore: the object form or a strict box-shadow string; `css` is ignored, and the color goes through the
|
|
132
|
+
// picker.
|
|
73
133
|
const set = (v: unknown) => {
|
|
74
|
-
const inp = read(v);
|
|
75
|
-
if (inp
|
|
76
|
-
|
|
134
|
+
const inp = read(v);
|
|
135
|
+
if (!inp) return;
|
|
136
|
+
if (inp.inset != null) {
|
|
137
|
+
inset = !!inp.inset;
|
|
138
|
+
insetSeg.set(inset ? "inner" : "outer");
|
|
139
|
+
}
|
|
140
|
+
(
|
|
141
|
+
[
|
|
142
|
+
["x", fx],
|
|
143
|
+
["y", fy],
|
|
144
|
+
["blur", fb],
|
|
145
|
+
["spread", fs],
|
|
146
|
+
] as const
|
|
147
|
+
).forEach(([k, f]) => {
|
|
148
|
+
if (inp[k] != null) f.set(num(inp[k], f.get()));
|
|
149
|
+
});
|
|
77
150
|
if (typeof inp.color === "string" && isColor(inp.color)) body.set(inp.color);
|
|
78
151
|
update();
|
|
79
152
|
};
|