aporia 0.2.7 → 0.2.9

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/dist/index.js CHANGED
@@ -1,19 +1,398 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- import { jsx, jsxs } from "react/jsx-runtime";
5
- import t, { useContext, createContext, useId, useState, useCallback, useRef, useLayoutEffect, useEffect, useMemo } from "react";
6
- import { motion, useMotionValue, useSpring, useTransform } from "motion/react";
4
+ import { jsxs, jsx } from "react/jsx-runtime";
5
+ import t, { useState, useCallback, useRef, useEffect, useLayoutEffect, Fragment, useContext, createContext, useId, useMemo } from "react";
6
+ import { useReducedMotion, AnimatePresence, motion, useMotionValue, useSpring, useTransform } from "motion/react";
7
7
  import { Popover, Select } from "@base-ui/react";
8
- function Panel({ children, className, ...rest }) {
9
- return /* @__PURE__ */ jsx(
10
- "div",
11
- {
12
- className: ["aporiaPanel", className].filter(Boolean).join(" "),
13
- ...rest,
14
- children
8
+ const EASE_IN_OUT_STRONG = [0.77, 0, 0.175, 1];
9
+ const MORPH_DURATION = 0.28;
10
+ const FAST_EASE_OUT = [0.23, 1, 0.32, 1];
11
+ const COLLAPSED_SIZE_PX = 32;
12
+ const DRAG_THRESHOLD_PX = 4;
13
+ const EDGE_PADDING_PX = 8;
14
+ const PANEL_EXPANDED_PADDING_PX = 16;
15
+ const PANEL_HEADER_HEIGHT_PX = 22;
16
+ const PANEL_BODY_MARGIN_TOP_PX = 16;
17
+ const SNAP_HINT_IDLE_OPACITY = 0.5;
18
+ const SNAP_HINT_ACTIVE_OPACITY = 0.8;
19
+ function clamp(n2, min, max) {
20
+ return Math.min(Math.max(n2, min), max);
21
+ }
22
+ function normalizePanelAnchor(anchor) {
23
+ if (anchor === "left") return "top-left";
24
+ if (anchor === "right") return "top-right";
25
+ return anchor;
26
+ }
27
+ function readRootPxVar(name, fallback) {
28
+ if (typeof window === "undefined" || typeof document === "undefined") {
29
+ return fallback;
30
+ }
31
+ const raw = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim();
32
+ const parsed = Number.parseFloat(raw);
33
+ return Number.isFinite(parsed) ? parsed : fallback;
34
+ }
35
+ function anchorFromPoint(centerX, centerY, viewportWidth, viewportHeight) {
36
+ const vertical = centerY > viewportHeight / 2 ? "bottom" : "top";
37
+ const horizontal = centerX > viewportWidth / 2 ? "right" : "left";
38
+ return `${vertical}-${horizontal}`;
39
+ }
40
+ function anchorPosition(anchor, offsets, viewportWidth, viewportHeight) {
41
+ const left = anchor.endsWith("left") ? offsets.left : viewportWidth - offsets.right - COLLAPSED_SIZE_PX;
42
+ const top = anchor.startsWith("top") ? offsets.top : viewportHeight - offsets.bottom - COLLAPSED_SIZE_PX;
43
+ return { left, top };
44
+ }
45
+ function Panel({
46
+ children,
47
+ className,
48
+ floating = true,
49
+ defaultAnchor = "top-left",
50
+ collapsed: collapsedProp,
51
+ defaultCollapsed = false,
52
+ onCollapsedChange,
53
+ collapseAriaLabel = "Toggle panel",
54
+ style: externalStyle,
55
+ ...domRest
56
+ }) {
57
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState(defaultCollapsed);
58
+ const shouldReduceMotion = useReducedMotion();
59
+ const collapsed = collapsedProp !== void 0 ? collapsedProp : uncontrolledCollapsed;
60
+ const setCollapsed = useCallback(
61
+ (next) => {
62
+ onCollapsedChange == null ? void 0 : onCollapsedChange(next);
63
+ if (collapsedProp === void 0) {
64
+ setUncontrolledCollapsed(next);
65
+ }
66
+ },
67
+ [collapsedProp, onCollapsedChange]
68
+ );
69
+ const prevCollapsedRef = useRef(collapsed);
70
+ const collapsedChanged = prevCollapsedRef.current !== collapsed;
71
+ useEffect(() => {
72
+ prevCollapsedRef.current = collapsed;
73
+ }, [collapsed]);
74
+ const shellHeightTransition = shouldReduceMotion ? { duration: 0 } : collapsedChanged ? { duration: MORPH_DURATION, ease: EASE_IN_OUT_STRONG } : { duration: 0 };
75
+ const shellShapeTransition = shouldReduceMotion ? { duration: 0 } : collapsedChanged ? { duration: MORPH_DURATION, ease: EASE_IN_OUT_STRONG } : { duration: 0 };
76
+ const bodyTransition = shouldReduceMotion ? { duration: 0 } : { duration: collapsed ? 0.12 : 0.18, ease: FAST_EASE_OUT };
77
+ const initialAnchor = normalizePanelAnchor(defaultAnchor);
78
+ const [floatingAnchor, setFloatingAnchor] = useState(initialAnchor);
79
+ const bodyRef = useRef(null);
80
+ const bodyContentRef = useRef(null);
81
+ const [measuredBodyHeight, setMeasuredBodyHeight] = useState(0);
82
+ const [viewportHeight, setViewportHeight] = useState(
83
+ () => typeof window !== "undefined" ? window.innerHeight : 0
84
+ );
85
+ const [dragRect, setDragRect] = useState(null);
86
+ const [isDragging, setIsDragging] = useState(false);
87
+ const justDraggedRef = useRef(false);
88
+ const dragSessionRef = useRef(null);
89
+ useLayoutEffect(() => {
90
+ const bodyContent = bodyContentRef.current;
91
+ if (!bodyContent) return;
92
+ const measure = () => {
93
+ setMeasuredBodyHeight(bodyContent.scrollHeight);
94
+ };
95
+ measure();
96
+ const ro = new ResizeObserver(measure);
97
+ ro.observe(bodyContent);
98
+ return () => ro.disconnect();
99
+ }, []);
100
+ useEffect(() => {
101
+ if (typeof window === "undefined") return;
102
+ const onResize = () => setViewportHeight(window.innerHeight);
103
+ window.addEventListener("resize", onResize);
104
+ return () => window.removeEventListener("resize", onResize);
105
+ }, []);
106
+ const handleShellClick = () => {
107
+ if (!collapsed) return;
108
+ if (justDraggedRef.current) {
109
+ justDraggedRef.current = false;
110
+ return;
111
+ }
112
+ setCollapsed(false);
113
+ };
114
+ const handleShellKeyDown = (e) => {
115
+ if (!collapsed) return;
116
+ if (e.key === "Enter" || e.key === " ") {
117
+ e.preventDefault();
118
+ setCollapsed(false);
119
+ }
120
+ };
121
+ const handlePointerDown = (e) => {
122
+ if (!collapsed || !floating) return;
123
+ if (e.button !== 0) return;
124
+ justDraggedRef.current = false;
125
+ const rect = e.currentTarget.getBoundingClientRect();
126
+ dragSessionRef.current = {
127
+ pointerId: e.pointerId,
128
+ startClientX: e.clientX,
129
+ startClientY: e.clientY,
130
+ startTop: rect.top,
131
+ startLeft: rect.left,
132
+ moved: false
133
+ };
134
+ setDragRect({ top: rect.top, left: rect.left });
135
+ e.currentTarget.setPointerCapture(e.pointerId);
136
+ };
137
+ const handlePointerMove = (e) => {
138
+ const session = dragSessionRef.current;
139
+ if (!session || session.pointerId !== e.pointerId) return;
140
+ const deltaY = e.clientY - session.startClientY;
141
+ const deltaX = e.clientX - session.startClientX;
142
+ if (!session.moved && (Math.abs(deltaY) >= DRAG_THRESHOLD_PX || Math.abs(deltaX) >= DRAG_THRESHOLD_PX)) {
143
+ session.moved = true;
144
+ setIsDragging(true);
145
+ }
146
+ if (!session.moved) return;
147
+ const viewportHeight2 = typeof window !== "undefined" ? window.innerHeight : COLLAPSED_SIZE_PX + EDGE_PADDING_PX * 2;
148
+ const viewportWidth = typeof window !== "undefined" ? window.innerWidth : COLLAPSED_SIZE_PX + EDGE_PADDING_PX * 2;
149
+ const maxTop = Math.max(
150
+ EDGE_PADDING_PX,
151
+ viewportHeight2 - COLLAPSED_SIZE_PX - EDGE_PADDING_PX
152
+ );
153
+ const maxLeft = Math.max(
154
+ EDGE_PADDING_PX,
155
+ viewportWidth - COLLAPSED_SIZE_PX - EDGE_PADDING_PX
156
+ );
157
+ setDragRect({
158
+ top: clamp(session.startTop + deltaY, EDGE_PADDING_PX, maxTop),
159
+ left: clamp(session.startLeft + deltaX, EDGE_PADDING_PX, maxLeft)
160
+ });
161
+ };
162
+ const handlePointerUp = (e) => {
163
+ const session = dragSessionRef.current;
164
+ if (!session || session.pointerId !== e.pointerId) return;
165
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
166
+ e.currentTarget.releasePointerCapture(e.pointerId);
167
+ }
168
+ dragSessionRef.current = null;
169
+ setIsDragging(false);
170
+ if (!session.moved) {
171
+ setDragRect(null);
172
+ return;
15
173
  }
174
+ justDraggedRef.current = true;
175
+ const viewportHeight2 = typeof window !== "undefined" ? window.innerHeight : COLLAPSED_SIZE_PX + EDGE_PADDING_PX * 2;
176
+ const viewportWidth = typeof window !== "undefined" ? window.innerWidth : COLLAPSED_SIZE_PX + EDGE_PADDING_PX * 2;
177
+ const maxTop = Math.max(
178
+ EDGE_PADDING_PX,
179
+ viewportHeight2 - COLLAPSED_SIZE_PX - EDGE_PADDING_PX
180
+ );
181
+ const maxLeft = Math.max(
182
+ EDGE_PADDING_PX,
183
+ viewportWidth - COLLAPSED_SIZE_PX - EDGE_PADDING_PX
184
+ );
185
+ const finalTop = clamp(
186
+ (dragRect == null ? void 0 : dragRect.top) ?? session.startTop,
187
+ EDGE_PADDING_PX,
188
+ maxTop
189
+ );
190
+ const finalLeft = clamp(
191
+ (dragRect == null ? void 0 : dragRect.left) ?? session.startLeft,
192
+ EDGE_PADDING_PX,
193
+ maxLeft
194
+ );
195
+ const centerY = finalTop + COLLAPSED_SIZE_PX / 2;
196
+ const centerX = finalLeft + COLLAPSED_SIZE_PX / 2;
197
+ setFloatingAnchor(anchorFromPoint(centerX, centerY, viewportWidth, viewportHeight2));
198
+ setDragRect(null);
199
+ };
200
+ const offsets = {
201
+ top: readRootPxVar("--aporia-panel-offset-top", 40),
202
+ left: readRootPxVar("--aporia-panel-offset-left", 40),
203
+ right: readRootPxVar("--aporia-panel-offset-right", 40),
204
+ bottom: readRootPxVar("--aporia-panel-offset-bottom", 40),
205
+ zIndex: readRootPxVar("--aporia-panel-z-index", 1e3)
206
+ };
207
+ const expandedChromeHeight = PANEL_EXPANDED_PADDING_PX * 2 + PANEL_HEADER_HEIGHT_PX + PANEL_BODY_MARGIN_TOP_PX;
208
+ const expandedContentHeight = expandedChromeHeight + measuredBodyHeight;
209
+ const maxExpandedHeight = floating ? Math.max(
210
+ COLLAPSED_SIZE_PX,
211
+ viewportHeight - offsets.top - offsets.bottom
212
+ ) : Number.POSITIVE_INFINITY;
213
+ const targetExpandedHeight = Math.max(
214
+ COLLAPSED_SIZE_PX,
215
+ Math.min(expandedContentHeight, maxExpandedHeight)
16
216
  );
217
+ const viewport = {
218
+ width: typeof window !== "undefined" ? window.innerWidth : COLLAPSED_SIZE_PX + EDGE_PADDING_PX * 2,
219
+ height: typeof window !== "undefined" ? window.innerHeight : COLLAPSED_SIZE_PX + EDGE_PADDING_PX * 2
220
+ };
221
+ const floatingStyle = (() => {
222
+ if (!floating) return void 0;
223
+ if (dragRect !== null) {
224
+ return {
225
+ left: `${dragRect.left}px`,
226
+ top: `${dragRect.top}px`,
227
+ bottom: "auto",
228
+ right: "auto"
229
+ };
230
+ }
231
+ if (floatingAnchor === "bottom-left") {
232
+ return {
233
+ left: `${offsets.left}px`,
234
+ bottom: `${offsets.bottom}px`,
235
+ top: "auto",
236
+ right: "auto"
237
+ };
238
+ }
239
+ if (floatingAnchor === "top-right") {
240
+ return {
241
+ right: `${offsets.right}px`,
242
+ top: `${offsets.top}px`,
243
+ left: "auto",
244
+ bottom: "auto"
245
+ };
246
+ }
247
+ if (floatingAnchor === "bottom-right") {
248
+ return {
249
+ right: `${offsets.right}px`,
250
+ bottom: `${offsets.bottom}px`,
251
+ left: "auto",
252
+ top: "auto"
253
+ };
254
+ }
255
+ return {
256
+ left: `${offsets.left}px`,
257
+ top: `${offsets.top}px`,
258
+ bottom: "auto",
259
+ right: "auto"
260
+ };
261
+ })();
262
+ const snapHints = (() => {
263
+ if (!floating || !collapsed || !isDragging || dragRect === null) return [];
264
+ const corners = [
265
+ "top-left",
266
+ "top-right",
267
+ "bottom-left",
268
+ "bottom-right"
269
+ ];
270
+ const dragCenter = {
271
+ x: dragRect.left + COLLAPSED_SIZE_PX / 2,
272
+ y: dragRect.top + COLLAPSED_SIZE_PX / 2
273
+ };
274
+ const activeAnchor = anchorFromPoint(
275
+ dragCenter.x,
276
+ dragCenter.y,
277
+ viewport.width,
278
+ viewport.height
279
+ );
280
+ return corners.map((anchor) => {
281
+ const pos = anchorPosition(anchor, offsets, viewport.width, viewport.height);
282
+ const isActive = anchor === activeAnchor;
283
+ return {
284
+ anchor,
285
+ left: pos.left,
286
+ top: pos.top,
287
+ opacity: isActive ? SNAP_HINT_ACTIVE_OPACITY : SNAP_HINT_IDLE_OPACITY
288
+ };
289
+ });
290
+ })();
291
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
292
+ /* @__PURE__ */ jsx(AnimatePresence, { children: snapHints.map((hint) => /* @__PURE__ */ jsx(
293
+ motion.div,
294
+ {
295
+ className: "aporiaPanelSnapHint",
296
+ initial: { opacity: 0 },
297
+ animate: { opacity: hint.opacity },
298
+ exit: { opacity: 0 },
299
+ transition: shouldReduceMotion ? { duration: 0 } : { duration: 0.18, ease: FAST_EASE_OUT },
300
+ style: {
301
+ top: hint.top,
302
+ left: hint.left,
303
+ zIndex: Math.max(0, Math.round(offsets.zIndex - 1))
304
+ }
305
+ },
306
+ hint.anchor
307
+ )) }),
308
+ /* @__PURE__ */ jsxs(
309
+ motion.section,
310
+ {
311
+ ...domRest,
312
+ initial: false,
313
+ onClick: handleShellClick,
314
+ onKeyDown: handleShellKeyDown,
315
+ onPointerDown: handlePointerDown,
316
+ onPointerMove: handlePointerMove,
317
+ onPointerUp: handlePointerUp,
318
+ onPointerCancel: handlePointerUp,
319
+ role: collapsed ? "button" : void 0,
320
+ tabIndex: collapsed ? 0 : void 0,
321
+ "aria-label": collapsed ? "Expand panel" : void 0,
322
+ animate: {
323
+ height: collapsed ? COLLAPSED_SIZE_PX : targetExpandedHeight,
324
+ borderRadius: collapsed ? 12 : 24,
325
+ padding: collapsed ? 5 : 16,
326
+ scale: collapsed && isDragging ? 1.06 : 1,
327
+ rotate: collapsed && isDragging ? -2.5 : 0,
328
+ y: collapsed && isDragging ? -4 : 0
329
+ },
330
+ transition: {
331
+ height: shellHeightTransition,
332
+ borderRadius: shellShapeTransition,
333
+ padding: shellShapeTransition,
334
+ scale: shouldReduceMotion ? { duration: 0 } : { duration: 0.18, ease: FAST_EASE_OUT },
335
+ rotate: shouldReduceMotion ? { duration: 0 } : { duration: 0.18, ease: FAST_EASE_OUT },
336
+ y: shouldReduceMotion ? { duration: 0 } : { duration: 0.18, ease: FAST_EASE_OUT }
337
+ },
338
+ style: {
339
+ ...externalStyle,
340
+ ...floatingStyle
341
+ },
342
+ className: ["aporiaPanel", className].filter(Boolean).join(" "),
343
+ "data-collapsed": collapsed ? "true" : "false",
344
+ "data-floating": floating ? "true" : "false",
345
+ "data-anchor": floatingAnchor,
346
+ "data-dragging": isDragging ? "true" : "false",
347
+ children: [
348
+ /* @__PURE__ */ jsx("div", { className: "aporiaPanelHeader", children: /* @__PURE__ */ jsx(
349
+ "button",
350
+ {
351
+ type: "button",
352
+ className: "aporiaPanelToggle",
353
+ "aria-label": collapseAriaLabel,
354
+ "aria-pressed": collapsed,
355
+ onClick: () => setCollapsed(!collapsed),
356
+ children: /* @__PURE__ */ jsx(
357
+ "svg",
358
+ {
359
+ className: "aporiaPanelLogoGlyph",
360
+ viewBox: "0 0 22 22",
361
+ "aria-hidden": "true",
362
+ focusable: "false",
363
+ children: /* @__PURE__ */ jsx(
364
+ "path",
365
+ {
366
+ fillRule: "evenodd",
367
+ clipRule: "evenodd",
368
+ d: "M17.6185 16.089C18 15.3403 18 14.3602 18 12.4V9.6C18 7.63982 18 6.65972 17.6185 5.91103C17.283 5.25247 16.7475 4.71703 16.089 4.38148C15.3403 4 14.3602 4 12.4 4H9.6C7.63982 4 6.65972 4 5.91103 4.38148C5.25247 4.71703 4.71703 5.25247 4.38148 5.91103C4 6.65972 4 7.63982 4 9.6V12.4C4 14.3602 4 15.3403 4.38148 16.089C4.71703 16.7475 5.25247 17.283 5.91103 17.6185C6.65972 18 7.63982 18 9.6 18H12.4C14.3602 18 15.3403 18 16.089 17.6185C16.7475 17.283 17.283 16.7475 17.6185 16.089ZM10.125 13.4062V8.59375C10.125 7.5745 10.125 7.06488 10.2915 6.66288C10.5135 6.12688 10.9394 5.70103 11.4754 5.47901C11.8774 5.3125 12.387 5.3125 13.4062 5.3125C14.4255 5.3125 14.9351 5.3125 15.3371 5.47901C15.8731 5.70103 16.299 6.12688 16.521 6.66288C16.6875 7.06488 16.6875 7.5745 16.6875 8.59375V13.4062C16.6875 14.4255 16.6875 14.9351 16.521 15.3371C16.299 15.8731 15.8731 16.299 15.3371 16.521C14.9351 16.6875 14.4255 16.6875 13.4062 16.6875C12.387 16.6875 11.8774 16.6875 11.4754 16.521C10.9394 16.299 10.5135 15.8731 10.2915 15.3371C10.125 14.9351 10.125 14.4255 10.125 13.4062Z",
369
+ fill: "currentColor"
370
+ }
371
+ )
372
+ }
373
+ )
374
+ }
375
+ ) }),
376
+ /* @__PURE__ */ jsx(
377
+ motion.div,
378
+ {
379
+ ref: bodyRef,
380
+ className: "aporiaPanelBody",
381
+ "aria-hidden": collapsed,
382
+ inert: collapsed,
383
+ initial: false,
384
+ animate: {
385
+ opacity: collapsed ? 0 : 1,
386
+ marginTop: collapsed ? 0 : 16
387
+ },
388
+ transition: bodyTransition,
389
+ children: /* @__PURE__ */ jsx("div", { ref: bodyContentRef, className: "aporiaPanelBodyContent", children })
390
+ }
391
+ )
392
+ ]
393
+ }
394
+ )
395
+ ] });
17
396
  }
