aporia 0.1.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/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/ThemeProvider.d.ts +13 -0
- package/dist/ThemeProvider.d.ts.map +1 -0
- package/dist/components/ColorRow.d.ts +7 -0
- package/dist/components/ColorRow.d.ts.map +1 -0
- package/dist/components/GradientPicker.d.ts +19 -0
- package/dist/components/GradientPicker.d.ts.map +1 -0
- package/dist/components/GradientRow.d.ts +12 -0
- package/dist/components/GradientRow.d.ts.map +1 -0
- package/dist/components/SliderOverlapDebug.d.ts +6 -0
- package/dist/components/SliderOverlapDebug.d.ts.map +1 -0
- package/dist/components/SliderRow.d.ts +11 -0
- package/dist/components/SliderRow.d.ts.map +1 -0
- package/dist/components/SwatchPopover.d.ts +16 -0
- package/dist/components/SwatchPopover.d.ts.map +1 -0
- package/dist/components/ToggleRow.d.ts +7 -0
- package/dist/components/ToggleRow.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1169 -0
- package/dist/index.js.map +1 -0
- package/dist/style.css +977 -0
- package/dist/utils/isTextInputTarget.d.ts +3 -0
- package/dist/utils/isTextInputTarget.d.ts.map +1 -0
- package/package.json +78 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1169 @@
|
|
|
1
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useContext, createContext, useCallback, useRef, useLayoutEffect } from "react";
|
|
3
|
+
import { useMotionValue, useSpring, useTransform, motion } from "motion/react";
|
|
4
|
+
import { Popover } from "@base-ui/react";
|
|
5
|
+
function isTextInputTarget(target) {
|
|
6
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
7
|
+
if (target.isContentEditable) return true;
|
|
8
|
+
const tag = target.tagName;
|
|
9
|
+
if (tag === "TEXTAREA" || tag === "SELECT") return true;
|
|
10
|
+
if (tag !== "INPUT") return false;
|
|
11
|
+
const type = target.type;
|
|
12
|
+
return type === "text" || type === "search" || type === "url" || type === "tel" || type === "email" || type === "password" || type === "number" || type === "";
|
|
13
|
+
}
|
|
14
|
+
const SliderOverlapDebugContext = createContext(false);
|
|
15
|
+
function SliderOverlapDebugProvider({ children }) {
|
|
16
|
+
const [enabled, setEnabled] = useState(false);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const onKeyDown = (e) => {
|
|
19
|
+
if (!e.shiftKey || e.repeat) return;
|
|
20
|
+
if (e.code !== "KeyD") return;
|
|
21
|
+
if (isTextInputTarget(e.target)) return;
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
setEnabled((v) => !v);
|
|
24
|
+
};
|
|
25
|
+
window.addEventListener("keydown", onKeyDown);
|
|
26
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
27
|
+
}, []);
|
|
28
|
+
return /* @__PURE__ */ jsx(SliderOverlapDebugContext.Provider, { value: enabled, children });
|
|
29
|
+
}
|
|
30
|
+
function useSliderOverlapDebugEnabled() {
|
|
31
|
+
return useContext(SliderOverlapDebugContext);
|
|
32
|
+
}
|
|
33
|
+
const STORAGE_KEY = "aporia-theme";
|
|
34
|
+
const ThemeContext = createContext(null);
|
|
35
|
+
function readStoredTheme() {
|
|
36
|
+
try {
|
|
37
|
+
const s = localStorage.getItem(STORAGE_KEY);
|
|
38
|
+
if (s === "light" || s === "dark") return s;
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
return "dark";
|
|
42
|
+
}
|
|
43
|
+
function ThemeProvider({ children }) {
|
|
44
|
+
const [theme, setThemeState] = useState(readStoredTheme);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
document.documentElement.dataset.theme = theme;
|
|
47
|
+
try {
|
|
48
|
+
localStorage.setItem(STORAGE_KEY, theme);
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
}, [theme]);
|
|
52
|
+
const setTheme = useCallback((t) => setThemeState(t), []);
|
|
53
|
+
const toggleTheme = useCallback(() => {
|
|
54
|
+
setThemeState((t) => t === "dark" ? "light" : "dark");
|
|
55
|
+
}, []);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const onKeyDown = (e) => {
|
|
58
|
+
if (!e.shiftKey || e.repeat) return;
|
|
59
|
+
if (e.code !== "KeyT") return;
|
|
60
|
+
if (isTextInputTarget(e.target)) return;
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
toggleTheme();
|
|
63
|
+
};
|
|
64
|
+
window.addEventListener("keydown", onKeyDown);
|
|
65
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
66
|
+
}, [toggleTheme]);
|
|
67
|
+
return /* @__PURE__ */ jsx(ThemeContext.Provider, { value: { theme, setTheme, toggleTheme }, children });
|
|
68
|
+
}
|
|
69
|
+
function useTheme() {
|
|
70
|
+
const ctx = useContext(ThemeContext);
|
|
71
|
+
if (!ctx) {
|
|
72
|
+
throw new Error("useTheme must be used within ThemeProvider");
|
|
73
|
+
}
|
|
74
|
+
return ctx;
|
|
75
|
+
}
|
|
76
|
+
function clearDocumentSelection() {
|
|
77
|
+
var _a;
|
|
78
|
+
const sel = (_a = window.getSelection) == null ? void 0 : _a.call(window);
|
|
79
|
+
if (sel && sel.rangeCount > 0) sel.removeAllRanges();
|
|
80
|
+
}
|
|
81
|
+
function stepAllowsDecimal(step) {
|
|
82
|
+
if (!Number.isFinite(step) || step === 0) return true;
|
|
83
|
+
return step % 1 !== 0;
|
|
84
|
+
}
|
|
85
|
+
function sanitizeValueDraft(raw, min, step) {
|
|
86
|
+
const allowNeg = min < 0;
|
|
87
|
+
const allowDecimal = stepAllowsDecimal(step);
|
|
88
|
+
let out = "";
|
|
89
|
+
let dotUsed = false;
|
|
90
|
+
for (let i = 0; i < raw.length; i++) {
|
|
91
|
+
const c = raw[i];
|
|
92
|
+
if (allowNeg && c === "-" && out === "") {
|
|
93
|
+
out += "-";
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (allowDecimal && c === "." && !dotUsed) {
|
|
97
|
+
dotUsed = true;
|
|
98
|
+
out += ".";
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (c >= "0" && c <= "9") {
|
|
102
|
+
out += c;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
function edgeStretchPx(u, maxStretch, edge, dragBoost) {
|
|
109
|
+
if (!Number.isFinite(u)) return { left: 0, right: 0 };
|
|
110
|
+
const uClamped = Math.min(1, Math.max(0, u));
|
|
111
|
+
let left = 0;
|
|
112
|
+
let right = 0;
|
|
113
|
+
if (uClamped < edge) left = (1 - uClamped / edge) * maxStretch;
|
|
114
|
+
else if (uClamped > 1 - edge) right = (uClamped - (1 - edge)) / edge * maxStretch;
|
|
115
|
+
return { left: left * dragBoost, right: right * dragBoost };
|
|
116
|
+
}
|
|
117
|
+
const MAX_EDGE_STRETCH_PX = 2;
|
|
118
|
+
const EDGE_FRACTION = 0.14;
|
|
119
|
+
const AT_END_DRAG_BOOST = 1.04;
|
|
120
|
+
const PULL_GAIN = 0.05;
|
|
121
|
+
const PULL_CAP_PX = 3;
|
|
122
|
+
const STRETCH_SNAP_EPS_PX = 0.4;
|
|
123
|
+
const stretchSpringOpts = {
|
|
124
|
+
stiffness: 340,
|
|
125
|
+
damping: 42,
|
|
126
|
+
mass: 0.4,
|
|
127
|
+
restDelta: 0.012,
|
|
128
|
+
restSpeed: 0.045
|
|
129
|
+
};
|
|
130
|
+
const pullSpringOpts = {
|
|
131
|
+
stiffness: 520,
|
|
132
|
+
damping: 46,
|
|
133
|
+
mass: 0.34,
|
|
134
|
+
restDelta: 0.012,
|
|
135
|
+
restSpeed: 0.045
|
|
136
|
+
};
|
|
137
|
+
const THUMB_WIDTH_PX = 2;
|
|
138
|
+
const THUMB_HEIGHT_PX = 16;
|
|
139
|
+
const THUMB_HEIGHT_BEHIND_LABEL_PX = 10;
|
|
140
|
+
const THUMB_INSET_FROM_FILL_END_PX = 10;
|
|
141
|
+
const THUMB_OVERLAP_PAD_PX = 10;
|
|
142
|
+
const VALUE_EDIT_WIDTH_CARET_PAD_PX = 6;
|
|
143
|
+
function SliderRow({
|
|
144
|
+
label,
|
|
145
|
+
value,
|
|
146
|
+
min,
|
|
147
|
+
max,
|
|
148
|
+
step,
|
|
149
|
+
onChange,
|
|
150
|
+
fmt
|
|
151
|
+
}) {
|
|
152
|
+
const { theme } = useTheme();
|
|
153
|
+
const debugOverlap = useSliderOverlapDebugEnabled();
|
|
154
|
+
const [editing, setEditing] = useState(false);
|
|
155
|
+
const [hovered, setHovered] = useState(false);
|
|
156
|
+
const [dragging, setDragging] = useState(false);
|
|
157
|
+
const [draft, setDraft] = useState("");
|
|
158
|
+
const [mounted, setMounted] = useState(false);
|
|
159
|
+
const inputRef = useRef(null);
|
|
160
|
+
const cardRef = useRef(null);
|
|
161
|
+
const wrapRef = useRef(null);
|
|
162
|
+
const labelTextRef = useRef(null);
|
|
163
|
+
const valueTextRef = useRef(null);
|
|
164
|
+
const valueEditMeasureRef = useRef(null);
|
|
165
|
+
const valueRef = useRef(null);
|
|
166
|
+
const [valueEditWidthPx, setValueEditWidthPx] = useState(null);
|
|
167
|
+
const [overlapLayout, setOverlapLayout] = useState(null);
|
|
168
|
+
const [idleThumbOpacity, setIdleThumbOpacity] = useState(0);
|
|
169
|
+
const lastPointerXRef = useRef(0);
|
|
170
|
+
const dragStateRef = useRef({ value, min, max });
|
|
171
|
+
dragStateRef.current = { value, min, max };
|
|
172
|
+
const targetStretchL = useMotionValue(0);
|
|
173
|
+
const targetStretchR = useMotionValue(0);
|
|
174
|
+
const stretchL = useSpring(targetStretchL, stretchSpringOpts);
|
|
175
|
+
const stretchR = useSpring(targetStretchR, stretchSpringOpts);
|
|
176
|
+
const pullL = useMotionValue(0);
|
|
177
|
+
const pullR = useMotionValue(0);
|
|
178
|
+
const pullLSpring = useSpring(pullL, pullSpringOpts);
|
|
179
|
+
const pullRSpring = useSpring(pullR, pullSpringOpts);
|
|
180
|
+
const baseW = useMotionValue(0);
|
|
181
|
+
const cardWidth = useTransform(
|
|
182
|
+
[stretchL, stretchR, pullLSpring, pullRSpring, baseW],
|
|
183
|
+
([l, r, pl, pr, b]) => {
|
|
184
|
+
const base = Number(b);
|
|
185
|
+
let extra = Number(l) + Number(r) + Number(pl) + Number(pr);
|
|
186
|
+
if (Math.abs(extra) < STRETCH_SNAP_EPS_PX) extra = 0;
|
|
187
|
+
return Math.max(1, Math.round(base + extra));
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
const cardMarginLeft = useTransform([stretchL, pullLSpring], ([l, pl]) => {
|
|
191
|
+
let leftExtra = Number(l) + Number(pl);
|
|
192
|
+
if (Math.abs(leftExtra) < STRETCH_SNAP_EPS_PX) leftExtra = 0;
|
|
193
|
+
return Math.round(-leftExtra);
|
|
194
|
+
});
|
|
195
|
+
const syncBaseWidthFromParent = () => {
|
|
196
|
+
const wrap = wrapRef.current;
|
|
197
|
+
const parent = wrap == null ? void 0 : wrap.parentElement;
|
|
198
|
+
if (!parent) return;
|
|
199
|
+
baseW.set(parent.offsetWidth);
|
|
200
|
+
};
|
|
201
|
+
useLayoutEffect(() => {
|
|
202
|
+
const wrap = wrapRef.current;
|
|
203
|
+
const parent = wrap == null ? void 0 : wrap.parentElement;
|
|
204
|
+
if (!parent) return;
|
|
205
|
+
syncBaseWidthFromParent();
|
|
206
|
+
const ro = new ResizeObserver(() => {
|
|
207
|
+
syncBaseWidthFromParent();
|
|
208
|
+
});
|
|
209
|
+
ro.observe(parent);
|
|
210
|
+
return () => ro.disconnect();
|
|
211
|
+
}, [baseW]);
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
setMounted(true);
|
|
214
|
+
}, []);
|
|
215
|
+
useLayoutEffect(() => {
|
|
216
|
+
if (theme !== "light") {
|
|
217
|
+
setIdleThumbOpacity(0);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const raw = getComputedStyle(document.documentElement).getPropertyValue("--slider-thumb-idle-opacity").trim();
|
|
221
|
+
const n = parseFloat(raw);
|
|
222
|
+
setIdleThumbOpacity(Number.isFinite(n) ? n : 0.4);
|
|
223
|
+
}, [theme]);
|
|
224
|
+
const display = fmt ? fmt(value) : value;
|
|
225
|
+
useLayoutEffect(() => {
|
|
226
|
+
const card = cardRef.current;
|
|
227
|
+
const labelEl = labelTextRef.current;
|
|
228
|
+
const valueEl = editing ? inputRef.current : valueTextRef.current;
|
|
229
|
+
if (!mounted || !card || !labelEl || !valueEl) {
|
|
230
|
+
setOverlapLayout(null);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const measure = () => {
|
|
234
|
+
const c = cardRef.current;
|
|
235
|
+
const le = labelTextRef.current;
|
|
236
|
+
const ve = editing ? inputRef.current : valueTextRef.current;
|
|
237
|
+
if (!c || !le || !ve) {
|
|
238
|
+
setOverlapLayout(null);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const cardRect = c.getBoundingClientRect();
|
|
242
|
+
const lr = le.getBoundingClientRect();
|
|
243
|
+
const vr = ve.getBoundingClientRect();
|
|
244
|
+
const w = cardRect.width;
|
|
245
|
+
let labelEnd = lr.right - cardRect.left + THUMB_OVERLAP_PAD_PX;
|
|
246
|
+
let valueStart = vr.left - cardRect.left - THUMB_OVERLAP_PAD_PX;
|
|
247
|
+
labelEnd = Math.min(Math.max(0, labelEnd), w);
|
|
248
|
+
valueStart = Math.min(Math.max(0, valueStart), w);
|
|
249
|
+
if (valueStart < labelEnd) valueStart = labelEnd;
|
|
250
|
+
setOverlapLayout((prev) => {
|
|
251
|
+
if (prev && prev.labelEnd === labelEnd && prev.valueStart === valueStart) {
|
|
252
|
+
return prev;
|
|
253
|
+
}
|
|
254
|
+
return { labelEnd, valueStart };
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
measure();
|
|
258
|
+
const ro = new ResizeObserver(measure);
|
|
259
|
+
ro.observe(card);
|
|
260
|
+
return () => ro.disconnect();
|
|
261
|
+
}, [mounted, label, display, editing, draft, value]);
|
|
262
|
+
useLayoutEffect(() => {
|
|
263
|
+
if (!editing) {
|
|
264
|
+
setValueEditWidthPx(null);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const measure = () => {
|
|
268
|
+
const m = valueEditMeasureRef.current;
|
|
269
|
+
const zone = valueRef.current;
|
|
270
|
+
if (!m || !zone) return;
|
|
271
|
+
const w = m.getBoundingClientRect().width;
|
|
272
|
+
const cs = getComputedStyle(zone);
|
|
273
|
+
const padL = parseFloat(cs.paddingLeft) || 0;
|
|
274
|
+
const padR = parseFloat(cs.paddingRight) || 0;
|
|
275
|
+
const maxInner = Math.max(0, zone.clientWidth - padL - padR);
|
|
276
|
+
const next = Math.min(
|
|
277
|
+
Math.max(Math.ceil(w + VALUE_EDIT_WIDTH_CARET_PAD_PX), 22),
|
|
278
|
+
Math.max(maxInner, 22)
|
|
279
|
+
);
|
|
280
|
+
setValueEditWidthPx((prev) => prev === next ? prev : next);
|
|
281
|
+
};
|
|
282
|
+
measure();
|
|
283
|
+
const ro = new ResizeObserver(measure);
|
|
284
|
+
const z = valueRef.current;
|
|
285
|
+
if (z) ro.observe(z);
|
|
286
|
+
return () => ro.disconnect();
|
|
287
|
+
}, [editing, draft]);
|
|
288
|
+
const span = max - min;
|
|
289
|
+
const u = span > 0 ? (value - min) / span : 0;
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
if (span <= 0 || !Number.isFinite(u) || !dragging) {
|
|
292
|
+
targetStretchL.set(0);
|
|
293
|
+
targetStretchR.set(0);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const atEnd = value <= min + 1e-9 || value >= max - 1e-9;
|
|
297
|
+
const dragBoost = atEnd ? AT_END_DRAG_BOOST : 1;
|
|
298
|
+
const { left, right } = edgeStretchPx(u, MAX_EDGE_STRETCH_PX, EDGE_FRACTION, dragBoost);
|
|
299
|
+
targetStretchL.set(left);
|
|
300
|
+
targetStretchR.set(right);
|
|
301
|
+
}, [u, span, dragging, value, min, max, targetStretchL, targetStretchR]);
|
|
302
|
+
const resetPull = () => {
|
|
303
|
+
pullL.set(0);
|
|
304
|
+
pullR.set(0);
|
|
305
|
+
};
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
if (!dragging) return;
|
|
308
|
+
const onMove = (e) => {
|
|
309
|
+
const { value: v, min: lo, max: hi } = dragStateRef.current;
|
|
310
|
+
const sp = hi - lo;
|
|
311
|
+
if (sp <= 0) return;
|
|
312
|
+
const uNow = (v - lo) / sp;
|
|
313
|
+
const dx = e.clientX - lastPointerXRef.current;
|
|
314
|
+
lastPointerXRef.current = e.clientX;
|
|
315
|
+
if (uNow >= 1 - 1e-6 && dx > 0) {
|
|
316
|
+
pullR.set(Math.min(PULL_CAP_PX, Math.max(0, pullR.get() + dx * PULL_GAIN)));
|
|
317
|
+
} else if (uNow <= 1e-6 && dx < 0) {
|
|
318
|
+
pullL.set(Math.min(PULL_CAP_PX, Math.max(0, pullL.get() - dx * PULL_GAIN)));
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const onEnd = () => {
|
|
322
|
+
resetPull();
|
|
323
|
+
requestAnimationFrame(() => {
|
|
324
|
+
requestAnimationFrame(syncBaseWidthFromParent);
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
window.addEventListener("pointermove", onMove);
|
|
328
|
+
window.addEventListener("pointerup", onEnd);
|
|
329
|
+
window.addEventListener("pointercancel", onEnd);
|
|
330
|
+
return () => {
|
|
331
|
+
window.removeEventListener("pointermove", onMove);
|
|
332
|
+
window.removeEventListener("pointerup", onEnd);
|
|
333
|
+
window.removeEventListener("pointercancel", onEnd);
|
|
334
|
+
};
|
|
335
|
+
}, [dragging, pullL, pullR]);
|
|
336
|
+
const pct = u * 100;
|
|
337
|
+
const getThumbLayout = () => {
|
|
338
|
+
if (!mounted || !cardRef.current || !overlapLayout) {
|
|
339
|
+
return {
|
|
340
|
+
opacity: dragging ? 1 : 0.7,
|
|
341
|
+
height: THUMB_HEIGHT_PX,
|
|
342
|
+
debugZones: null
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const cardRect = cardRef.current.getBoundingClientRect();
|
|
346
|
+
const fillEndPx = cardRect.width * pct / 100;
|
|
347
|
+
const thumbRightPx = fillEndPx - THUMB_INSET_FROM_FILL_END_PX;
|
|
348
|
+
const thumbCenterX = thumbRightPx - THUMB_WIDTH_PX / 2;
|
|
349
|
+
const { labelEnd, valueStart } = overlapLayout;
|
|
350
|
+
const behindLabel = thumbCenterX < labelEnd;
|
|
351
|
+
const behindValue = thumbCenterX > valueStart;
|
|
352
|
+
let opacity;
|
|
353
|
+
if (behindLabel || behindValue) {
|
|
354
|
+
opacity = 0.1;
|
|
355
|
+
} else {
|
|
356
|
+
opacity = dragging ? 1 : 0.7;
|
|
357
|
+
}
|
|
358
|
+
const height = behindLabel ? THUMB_HEIGHT_BEHIND_LABEL_PX : THUMB_HEIGHT_PX;
|
|
359
|
+
let debugZones2 = null;
|
|
360
|
+
if (debugOverlap && labelTextRef.current && (editing ? inputRef.current : valueTextRef.current)) {
|
|
361
|
+
const cr = cardRef.current.getBoundingClientRect();
|
|
362
|
+
const lr = labelTextRef.current.getBoundingClientRect();
|
|
363
|
+
const ve = editing ? inputRef.current : valueTextRef.current;
|
|
364
|
+
const vr = ve.getBoundingClientRect();
|
|
365
|
+
debugZones2 = {
|
|
366
|
+
overlapLabelEnd: labelEnd,
|
|
367
|
+
overlapValueStart: valueStart,
|
|
368
|
+
domLabelText: {
|
|
369
|
+
left: lr.left - cr.left,
|
|
370
|
+
top: lr.top - cr.top,
|
|
371
|
+
width: lr.width,
|
|
372
|
+
height: lr.height
|
|
373
|
+
},
|
|
374
|
+
domValueText: {
|
|
375
|
+
left: vr.left - cr.left,
|
|
376
|
+
top: vr.top - cr.top,
|
|
377
|
+
width: vr.width,
|
|
378
|
+
height: vr.height
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return { opacity, height, debugZones: debugZones2 };
|
|
383
|
+
};
|
|
384
|
+
const { opacity: thumbOpacity, height: thumbHeight, debugZones } = getThumbLayout();
|
|
385
|
+
const startEdit = (e) => {
|
|
386
|
+
e.stopPropagation();
|
|
387
|
+
setDraft(String(value));
|
|
388
|
+
setEditing(true);
|
|
389
|
+
setTimeout(() => {
|
|
390
|
+
var _a;
|
|
391
|
+
return (_a = inputRef.current) == null ? void 0 : _a.select();
|
|
392
|
+
}, 0);
|
|
393
|
+
};
|
|
394
|
+
const commitEdit = () => {
|
|
395
|
+
const parsed = parseFloat(draft);
|
|
396
|
+
if (!Number.isNaN(parsed)) {
|
|
397
|
+
onChange(Math.max(min, Math.min(max, parsed)));
|
|
398
|
+
}
|
|
399
|
+
setEditing(false);
|
|
400
|
+
};
|
|
401
|
+
const handleKeyDown = (e) => {
|
|
402
|
+
if (e.key === "Enter") {
|
|
403
|
+
e.preventDefault();
|
|
404
|
+
commitEdit();
|
|
405
|
+
}
|
|
406
|
+
if (e.key === "Escape") {
|
|
407
|
+
setEditing(false);
|
|
408
|
+
}
|
|
409
|
+
e.stopPropagation();
|
|
410
|
+
};
|
|
411
|
+
const isActive = hovered || dragging;
|
|
412
|
+
const idleThumbWidth = theme === "light" ? THUMB_WIDTH_PX : 0;
|
|
413
|
+
const handleRangePointerDown = (e) => {
|
|
414
|
+
clearDocumentSelection();
|
|
415
|
+
lastPointerXRef.current = e.clientX;
|
|
416
|
+
syncBaseWidthFromParent();
|
|
417
|
+
try {
|
|
418
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
setDragging(true);
|
|
422
|
+
};
|
|
423
|
+
const handleRangePointerUp = (e) => {
|
|
424
|
+
var _a, _b;
|
|
425
|
+
try {
|
|
426
|
+
if ((_b = (_a = e.currentTarget).hasPointerCapture) == null ? void 0 : _b.call(_a, e.pointerId)) {
|
|
427
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
targetStretchL.set(0);
|
|
432
|
+
targetStretchR.set(0);
|
|
433
|
+
resetPull();
|
|
434
|
+
setDragging(false);
|
|
435
|
+
requestAnimationFrame(() => {
|
|
436
|
+
requestAnimationFrame(syncBaseWidthFromParent);
|
|
437
|
+
});
|
|
438
|
+
};
|
|
439
|
+
return /* @__PURE__ */ jsx(
|
|
440
|
+
"div",
|
|
441
|
+
{
|
|
442
|
+
className: "sliderCardWrap",
|
|
443
|
+
ref: wrapRef,
|
|
444
|
+
onPointerEnter: () => setHovered(true),
|
|
445
|
+
onPointerLeave: () => setHovered(false),
|
|
446
|
+
children: /* @__PURE__ */ jsxs(
|
|
447
|
+
motion.div,
|
|
448
|
+
{
|
|
449
|
+
ref: cardRef,
|
|
450
|
+
className: "sliderCard",
|
|
451
|
+
"data-active": isActive ? "true" : "false",
|
|
452
|
+
style: { width: cardWidth, marginLeft: cardMarginLeft },
|
|
453
|
+
draggable: false,
|
|
454
|
+
onDragStart: (e) => e.preventDefault(),
|
|
455
|
+
children: [
|
|
456
|
+
/* @__PURE__ */ jsx(
|
|
457
|
+
motion.div,
|
|
458
|
+
{
|
|
459
|
+
className: "sliderTrack",
|
|
460
|
+
"data-active": isActive ? "true" : "false",
|
|
461
|
+
style: { width: `${pct}%` }
|
|
462
|
+
}
|
|
463
|
+
),
|
|
464
|
+
debugZones ? /* @__PURE__ */ jsxs("div", { className: "sliderDebugOverlap", "aria-hidden": true, children: [
|
|
465
|
+
/* @__PURE__ */ jsx(
|
|
466
|
+
"div",
|
|
467
|
+
{
|
|
468
|
+
className: "sliderDebugOverlap__heuristic sliderDebugOverlap__heuristic--label",
|
|
469
|
+
style: { width: debugZones.overlapLabelEnd },
|
|
470
|
+
title: "Label overlap threshold (text right + pad)"
|
|
471
|
+
}
|
|
472
|
+
),
|
|
473
|
+
/* @__PURE__ */ jsx(
|
|
474
|
+
"div",
|
|
475
|
+
{
|
|
476
|
+
className: "sliderDebugOverlap__heuristic sliderDebugOverlap__heuristic--value",
|
|
477
|
+
style: { left: debugZones.overlapValueStart },
|
|
478
|
+
title: "Value overlap threshold (text left − pad)"
|
|
479
|
+
}
|
|
480
|
+
),
|
|
481
|
+
/* @__PURE__ */ jsx(
|
|
482
|
+
"div",
|
|
483
|
+
{
|
|
484
|
+
className: "sliderDebugOverlap__dom sliderDebugOverlap__dom--label",
|
|
485
|
+
style: {
|
|
486
|
+
left: debugZones.domLabelText.left,
|
|
487
|
+
top: debugZones.domLabelText.top,
|
|
488
|
+
width: debugZones.domLabelText.width,
|
|
489
|
+
height: debugZones.domLabelText.height
|
|
490
|
+
},
|
|
491
|
+
title: "Measured label text box"
|
|
492
|
+
}
|
|
493
|
+
),
|
|
494
|
+
/* @__PURE__ */ jsx(
|
|
495
|
+
"div",
|
|
496
|
+
{
|
|
497
|
+
className: "sliderDebugOverlap__dom sliderDebugOverlap__dom--value",
|
|
498
|
+
style: {
|
|
499
|
+
left: debugZones.domValueText.left,
|
|
500
|
+
top: debugZones.domValueText.top,
|
|
501
|
+
width: debugZones.domValueText.width,
|
|
502
|
+
height: debugZones.domValueText.height
|
|
503
|
+
},
|
|
504
|
+
title: "Measured value text / input box"
|
|
505
|
+
}
|
|
506
|
+
)
|
|
507
|
+
] }) : null,
|
|
508
|
+
/* @__PURE__ */ jsx(
|
|
509
|
+
motion.div,
|
|
510
|
+
{
|
|
511
|
+
className: "sliderThumb",
|
|
512
|
+
"data-dragging": dragging ? "true" : "false",
|
|
513
|
+
style: {
|
|
514
|
+
left: `calc(${pct}% - ${THUMB_INSET_FROM_FILL_END_PX}px)`
|
|
515
|
+
},
|
|
516
|
+
animate: {
|
|
517
|
+
width: isActive ? THUMB_WIDTH_PX : idleThumbWidth,
|
|
518
|
+
height: isActive ? thumbHeight : THUMB_HEIGHT_PX,
|
|
519
|
+
opacity: isActive ? thumbOpacity : idleThumbOpacity
|
|
520
|
+
},
|
|
521
|
+
transition: { duration: 0.15, ease: "easeOut" }
|
|
522
|
+
}
|
|
523
|
+
),
|
|
524
|
+
/* @__PURE__ */ jsx(
|
|
525
|
+
motion.div,
|
|
526
|
+
{
|
|
527
|
+
className: "sliderTicks",
|
|
528
|
+
animate: { opacity: isActive ? 1 : 0 },
|
|
529
|
+
transition: { duration: 0.15, ease: "easeOut" },
|
|
530
|
+
children: [...Array(10)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "sliderTick" }, i))
|
|
531
|
+
}
|
|
532
|
+
),
|
|
533
|
+
/* @__PURE__ */ jsx(
|
|
534
|
+
"input",
|
|
535
|
+
{
|
|
536
|
+
className: "sliderInput",
|
|
537
|
+
type: "range",
|
|
538
|
+
min,
|
|
539
|
+
max,
|
|
540
|
+
step,
|
|
541
|
+
value,
|
|
542
|
+
draggable: false,
|
|
543
|
+
onChange: (e) => onChange(parseFloat(e.target.value)),
|
|
544
|
+
onPointerDown: handleRangePointerDown,
|
|
545
|
+
onPointerUp: handleRangePointerUp,
|
|
546
|
+
onPointerCancel: () => {
|
|
547
|
+
targetStretchL.set(0);
|
|
548
|
+
targetStretchR.set(0);
|
|
549
|
+
resetPull();
|
|
550
|
+
setDragging(false);
|
|
551
|
+
requestAnimationFrame(() => {
|
|
552
|
+
requestAnimationFrame(syncBaseWidthFromParent);
|
|
553
|
+
});
|
|
554
|
+
},
|
|
555
|
+
onLostPointerCapture: () => {
|
|
556
|
+
targetStretchL.set(0);
|
|
557
|
+
targetStretchR.set(0);
|
|
558
|
+
resetPull();
|
|
559
|
+
setDragging(false);
|
|
560
|
+
requestAnimationFrame(() => {
|
|
561
|
+
requestAnimationFrame(syncBaseWidthFromParent);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
),
|
|
566
|
+
/* @__PURE__ */ jsx("span", { className: "sliderLabel", draggable: false, children: /* @__PURE__ */ jsx("span", { ref: labelTextRef, className: "sliderLabelText", children: label }) }),
|
|
567
|
+
/* @__PURE__ */ jsx(
|
|
568
|
+
"div",
|
|
569
|
+
{
|
|
570
|
+
ref: valueRef,
|
|
571
|
+
className: "sliderValueZone",
|
|
572
|
+
onClick: (e) => e.stopPropagation(),
|
|
573
|
+
children: editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
574
|
+
/* @__PURE__ */ jsx(
|
|
575
|
+
"span",
|
|
576
|
+
{
|
|
577
|
+
ref: valueEditMeasureRef,
|
|
578
|
+
className: "sliderValue sliderValueText sliderValueMeasure",
|
|
579
|
+
"aria-hidden": true,
|
|
580
|
+
children: draft.length > 0 ? draft : "0"
|
|
581
|
+
}
|
|
582
|
+
),
|
|
583
|
+
/* @__PURE__ */ jsx(
|
|
584
|
+
"input",
|
|
585
|
+
{
|
|
586
|
+
ref: inputRef,
|
|
587
|
+
className: "sliderValueInput",
|
|
588
|
+
type: "text",
|
|
589
|
+
inputMode: min < 0 || stepAllowsDecimal(step) ? "decimal" : "numeric",
|
|
590
|
+
value: draft,
|
|
591
|
+
autoComplete: "off",
|
|
592
|
+
spellCheck: false,
|
|
593
|
+
"aria-valuemin": min,
|
|
594
|
+
"aria-valuemax": max,
|
|
595
|
+
style: valueEditWidthPx != null ? { width: valueEditWidthPx } : { minWidth: 22 },
|
|
596
|
+
onChange: (e) => setDraft(sanitizeValueDraft(e.target.value, min, step)),
|
|
597
|
+
onBlur: commitEdit,
|
|
598
|
+
onKeyDown: handleKeyDown,
|
|
599
|
+
onClick: (e) => e.stopPropagation()
|
|
600
|
+
}
|
|
601
|
+
)
|
|
602
|
+
] }) : /* @__PURE__ */ jsx(
|
|
603
|
+
"span",
|
|
604
|
+
{
|
|
605
|
+
className: "sliderValue",
|
|
606
|
+
draggable: false,
|
|
607
|
+
"data-active": isActive ? "true" : "false",
|
|
608
|
+
onClick: startEdit,
|
|
609
|
+
children: /* @__PURE__ */ jsx("span", { ref: valueTextRef, className: "sliderValueText", children: display })
|
|
610
|
+
}
|
|
611
|
+
)
|
|
612
|
+
}
|
|
613
|
+
)
|
|
614
|
+
]
|
|
615
|
+
}
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
function SwatchPopover({ title, renderTrigger, children }) {
|
|
621
|
+
return /* @__PURE__ */ jsxs(Popover.Root, { modal: true, children: [
|
|
622
|
+
/* @__PURE__ */ jsx(Popover.Trigger, { render: renderTrigger }),
|
|
623
|
+
/* @__PURE__ */ jsx(Popover.Portal, { className: "swatchPopoverPortal", children: /* @__PURE__ */ jsx(
|
|
624
|
+
Popover.Positioner,
|
|
625
|
+
{
|
|
626
|
+
className: "swatchPopoverPositioner",
|
|
627
|
+
positionMethod: "fixed",
|
|
628
|
+
side: "bottom",
|
|
629
|
+
align: "end",
|
|
630
|
+
sideOffset: 6,
|
|
631
|
+
collisionAvoidance: { side: "flip", align: "shift", fallbackAxisSide: "none" },
|
|
632
|
+
children: /* @__PURE__ */ jsxs(
|
|
633
|
+
Popover.Popup,
|
|
634
|
+
{
|
|
635
|
+
className: "swatchPopoverPopup",
|
|
636
|
+
initialFocus: (openType) => openType === "keyboard",
|
|
637
|
+
children: [
|
|
638
|
+
title && /* @__PURE__ */ jsx(Popover.Title, { className: "swatchPopoverTitle", children: title }),
|
|
639
|
+
/* @__PURE__ */ jsx(Popover.Close, { type: "button", className: "swatchPopoverCloseSrOnly", children: "Close" }),
|
|
640
|
+
/* @__PURE__ */ jsx("div", { className: "swatchPopoverBody", children })
|
|
641
|
+
]
|
|
642
|
+
}
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
) })
|
|
646
|
+
] });
|
|
647
|
+
}
|
|
648
|
+
function normalizeHex$1(raw) {
|
|
649
|
+
const s = raw.trim();
|
|
650
|
+
const m = s.match(/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
|
|
651
|
+
if (!m) return "#808080";
|
|
652
|
+
let h = m[1];
|
|
653
|
+
if (h.length === 3) {
|
|
654
|
+
h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
655
|
+
}
|
|
656
|
+
return `#${h.toUpperCase()}`;
|
|
657
|
+
}
|
|
658
|
+
function hexDigits$1(hex) {
|
|
659
|
+
const n = normalizeHex$1(hex);
|
|
660
|
+
return n.slice(1);
|
|
661
|
+
}
|
|
662
|
+
function isValidSixHex$1(d) {
|
|
663
|
+
return /^[0-9a-fA-F]{6}$/.test(d);
|
|
664
|
+
}
|
|
665
|
+
function ColorRow({ label = "Color", value, onChange }) {
|
|
666
|
+
const [hovered, setHovered] = useState(false);
|
|
667
|
+
const [editing, setEditing] = useState(false);
|
|
668
|
+
const [draft, setDraft] = useState("");
|
|
669
|
+
const inputRef = useRef(null);
|
|
670
|
+
const hex = normalizeHex$1(value);
|
|
671
|
+
const isActive = hovered || editing;
|
|
672
|
+
useLayoutEffect(() => {
|
|
673
|
+
var _a, _b;
|
|
674
|
+
if (!editing) return;
|
|
675
|
+
(_a = inputRef.current) == null ? void 0 : _a.focus();
|
|
676
|
+
(_b = inputRef.current) == null ? void 0 : _b.select();
|
|
677
|
+
}, [editing]);
|
|
678
|
+
const startEdit = (e) => {
|
|
679
|
+
e.stopPropagation();
|
|
680
|
+
setDraft(hexDigits$1(value));
|
|
681
|
+
setEditing(true);
|
|
682
|
+
};
|
|
683
|
+
const commit = () => {
|
|
684
|
+
const d = draft.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
|
|
685
|
+
if (isValidSixHex$1(d)) {
|
|
686
|
+
onChange(`#${d.toUpperCase()}`);
|
|
687
|
+
}
|
|
688
|
+
setEditing(false);
|
|
689
|
+
};
|
|
690
|
+
const onHexKeyDown = (e) => {
|
|
691
|
+
if (e.key === "Enter") {
|
|
692
|
+
e.preventDefault();
|
|
693
|
+
commit();
|
|
694
|
+
}
|
|
695
|
+
if (e.key === "Escape") {
|
|
696
|
+
setEditing(false);
|
|
697
|
+
}
|
|
698
|
+
e.stopPropagation();
|
|
699
|
+
};
|
|
700
|
+
const onHexChange = (e) => {
|
|
701
|
+
const next = e.target.value.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
|
|
702
|
+
setDraft(next);
|
|
703
|
+
};
|
|
704
|
+
const onHexPaste = (e) => {
|
|
705
|
+
e.preventDefault();
|
|
706
|
+
const pasted = e.clipboardData.getData("text");
|
|
707
|
+
const cleaned = pasted.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
|
|
708
|
+
setDraft(cleaned);
|
|
709
|
+
};
|
|
710
|
+
const onPickerChange = (e) => {
|
|
711
|
+
onChange(normalizeHex$1(e.target.value));
|
|
712
|
+
};
|
|
713
|
+
return /* @__PURE__ */ jsx(
|
|
714
|
+
"div",
|
|
715
|
+
{
|
|
716
|
+
className: "colorRowWrap",
|
|
717
|
+
onPointerEnter: () => setHovered(true),
|
|
718
|
+
onPointerLeave: () => setHovered(false),
|
|
719
|
+
children: /* @__PURE__ */ jsxs("div", { className: "colorCard", "data-active": isActive ? "true" : "false", children: [
|
|
720
|
+
/* @__PURE__ */ jsx("span", { className: "colorLabel", children: label }),
|
|
721
|
+
/* @__PURE__ */ jsxs("div", { className: "colorValueZone", children: [
|
|
722
|
+
editing ? /* @__PURE__ */ jsxs("span", { className: "colorRowValue", "data-active": isActive ? "true" : "false", children: [
|
|
723
|
+
/* @__PURE__ */ jsx("span", { className: "colorHexHash", children: "#" }),
|
|
724
|
+
/* @__PURE__ */ jsx(
|
|
725
|
+
"input",
|
|
726
|
+
{
|
|
727
|
+
ref: inputRef,
|
|
728
|
+
className: "colorHexInput",
|
|
729
|
+
value: draft,
|
|
730
|
+
onChange: onHexChange,
|
|
731
|
+
onPaste: onHexPaste,
|
|
732
|
+
onBlur: commit,
|
|
733
|
+
onKeyDown: onHexKeyDown,
|
|
734
|
+
onClick: (e) => e.stopPropagation(),
|
|
735
|
+
maxLength: 6,
|
|
736
|
+
inputMode: "text",
|
|
737
|
+
autoComplete: "off",
|
|
738
|
+
spellCheck: false,
|
|
739
|
+
"aria-label": "Hex color without hash"
|
|
740
|
+
}
|
|
741
|
+
)
|
|
742
|
+
] }) : /* @__PURE__ */ jsxs(
|
|
743
|
+
"span",
|
|
744
|
+
{
|
|
745
|
+
className: "colorRowValue",
|
|
746
|
+
"data-active": isActive ? "true" : "false",
|
|
747
|
+
role: "button",
|
|
748
|
+
tabIndex: 0,
|
|
749
|
+
"aria-label": `Edit hex color, ${hex}`,
|
|
750
|
+
onClick: startEdit,
|
|
751
|
+
onKeyDown: (e) => {
|
|
752
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
753
|
+
e.preventDefault();
|
|
754
|
+
setDraft(hexDigits$1(value));
|
|
755
|
+
setEditing(true);
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
children: [
|
|
759
|
+
/* @__PURE__ */ jsx("span", { className: "colorHexHash", children: "#" }),
|
|
760
|
+
/* @__PURE__ */ jsx("span", { className: "colorHexDigits", children: hexDigits$1(hex) })
|
|
761
|
+
]
|
|
762
|
+
}
|
|
763
|
+
),
|
|
764
|
+
/* @__PURE__ */ jsxs(
|
|
765
|
+
SwatchPopover,
|
|
766
|
+
{
|
|
767
|
+
title: label,
|
|
768
|
+
renderTrigger: (props) => /* @__PURE__ */ jsx(
|
|
769
|
+
"button",
|
|
770
|
+
{
|
|
771
|
+
type: "button",
|
|
772
|
+
...props,
|
|
773
|
+
className: ["colorSwatchBtn", props.className].filter(Boolean).join(" "),
|
|
774
|
+
style: { ...props.style, backgroundColor: hex },
|
|
775
|
+
"aria-label": `Open ${label} picker`
|
|
776
|
+
}
|
|
777
|
+
),
|
|
778
|
+
children: [
|
|
779
|
+
/* @__PURE__ */ jsx("p", { children: "Custom color controls will live here. For now, use the system picker below." }),
|
|
780
|
+
/* @__PURE__ */ jsx(
|
|
781
|
+
"input",
|
|
782
|
+
{
|
|
783
|
+
type: "color",
|
|
784
|
+
className: "swatchPopoverNativeColor",
|
|
785
|
+
value: hex,
|
|
786
|
+
onChange: onPickerChange,
|
|
787
|
+
"aria-label": "Native color picker"
|
|
788
|
+
}
|
|
789
|
+
)
|
|
790
|
+
]
|
|
791
|
+
}
|
|
792
|
+
)
|
|
793
|
+
] })
|
|
794
|
+
] })
|
|
795
|
+
}
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
function ToggleRow({ label, checked, onChange }) {
|
|
799
|
+
const toggle = () => onChange(!checked);
|
|
800
|
+
const onKeyDown = (e) => {
|
|
801
|
+
if (e.key === " " || e.key === "Enter") {
|
|
802
|
+
e.preventDefault();
|
|
803
|
+
toggle();
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
return /* @__PURE__ */ jsx("div", { className: "toggleRowWrap", children: /* @__PURE__ */ jsxs(
|
|
807
|
+
"div",
|
|
808
|
+
{
|
|
809
|
+
role: "switch",
|
|
810
|
+
tabIndex: 0,
|
|
811
|
+
"aria-checked": checked,
|
|
812
|
+
"aria-label": label,
|
|
813
|
+
className: "toggleCard",
|
|
814
|
+
"data-on": checked ? "true" : "false",
|
|
815
|
+
onClick: toggle,
|
|
816
|
+
onKeyDown,
|
|
817
|
+
children: [
|
|
818
|
+
/* @__PURE__ */ jsx("span", { className: "toggleLabel", children: label }),
|
|
819
|
+
/* @__PURE__ */ jsx("div", { className: "toggleControl", "aria-hidden": true, children: /* @__PURE__ */ jsx("div", { className: "toggleSwitch", children: /* @__PURE__ */ jsx("div", { className: "toggleSwitch__track", children: /* @__PURE__ */ jsx("div", { className: "toggleSwitch__thumb" }) }) }) })
|
|
820
|
+
]
|
|
821
|
+
}
|
|
822
|
+
) });
|
|
823
|
+
}
|
|
824
|
+
function normalizeHex(raw) {
|
|
825
|
+
const s = raw.trim();
|
|
826
|
+
const m = s.match(/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
|
|
827
|
+
if (!m) return "#808080";
|
|
828
|
+
let h = m[1];
|
|
829
|
+
if (h.length === 3) {
|
|
830
|
+
h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
831
|
+
}
|
|
832
|
+
return `#${h.toUpperCase()}`;
|
|
833
|
+
}
|
|
834
|
+
function hslToHex(h, s, l) {
|
|
835
|
+
const hue = (h % 360 + 360) % 360;
|
|
836
|
+
const sat = Math.max(0, Math.min(1, s));
|
|
837
|
+
const lit = Math.max(0, Math.min(1, l));
|
|
838
|
+
const c = (1 - Math.abs(2 * lit - 1)) * sat;
|
|
839
|
+
const x = c * (1 - Math.abs(hue / 60 % 2 - 1));
|
|
840
|
+
const m = lit - c / 2;
|
|
841
|
+
let r = 0, g = 0, b = 0;
|
|
842
|
+
if (hue < 60) {
|
|
843
|
+
r = c;
|
|
844
|
+
g = x;
|
|
845
|
+
b = 0;
|
|
846
|
+
} else if (hue < 120) {
|
|
847
|
+
r = x;
|
|
848
|
+
g = c;
|
|
849
|
+
b = 0;
|
|
850
|
+
} else if (hue < 180) {
|
|
851
|
+
r = 0;
|
|
852
|
+
g = c;
|
|
853
|
+
b = x;
|
|
854
|
+
} else if (hue < 240) {
|
|
855
|
+
r = 0;
|
|
856
|
+
g = x;
|
|
857
|
+
b = c;
|
|
858
|
+
} else if (hue < 300) {
|
|
859
|
+
r = x;
|
|
860
|
+
g = 0;
|
|
861
|
+
b = c;
|
|
862
|
+
} else {
|
|
863
|
+
r = c;
|
|
864
|
+
g = 0;
|
|
865
|
+
b = x;
|
|
866
|
+
}
|
|
867
|
+
const toHex = (v) => Math.round((v + m) * 255).toString(16).padStart(2, "0");
|
|
868
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
|
|
869
|
+
}
|
|
870
|
+
function generateAestheticGradient(stopCount) {
|
|
871
|
+
const baseHue = Math.random() * 360;
|
|
872
|
+
const hueShift = 25 + Math.random() * 35;
|
|
873
|
+
const hueDirection = Math.random() > 0.5 ? 1 : -1;
|
|
874
|
+
const stops = [];
|
|
875
|
+
for (let i = 0; i < stopCount; i++) {
|
|
876
|
+
const t = i / (stopCount - 1);
|
|
877
|
+
const hue = baseHue + hueDirection * hueShift * t;
|
|
878
|
+
const satBase = 0.35 + Math.random() * 0.15;
|
|
879
|
+
const satPeak = 0.65 + Math.random() * 0.25;
|
|
880
|
+
const saturation = satBase + (satPeak - satBase) * Math.sin(t * Math.PI * 0.8);
|
|
881
|
+
const lightStart = 0.75 + Math.random() * 0.1;
|
|
882
|
+
const lightEnd = 0.08 + Math.random() * 0.07;
|
|
883
|
+
const eased = t * t * (3 - 2 * t);
|
|
884
|
+
const lightness = lightStart + (lightEnd - lightStart) * eased;
|
|
885
|
+
stops.push({ color: hslToHex(hue, saturation, lightness) });
|
|
886
|
+
}
|
|
887
|
+
return stops;
|
|
888
|
+
}
|
|
889
|
+
function hexDigits(hex) {
|
|
890
|
+
return normalizeHex(hex).slice(1);
|
|
891
|
+
}
|
|
892
|
+
function isValidSixHex(d) {
|
|
893
|
+
return /^[0-9a-fA-F]{6}$/.test(d);
|
|
894
|
+
}
|
|
895
|
+
function stopsToGradient(stops, angle = 90) {
|
|
896
|
+
if (stops.length === 0) return "linear-gradient(90deg, #808080, #808080)";
|
|
897
|
+
if (stops.length === 1) {
|
|
898
|
+
const c = normalizeHex(stops[0].color);
|
|
899
|
+
return `linear-gradient(${angle}deg, ${c}, ${c})`;
|
|
900
|
+
}
|
|
901
|
+
const colorStops = stops.map((s, i) => {
|
|
902
|
+
const pos = Math.round(i / (stops.length - 1) * 100);
|
|
903
|
+
return `${normalizeHex(s.color)} ${pos}%`;
|
|
904
|
+
});
|
|
905
|
+
return `linear-gradient(${angle}deg, ${colorStops.join(", ")})`;
|
|
906
|
+
}
|
|
907
|
+
function parseGradient(gradient) {
|
|
908
|
+
const match = gradient.match(/linear-gradient\s*\([^,]+,\s*(.+)\)/);
|
|
909
|
+
if (!match) return [{ color: "#FF0000" }, { color: "#0000FF" }];
|
|
910
|
+
const colorPart = match[1];
|
|
911
|
+
const parts = colorPart.split(/,\s*/);
|
|
912
|
+
const stops = [];
|
|
913
|
+
for (const part of parts) {
|
|
914
|
+
const hexMatch = part.match(/#[0-9a-fA-F]{3,6}/);
|
|
915
|
+
if (hexMatch) {
|
|
916
|
+
stops.push({ color: normalizeHex(hexMatch[0]) });
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return stops.length >= 2 ? stops : [{ color: "#FF0000" }, { color: "#0000FF" }];
|
|
920
|
+
}
|
|
921
|
+
const DEFAULT_GRADIENT_STOPS = [
|
|
922
|
+
{ color: "#0E171B" },
|
|
923
|
+
{ color: "#00354F" },
|
|
924
|
+
{ color: "#005F7A" },
|
|
925
|
+
{ color: "#009A97" },
|
|
926
|
+
{ color: "#42C0B0" },
|
|
927
|
+
{ color: "#BAC9C7" }
|
|
928
|
+
];
|
|
929
|
+
function StopRow({ index, stop, canDelete, onColorChange, onDelete }) {
|
|
930
|
+
const [editing, setEditing] = useState(false);
|
|
931
|
+
const [draft, setDraft] = useState("");
|
|
932
|
+
const inputRef = useRef(null);
|
|
933
|
+
const hex = normalizeHex(stop.color);
|
|
934
|
+
useLayoutEffect(() => {
|
|
935
|
+
var _a, _b;
|
|
936
|
+
if (!editing) return;
|
|
937
|
+
(_a = inputRef.current) == null ? void 0 : _a.focus();
|
|
938
|
+
(_b = inputRef.current) == null ? void 0 : _b.select();
|
|
939
|
+
}, [editing]);
|
|
940
|
+
const startEdit = (e) => {
|
|
941
|
+
e.stopPropagation();
|
|
942
|
+
setDraft(hexDigits(stop.color));
|
|
943
|
+
setEditing(true);
|
|
944
|
+
};
|
|
945
|
+
const commit = () => {
|
|
946
|
+
const d = draft.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
|
|
947
|
+
if (isValidSixHex(d)) {
|
|
948
|
+
onColorChange(`#${d.toUpperCase()}`);
|
|
949
|
+
}
|
|
950
|
+
setEditing(false);
|
|
951
|
+
};
|
|
952
|
+
const onHexKeyDown = (e) => {
|
|
953
|
+
if (e.key === "Enter") {
|
|
954
|
+
e.preventDefault();
|
|
955
|
+
commit();
|
|
956
|
+
}
|
|
957
|
+
if (e.key === "Escape") {
|
|
958
|
+
setEditing(false);
|
|
959
|
+
}
|
|
960
|
+
e.stopPropagation();
|
|
961
|
+
};
|
|
962
|
+
const onHexChange = (e) => {
|
|
963
|
+
const next = e.target.value.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
|
|
964
|
+
setDraft(next);
|
|
965
|
+
};
|
|
966
|
+
const onHexPaste = (e) => {
|
|
967
|
+
e.preventDefault();
|
|
968
|
+
const pasted = e.clipboardData.getData("text");
|
|
969
|
+
const cleaned = pasted.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
|
|
970
|
+
setDraft(cleaned);
|
|
971
|
+
};
|
|
972
|
+
return /* @__PURE__ */ jsxs("div", { className: "gradientPickerStop", children: [
|
|
973
|
+
/* @__PURE__ */ jsxs("div", { className: "gradientPickerStopColorHex", children: [
|
|
974
|
+
/* @__PURE__ */ jsx(
|
|
975
|
+
"button",
|
|
976
|
+
{
|
|
977
|
+
type: "button",
|
|
978
|
+
className: "gradientPickerStopSwatch",
|
|
979
|
+
style: { backgroundColor: hex },
|
|
980
|
+
onClick: startEdit,
|
|
981
|
+
"aria-label": `Edit color for stop ${index + 1}`
|
|
982
|
+
}
|
|
983
|
+
),
|
|
984
|
+
editing ? /* @__PURE__ */ jsxs("span", { className: "gradientPickerStopHexWrap", children: [
|
|
985
|
+
/* @__PURE__ */ jsx("span", { className: "gradientPickerStopHexHash", children: "#" }),
|
|
986
|
+
/* @__PURE__ */ jsx(
|
|
987
|
+
"input",
|
|
988
|
+
{
|
|
989
|
+
ref: inputRef,
|
|
990
|
+
className: "gradientPickerStopHexInput",
|
|
991
|
+
value: draft,
|
|
992
|
+
onChange: onHexChange,
|
|
993
|
+
onPaste: onHexPaste,
|
|
994
|
+
onBlur: commit,
|
|
995
|
+
onKeyDown: onHexKeyDown,
|
|
996
|
+
onClick: (e) => e.stopPropagation(),
|
|
997
|
+
maxLength: 6,
|
|
998
|
+
inputMode: "text",
|
|
999
|
+
autoComplete: "off",
|
|
1000
|
+
spellCheck: false,
|
|
1001
|
+
"aria-label": "Hex color without hash"
|
|
1002
|
+
}
|
|
1003
|
+
)
|
|
1004
|
+
] }) : /* @__PURE__ */ jsxs(
|
|
1005
|
+
"span",
|
|
1006
|
+
{
|
|
1007
|
+
className: "gradientPickerStopHex",
|
|
1008
|
+
role: "button",
|
|
1009
|
+
tabIndex: 0,
|
|
1010
|
+
onClick: startEdit,
|
|
1011
|
+
onKeyDown: (e) => {
|
|
1012
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1013
|
+
e.preventDefault();
|
|
1014
|
+
setDraft(hexDigits(stop.color));
|
|
1015
|
+
setEditing(true);
|
|
1016
|
+
}
|
|
1017
|
+
},
|
|
1018
|
+
children: [
|
|
1019
|
+
/* @__PURE__ */ jsx("span", { className: "gradientPickerStopHexHash", children: "#" }),
|
|
1020
|
+
/* @__PURE__ */ jsx("span", { className: "gradientPickerStopHexDigits", children: hexDigits(hex) })
|
|
1021
|
+
]
|
|
1022
|
+
}
|
|
1023
|
+
)
|
|
1024
|
+
] }),
|
|
1025
|
+
/* @__PURE__ */ jsx(
|
|
1026
|
+
"button",
|
|
1027
|
+
{
|
|
1028
|
+
type: "button",
|
|
1029
|
+
className: "gradientPickerStopDelete",
|
|
1030
|
+
onClick: onDelete,
|
|
1031
|
+
disabled: !canDelete,
|
|
1032
|
+
"aria-label": `Remove stop ${index + 1}`,
|
|
1033
|
+
children: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", "aria-hidden": "true", children: /* @__PURE__ */ jsx("use", { href: "/icons.svg#minus" }) })
|
|
1034
|
+
}
|
|
1035
|
+
)
|
|
1036
|
+
] });
|
|
1037
|
+
}
|
|
1038
|
+
function GradientPicker({ stops, onChange }) {
|
|
1039
|
+
const handleInvert = () => {
|
|
1040
|
+
onChange([...stops].reverse());
|
|
1041
|
+
};
|
|
1042
|
+
const handleAddStop = () => {
|
|
1043
|
+
let newColor = "#808080";
|
|
1044
|
+
if (stops.length >= 2) {
|
|
1045
|
+
const midIdx = Math.floor(stops.length / 2);
|
|
1046
|
+
newColor = stops[midIdx].color;
|
|
1047
|
+
} else if (stops.length === 1) {
|
|
1048
|
+
newColor = stops[0].color;
|
|
1049
|
+
}
|
|
1050
|
+
onChange([...stops, { color: newColor }]);
|
|
1051
|
+
};
|
|
1052
|
+
const handleShuffle = () => {
|
|
1053
|
+
onChange(generateAestheticGradient(stops.length));
|
|
1054
|
+
};
|
|
1055
|
+
const handleDeleteStop = (index) => {
|
|
1056
|
+
if (stops.length <= 2) return;
|
|
1057
|
+
onChange(stops.filter((_, i) => i !== index));
|
|
1058
|
+
};
|
|
1059
|
+
const handleColorChange = (index, color) => {
|
|
1060
|
+
const newStops = stops.map((s, i) => i === index ? { ...s, color } : s);
|
|
1061
|
+
onChange(newStops);
|
|
1062
|
+
};
|
|
1063
|
+
return /* @__PURE__ */ jsxs("div", { className: "gradientPicker", children: [
|
|
1064
|
+
/* @__PURE__ */ jsxs("div", { className: "gradientPickerHeader", children: [
|
|
1065
|
+
/* @__PURE__ */ jsxs("div", { className: "gradientPickerHeaderIconsLeft", children: [
|
|
1066
|
+
/* @__PURE__ */ jsx(
|
|
1067
|
+
"button",
|
|
1068
|
+
{
|
|
1069
|
+
type: "button",
|
|
1070
|
+
className: "gradientPickerIconBtn",
|
|
1071
|
+
onClick: handleShuffle,
|
|
1072
|
+
"aria-label": "Randomize gradient",
|
|
1073
|
+
title: "Randomize gradient",
|
|
1074
|
+
children: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", "aria-hidden": "true", children: /* @__PURE__ */ jsx("use", { href: "/icons.svg#shuffle-sparkle" }) })
|
|
1075
|
+
}
|
|
1076
|
+
),
|
|
1077
|
+
/* @__PURE__ */ jsx(
|
|
1078
|
+
"button",
|
|
1079
|
+
{
|
|
1080
|
+
type: "button",
|
|
1081
|
+
className: "gradientPickerIconBtn",
|
|
1082
|
+
onClick: handleInvert,
|
|
1083
|
+
"aria-label": "Invert gradient",
|
|
1084
|
+
title: "Invert gradient",
|
|
1085
|
+
children: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", "aria-hidden": "true", children: /* @__PURE__ */ jsx("use", { href: "/icons.svg#dark-mode" }) })
|
|
1086
|
+
}
|
|
1087
|
+
)
|
|
1088
|
+
] }),
|
|
1089
|
+
/* @__PURE__ */ jsx(
|
|
1090
|
+
"button",
|
|
1091
|
+
{
|
|
1092
|
+
type: "button",
|
|
1093
|
+
className: "gradientPickerIconBtn",
|
|
1094
|
+
onClick: handleAddStop,
|
|
1095
|
+
"aria-label": "Add color stop",
|
|
1096
|
+
title: "Add color stop",
|
|
1097
|
+
children: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", "aria-hidden": "true", children: /* @__PURE__ */ jsx("use", { href: "/icons.svg#plus" }) })
|
|
1098
|
+
}
|
|
1099
|
+
)
|
|
1100
|
+
] }),
|
|
1101
|
+
/* @__PURE__ */ jsx("div", { className: "gradientPickerStopsList", children: stops.map((stop, i) => /* @__PURE__ */ jsx(
|
|
1102
|
+
StopRow,
|
|
1103
|
+
{
|
|
1104
|
+
index: i,
|
|
1105
|
+
stop,
|
|
1106
|
+
canDelete: stops.length > 2,
|
|
1107
|
+
onColorChange: (color) => handleColorChange(i, color),
|
|
1108
|
+
onDelete: () => handleDeleteStop(i)
|
|
1109
|
+
},
|
|
1110
|
+
i
|
|
1111
|
+
)) })
|
|
1112
|
+
] });
|
|
1113
|
+
}
|
|
1114
|
+
function GradientRow({ label = "Gradient", initialStops, onChange, angle = 90 }) {
|
|
1115
|
+
const [hovered, setHovered] = useState(false);
|
|
1116
|
+
const isActive = hovered;
|
|
1117
|
+
const [stops, setStops] = useState(() => initialStops ?? DEFAULT_GRADIENT_STOPS);
|
|
1118
|
+
const handleStopsChange = (newStops) => {
|
|
1119
|
+
setStops(newStops);
|
|
1120
|
+
if (onChange) {
|
|
1121
|
+
onChange(stopsToGradient(newStops, angle));
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
const currentGradient = stopsToGradient(stops, angle);
|
|
1125
|
+
return /* @__PURE__ */ jsx(
|
|
1126
|
+
"div",
|
|
1127
|
+
{
|
|
1128
|
+
className: "gradientRowWrap",
|
|
1129
|
+
onPointerEnter: () => setHovered(true),
|
|
1130
|
+
onPointerLeave: () => setHovered(false),
|
|
1131
|
+
children: /* @__PURE__ */ jsxs("div", { className: "gradientCard", "data-active": isActive ? "true" : "false", children: [
|
|
1132
|
+
/* @__PURE__ */ jsx("span", { className: "gradientLabel", children: label }),
|
|
1133
|
+
/* @__PURE__ */ jsx("div", { className: "gradientSwatchZone", children: /* @__PURE__ */ jsx(
|
|
1134
|
+
SwatchPopover,
|
|
1135
|
+
{
|
|
1136
|
+
renderTrigger: (props) => /* @__PURE__ */ jsx(
|
|
1137
|
+
"button",
|
|
1138
|
+
{
|
|
1139
|
+
type: "button",
|
|
1140
|
+
...props,
|
|
1141
|
+
className: ["gradientSwatchBtn", props.className].filter(Boolean).join(" "),
|
|
1142
|
+
style: { ...props.style, background: currentGradient },
|
|
1143
|
+
"aria-label": `Open ${label} editor`
|
|
1144
|
+
}
|
|
1145
|
+
),
|
|
1146
|
+
children: /* @__PURE__ */ jsx(GradientPicker, { stops, onChange: handleStopsChange, angle })
|
|
1147
|
+
}
|
|
1148
|
+
) })
|
|
1149
|
+
] })
|
|
1150
|
+
}
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
export {
|
|
1154
|
+
ColorRow,
|
|
1155
|
+
DEFAULT_GRADIENT_STOPS,
|
|
1156
|
+
GradientPicker,
|
|
1157
|
+
GradientRow,
|
|
1158
|
+
SliderOverlapDebugProvider,
|
|
1159
|
+
SliderRow,
|
|
1160
|
+
SwatchPopover,
|
|
1161
|
+
ThemeProvider,
|
|
1162
|
+
ToggleRow,
|
|
1163
|
+
isTextInputTarget,
|
|
1164
|
+
parseGradient,
|
|
1165
|
+
stopsToGradient,
|
|
1166
|
+
useSliderOverlapDebugEnabled,
|
|
1167
|
+
useTheme
|
|
1168
|
+
};
|
|
1169
|
+
//# sourceMappingURL=index.js.map
|