18
397
  const CategoryDisabledContext = createContext(false);
19
398
  function CategoryDisabledProvider({
@@ -90,8 +469,8 @@ function Category({
90
469
  {
91
470
  className: "categoryChevronIcon",
92
471
  "data-collapsed": collapsed ? "true" : "false",
93
- width: "12",
94
- height: "12",
472
+ width: "14",
473
+ height: "14",
95
474
  viewBox: "0 0 12 12",
96
475
  "aria-hidden": true,
97
476
  children: /* @__PURE__ */ jsx(
@@ -153,8 +532,9 @@ function Category({
153
532
  opacity: collapsed ? { duration: 0.09, ease: "easeIn" } : { duration: 0.15, ease: "easeOut" }
154
533
  },
155
534
  style: {
156
- /* visible so SliderRow edge stretch (width/margin motion) isn’t clipped; collapse hides via height + opacity + inert */
157
- overflow: "visible",
535
+ // Keep overdrag visible while open, but fully clip collapsed content so
536
+ // the last category cannot leak height into panel measurements.
537
+ overflow: collapsed ? "hidden" : "visible",
158
538
  minHeight: 0,
159
539
  boxSizing: "border-box"
160
540
  },
@@ -1210,6 +1590,14 @@ function normalizeHex(raw) {
1210
1590
  }
1211
1591
  return `#${h2.toUpperCase()}`;
1212
1592
  }
1593
+ function sanitizeHexDigits(raw) {
1594
+ return raw.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
1595
+ }
1596
+ function expandHexDigitsToSix(raw) {
1597
+ const digits = sanitizeHexDigits(raw);
1598
+ if (!digits) return null;
1599
+ return digits.repeat(Math.ceil(6 / digits.length)).slice(0, 6).toUpperCase();
1600
+ }
1213
1601
  function hslToHex(h2, s, l2) {
1214
1602
  const [r, g, b2] = hslToRgbBytes(h2, s, l2);
1215
1603
  const toHex = (v2) => v2.toString(16).padStart(2, "0");
@@ -1447,12 +1835,6 @@ function clampHueDeg(h2) {
1447
1835
  function clampInt(n2, min, max) {
1448
1836
  return Math.max(min, Math.min(max, Math.round(n2)));
1449
1837
  }
1450
- function normalizeHexDigits(raw) {
1451
- return raw.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
1452
- }
1453
- function isValidSixHex$2(d2) {
1454
- return /^[0-9a-fA-F]{6}$/.test(d2);
1455
- }
1456
1838
  function clampHsv01(base) {
1457
1839
  return { h: clampHueDeg(base.h), s: clamp01(base.s), v: clamp01(base.v) };
1458
1840
  }
@@ -1796,11 +2178,11 @@ function ColorPicker({ value, onChange }) {
1796
2178
  value: hex.slice(1),
1797
2179
  prefix: "#",
1798
2180
  onCommit: (val) => {
1799
- const d2 = normalizeHexDigits(val);
1800
- if (isValidSixHex$2(d2)) onChange(`#${d2.toUpperCase()}`);
2181
+ const expanded = expandHexDigitsToSix(val);
2182
+ if (expanded) onChange(`#${expanded}`);
1801
2183
  },
1802
- sanitize: normalizeHexDigits,
1803
- validate: isValidSixHex$2,
2184
+ sanitize: sanitizeHexDigits,
2185
+ validate: (v2) => sanitizeHexDigits(v2).length > 0,
1804
2186
  maxLength: 6,
1805
2187
  inputMode: "text",
1806
2188
  ariaLabel: `Edit hex color, ${hex}`,
@@ -1811,7 +2193,7 @@ function ColorPicker({ value, onChange }) {
1811
2193
  onChange(parsed);
1812
2194
  return "";
1813
2195
  }
1814
- return normalizeHexDigits(pasted);
2196
+ return sanitizeHexDigits(pasted);
1815
2197
  }
1816
2198
  }
1817
2199
  ) : mode === "hsl" ? /* @__PURE__ */ jsxs("div", { className: "colorPickerTriplet", "aria-label": "HSL values", children: [
@@ -2048,20 +2430,15 @@ function hexDigits$1(hex) {
2048
2430
  const n2 = normalizeHex(hex);
2049
2431
  return n2.slice(1);
2050
2432
  }
2051
- function isValidSixHex$1(d2) {
2052
- return /^[0-9a-fA-F]{6}$/.test(d2);
2053
- }
2054
- function sanitizeHex$1(input) {
2055
- return input.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
2056
- }
2057
2433
  function ColorRow({ label = "Color", value, onChange, disabled: disabledProp }) {
2058
2434
  const categoryDisabled = useCategoryDisabled();
2059
2435
  const disabled = Boolean(disabledProp || categoryDisabled);
2060
2436
  const [hovered, setHovered] = useState(false);
2061
2437
  const hex = normalizeHex(value);
2062
2438
  const handleCommit = (sanitized) => {
2063
- if (isValidSixHex$1(sanitized)) {
2064
- onChange(`#${sanitized.toUpperCase()}`);
2439
+ const expanded = expandHexDigitsToSix(sanitized);
2440
+ if (expanded) {
2441
+ onChange(`#${expanded}`);
2065
2442
  }
2066
2443
  };
2067
2444
  const handlePaste = (pasted) => {
@@ -2083,8 +2460,8 @@ function ColorRow({ label = "Color", value, onChange, disabled: disabledProp })
2083
2460
  value: hexDigits$1(hex),
2084
2461
  prefix: "#",
2085
2462
  onCommit: handleCommit,
2086
- sanitize: sanitizeHex$1,
2087
- validate: isValidSixHex$1,
2463
+ sanitize: sanitizeHexDigits,
2464
+ validate: (v2) => sanitizeHexDigits(v2).length > 0,
2088
2465
  maxLength: 6,
2089
2466
  inputMode: "text",
2090
2467
  ariaLabel: `Edit hex color, ${hex}`,
@@ -2256,9 +2633,6 @@ function generateAestheticGradient(stopCount) {
2256
2633
  function hexDigits(hex) {
2257
2634
  return normalizeHex(hex).slice(1);
2258
2635
  }
2259
- function isValidSixHex(d2) {
2260
- return /^[0-9a-fA-F]{6}$/.test(d2);
2261
- }
2262
2636
  function stopsToGradient(stops, angle = 90) {
2263
2637
  if (stops.length === 0) return "linear-gradient(90deg, #808080, #808080)";
2264
2638
  if (stops.length === 1) {
@@ -2293,19 +2667,16 @@ const DEFAULT_GRADIENT_STOPS = [
2293
2667
  { color: "#42C0B0" },
2294
2668
  { color: "#BAC9C7" }
2295
2669
  ];
2296
- function sanitizeHex(input) {
2297
- return input.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
2298
- }
2299
2670
  function StopRow({ index, stop, canDelete, onColorChange, onDelete }) {
2300
2671
  const hex = normalizeHex(stop.color);
2301
2672
  const handleCommit = (val) => {
2302
- const d2 = sanitizeHex(val);
2303
- if (isValidSixHex(d2)) {
2304
- onColorChange(`#${d2.toUpperCase()}`);
2673
+ const expanded = expandHexDigitsToSix(val);
2674
+ if (expanded) {
2675
+ onColorChange(`#${expanded}`);
2305
2676
  }
2306
2677
  };
2307
2678
  const handlePaste = (pasted) => {
2308
- return sanitizeHex(pasted);
2679
+ return sanitizeHexDigits(pasted);
2309
2680
  };
2310
2681
  return /* @__PURE__ */ jsxs("div", { className: "gradientPickerStop", children: [
2311
2682
  /* @__PURE__ */ jsxs("div", { className: "gradientPickerStopColorHex", children: [
@@ -2336,8 +2707,8 @@ function StopRow({ index, stop, canDelete, onColorChange, onDelete }) {
2336
2707
  value: hexDigits(hex),
2337
2708
  prefix: "#",
2338
2709
  onCommit: handleCommit,
2339
- sanitize: sanitizeHex,
2340
- validate: isValidSixHex,
2710
+ sanitize: sanitizeHexDigits,
2711
+ validate: (v2) => sanitizeHexDigits(v2).length > 0,
2341
2712
  maxLength: 6,
2342
2713
  inputMode: "text",
2343
2714
  ariaLabel: "Hex color without hash",