aporia 0.2.6 → 0.2.8

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,391 @@
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 readRootPxVar(name, fallback) {
23
+ if (typeof window === "undefined" || typeof document === "undefined") {
24
+ return fallback;
25
+ }
26
+ const raw = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim();
27
+ const parsed = Number.parseFloat(raw);
28
+ return Number.isFinite(parsed) ? parsed : fallback;
29
+ }
30
+ function anchorFromPoint(centerX, centerY, viewportWidth, viewportHeight) {
31
+ const vertical = centerY > viewportHeight / 2 ? "bottom" : "top";
32
+ const horizontal = centerX > viewportWidth / 2 ? "right" : "left";
33
+ return `${vertical}-${horizontal}`;
34
+ }
35
+ function anchorPosition(anchor, offsets, viewportWidth, viewportHeight) {
36
+ const left = anchor.endsWith("left") ? offsets.left : viewportWidth - offsets.right - COLLAPSED_SIZE_PX;
37
+ const top = anchor.startsWith("top") ? offsets.top : viewportHeight - offsets.bottom - COLLAPSED_SIZE_PX;
38
+ return { left, top };
39
+ }
40
+ function Panel({
41
+ children,
42
+ className,
43
+ floating = true,
44
+ collapsed: collapsedProp,
45
+ defaultCollapsed = false,
46
+ onCollapsedChange,
47
+ collapseAriaLabel = "Toggle panel",
48
+ style: externalStyle,
49
+ ...domRest
50
+ }) {
51
+ const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState(defaultCollapsed);
52
+ const shouldReduceMotion = useReducedMotion();
53
+ const collapsed = collapsedProp !== void 0 ? collapsedProp : uncontrolledCollapsed;
54
+ const setCollapsed = useCallback(
55
+ (next) => {
56
+ onCollapsedChange == null ? void 0 : onCollapsedChange(next);
57
+ if (collapsedProp === void 0) {
58
+ setUncontrolledCollapsed(next);
59
+ }
60
+ },
61
+ [collapsedProp, onCollapsedChange]
62
+ );
63
+ const prevCollapsedRef = useRef(collapsed);
64
+ const collapsedChanged = prevCollapsedRef.current !== collapsed;
65
+ useEffect(() => {
66
+ prevCollapsedRef.current = collapsed;
67
+ }, [collapsed]);
68
+ const shellHeightTransition = shouldReduceMotion ? { duration: 0 } : collapsedChanged ? { duration: MORPH_DURATION, ease: EASE_IN_OUT_STRONG } : { duration: 0 };
69
+ const shellShapeTransition = shouldReduceMotion ? { duration: 0 } : collapsedChanged ? { duration: MORPH_DURATION, ease: EASE_IN_OUT_STRONG } : { duration: 0 };
70
+ const bodyTransition = shouldReduceMotion ? { duration: 0 } : { duration: collapsed ? 0.12 : 0.18, ease: FAST_EASE_OUT };
71
+ const [floatingAnchor, setFloatingAnchor] = useState("top-left");
72
+ const bodyRef = useRef(null);
73
+ const bodyContentRef = useRef(null);
74
+ const [measuredBodyHeight, setMeasuredBodyHeight] = useState(0);
75
+ const [viewportHeight, setViewportHeight] = useState(
76
+ () => typeof window !== "undefined" ? window.innerHeight : 0
77
+ );
78
+ const [dragRect, setDragRect] = useState(null);
79
+ const [isDragging, setIsDragging] = useState(false);
80
+ const justDraggedRef = useRef(false);
81
+ const dragSessionRef = useRef(null);
82
+ useLayoutEffect(() => {
83
+ const bodyContent = bodyContentRef.current;
84
+ if (!bodyContent) return;
85
+ const measure = () => {
86
+ setMeasuredBodyHeight(bodyContent.scrollHeight);
87
+ };
88
+ measure();
89
+ const ro = new ResizeObserver(measure);
90
+ ro.observe(bodyContent);
91
+ return () => ro.disconnect();
92
+ }, []);
93
+ useEffect(() => {
94
+ if (typeof window === "undefined") return;
95
+ const onResize = () => setViewportHeight(window.innerHeight);
96
+ window.addEventListener("resize", onResize);
97
+ return () => window.removeEventListener("resize", onResize);
98
+ }, []);
99
+ const handleShellClick = () => {
100
+ if (!collapsed) return;
101
+ if (justDraggedRef.current) {
102
+ justDraggedRef.current = false;
103
+ return;
104
+ }
105
+ setCollapsed(false);
106
+ };
107
+ const handleShellKeyDown = (e) => {
108
+ if (!collapsed) return;
109
+ if (e.key === "Enter" || e.key === " ") {
110
+ e.preventDefault();
111
+ setCollapsed(false);
112
+ }
113
+ };
114
+ const handlePointerDown = (e) => {
115
+ if (!collapsed || !floating) return;
116
+ if (e.button !== 0) return;
117
+ justDraggedRef.current = false;
118
+ const rect = e.currentTarget.getBoundingClientRect();
119
+ dragSessionRef.current = {
120
+ pointerId: e.pointerId,
121
+ startClientX: e.clientX,
122
+ startClientY: e.clientY,
123
+ startTop: rect.top,
124
+ startLeft: rect.left,
125
+ moved: false
126
+ };
127
+ setDragRect({ top: rect.top, left: rect.left });
128
+ e.currentTarget.setPointerCapture(e.pointerId);
129
+ };
130
+ const handlePointerMove = (e) => {
131
+ const session = dragSessionRef.current;
132
+ if (!session || session.pointerId !== e.pointerId) return;
133
+ const deltaY = e.clientY - session.startClientY;
134
+ const deltaX = e.clientX - session.startClientX;
135
+ if (!session.moved && (Math.abs(deltaY) >= DRAG_THRESHOLD_PX || Math.abs(deltaX) >= DRAG_THRESHOLD_PX)) {
136
+ session.moved = true;
137
+ setIsDragging(true);
138
+ }
139
+ if (!session.moved) return;
140
+ const viewportHeight2 = typeof window !== "undefined" ? window.innerHeight : COLLAPSED_SIZE_PX + EDGE_PADDING_PX * 2;
141
+ const viewportWidth = typeof window !== "undefined" ? window.innerWidth : COLLAPSED_SIZE_PX + EDGE_PADDING_PX * 2;
142
+ const maxTop = Math.max(
143
+ EDGE_PADDING_PX,
144
+ viewportHeight2 - COLLAPSED_SIZE_PX - EDGE_PADDING_PX
145
+ );
146
+ const maxLeft = Math.max(
147
+ EDGE_PADDING_PX,
148
+ viewportWidth - COLLAPSED_SIZE_PX - EDGE_PADDING_PX
149
+ );
150
+ setDragRect({
151
+ top: clamp(session.startTop + deltaY, EDGE_PADDING_PX, maxTop),
152
+ left: clamp(session.startLeft + deltaX, EDGE_PADDING_PX, maxLeft)
153
+ });
154
+ };
155
+ const handlePointerUp = (e) => {
156
+ const session = dragSessionRef.current;
157
+ if (!session || session.pointerId !== e.pointerId) return;
158
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
159
+ e.currentTarget.releasePointerCapture(e.pointerId);
160
+ }
161
+ dragSessionRef.current = null;
162
+ setIsDragging(false);
163
+ if (!session.moved) {
164
+ setDragRect(null);
165
+ return;
15
166
  }
167
+ justDraggedRef.current = true;
168
+ const viewportHeight2 = typeof window !== "undefined" ? window.innerHeight : COLLAPSED_SIZE_PX + EDGE_PADDING_PX * 2;
169
+ const viewportWidth = typeof window !== "undefined" ? window.innerWidth : COLLAPSED_SIZE_PX + EDGE_PADDING_PX * 2;
170
+ const maxTop = Math.max(
171
+ EDGE_PADDING_PX,
172
+ viewportHeight2 - COLLAPSED_SIZE_PX - EDGE_PADDING_PX
173
+ );
174
+ const maxLeft = Math.max(
175
+ EDGE_PADDING_PX,
176
+ viewportWidth - COLLAPSED_SIZE_PX - EDGE_PADDING_PX
177
+ );
178
+ const finalTop = clamp(
179
+ (dragRect == null ? void 0 : dragRect.top) ?? session.startTop,
180
+ EDGE_PADDING_PX,
181
+ maxTop
182
+ );
183
+ const finalLeft = clamp(
184
+ (dragRect == null ? void 0 : dragRect.left) ?? session.startLeft,
185
+ EDGE_PADDING_PX,
186
+ maxLeft
187
+ );
188
+ const centerY = finalTop + COLLAPSED_SIZE_PX / 2;
189
+ const centerX = finalLeft + COLLAPSED_SIZE_PX / 2;
190
+ setFloatingAnchor(anchorFromPoint(centerX, centerY, viewportWidth, viewportHeight2));
191
+ setDragRect(null);
192
+ };
193
+ const offsets = {
194
+ top: readRootPxVar("--aporia-panel-offset-top", 40),
195
+ left: readRootPxVar("--aporia-panel-offset-left", 40),
196
+ right: readRootPxVar("--aporia-panel-offset-right", 40),
197
+ bottom: readRootPxVar("--aporia-panel-offset-bottom", 40),
198
+ zIndex: readRootPxVar("--aporia-panel-z-index", 1e3)
199
+ };
200
+ const expandedChromeHeight = PANEL_EXPANDED_PADDING_PX * 2 + PANEL_HEADER_HEIGHT_PX + PANEL_BODY_MARGIN_TOP_PX;
201
+ const expandedContentHeight = expandedChromeHeight + measuredBodyHeight;
202
+ const maxExpandedHeight = floating ? Math.max(
203
+ COLLAPSED_SIZE_PX,
204
+ viewportHeight - offsets.top - offsets.bottom
205
+ ) : Number.POSITIVE_INFINITY;
206
+ const targetExpandedHeight = Math.max(
207
+ COLLAPSED_SIZE_PX,
208
+ Math.min(expandedContentHeight, maxExpandedHeight)
16
209
  );
210
+ const viewport = {
211
+ width: typeof window !== "undefined" ? window.innerWidth : COLLAPSED_SIZE_PX + EDGE_PADDING_PX * 2,
212
+ height: typeof window !== "undefined" ? window.innerHeight : COLLAPSED_SIZE_PX + EDGE_PADDING_PX * 2
213
+ };
214
+ const floatingStyle = (() => {
215
+ if (!floating) return void 0;
216
+ if (dragRect !== null) {
217
+ return {
218
+ left: `${dragRect.left}px`,
219
+ top: `${dragRect.top}px`,
220
+ bottom: "auto",
221
+ right: "auto"
222
+ };
223
+ }
224
+ if (floatingAnchor === "bottom-left") {
225
+ return {
226
+ left: `${offsets.left}px`,
227
+ bottom: `${offsets.bottom}px`,
228
+ top: "auto",
229
+ right: "auto"
230
+ };
231
+ }
232
+ if (floatingAnchor === "top-right") {
233
+ return {
234
+ right: `${offsets.right}px`,
235
+ top: `${offsets.top}px`,
236
+ left: "auto",
237
+ bottom: "auto"
238
+ };
239
+ }
240
+ if (floatingAnchor === "bottom-right") {
241
+ return {
242
+ right: `${offsets.right}px`,
243
+ bottom: `${offsets.bottom}px`,
244
+ left: "auto",
245
+ top: "auto"
246
+ };
247
+ }
248
+ return {
249
+ left: `${offsets.left}px`,
250
+ top: `${offsets.top}px`,
251
+ bottom: "auto",
252
+ right: "auto"
253
+ };
254
+ })();
255
+ const snapHints = (() => {
256
+ if (!floating || !collapsed || !isDragging || dragRect === null) return [];
257
+ const corners = [
258
+ "top-left",
259
+ "top-right",
260
+ "bottom-left",
261
+ "bottom-right"
262
+ ];
263
+ const dragCenter = {
264
+ x: dragRect.left + COLLAPSED_SIZE_PX / 2,
265
+ y: dragRect.top + COLLAPSED_SIZE_PX / 2
266
+ };
267
+ const activeAnchor = anchorFromPoint(
268
+ dragCenter.x,
269
+ dragCenter.y,
270
+ viewport.width,
271
+ viewport.height
272
+ );
273
+ return corners.map((anchor) => {
274
+ const pos = anchorPosition(anchor, offsets, viewport.width, viewport.height);
275
+ const isActive = anchor === activeAnchor;
276
+ return {
277
+ anchor,
278
+ left: pos.left,
279
+ top: pos.top,
280
+ opacity: isActive ? SNAP_HINT_ACTIVE_OPACITY : SNAP_HINT_IDLE_OPACITY
281
+ };
282
+ });
283
+ })();
284
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
285
+ /* @__PURE__ */ jsx(AnimatePresence, { children: snapHints.map((hint) => /* @__PURE__ */ jsx(
286
+ motion.div,
287
+ {
288
+ className: "aporiaPanelSnapHint",
289
+ initial: { opacity: 0 },
290
+ animate: { opacity: hint.opacity },
291
+ exit: { opacity: 0 },
292
+ transition: shouldReduceMotion ? { duration: 0 } : { duration: 0.18, ease: FAST_EASE_OUT },
293
+ style: {
294
+ top: hint.top,
295
+ left: hint.left,
296
+ zIndex: Math.max(0, Math.round(offsets.zIndex - 1))
297
+ }
298
+ },
299
+ hint.anchor
300
+ )) }),
301
+ /* @__PURE__ */ jsxs(
302
+ motion.section,
303
+ {
304
+ ...domRest,
305
+ initial: false,
306
+ onClick: handleShellClick,
307
+ onKeyDown: handleShellKeyDown,
308
+ onPointerDown: handlePointerDown,
309
+ onPointerMove: handlePointerMove,
310
+ onPointerUp: handlePointerUp,
311
+ onPointerCancel: handlePointerUp,
312
+ role: collapsed ? "button" : void 0,
313
+ tabIndex: collapsed ? 0 : void 0,
314
+ "aria-label": collapsed ? "Expand panel" : void 0,
315
+ animate: {
316
+ height: collapsed ? COLLAPSED_SIZE_PX : targetExpandedHeight,
317
+ borderRadius: collapsed ? 12 : 24,
318
+ padding: collapsed ? 5 : 16,
319
+ scale: collapsed && isDragging ? 1.06 : 1,
320
+ rotate: collapsed && isDragging ? -2.5 : 0,
321
+ y: collapsed && isDragging ? -4 : 0
322
+ },
323
+ transition: {
324
+ height: shellHeightTransition,
325
+ borderRadius: shellShapeTransition,
326
+ padding: shellShapeTransition,
327
+ scale: shouldReduceMotion ? { duration: 0 } : { duration: 0.18, ease: FAST_EASE_OUT },
328
+ rotate: shouldReduceMotion ? { duration: 0 } : { duration: 0.18, ease: FAST_EASE_OUT },
329
+ y: shouldReduceMotion ? { duration: 0 } : { duration: 0.18, ease: FAST_EASE_OUT }
330
+ },
331
+ style: {
332
+ ...externalStyle,
333
+ ...floatingStyle
334
+ },
335
+ className: ["aporiaPanel", className].filter(Boolean).join(" "),
336
+ "data-collapsed": collapsed ? "true" : "false",
337
+ "data-floating": floating ? "true" : "false",
338
+ "data-anchor": floatingAnchor,
339
+ "data-dragging": isDragging ? "true" : "false",
340
+ children: [
341
+ /* @__PURE__ */ jsx("div", { className: "aporiaPanelHeader", children: /* @__PURE__ */ jsx(
342
+ "button",
343
+ {
344
+ type: "button",
345
+ className: "aporiaPanelToggle",
346
+ "aria-label": collapseAriaLabel,
347
+ "aria-pressed": collapsed,
348
+ onClick: () => setCollapsed(!collapsed),
349
+ children: /* @__PURE__ */ jsx(
350
+ "svg",
351
+ {
352
+ className: "aporiaPanelLogoGlyph",
353
+ viewBox: "0 0 22 22",
354
+ "aria-hidden": "true",
355
+ focusable: "false",
356
+ children: /* @__PURE__ */ jsx(
357
+ "path",
358
+ {
359
+ fillRule: "evenodd",
360
+ clipRule: "evenodd",
361
+ 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",
362
+ fill: "currentColor"
363
+ }
364
+ )
365
+ }
366
+ )
367
+ }
368
+ ) }),
369
+ /* @__PURE__ */ jsx(
370
+ motion.div,
371
+ {
372
+ ref: bodyRef,
373
+ className: "aporiaPanelBody",
374
+ "aria-hidden": collapsed,
375
+ inert: collapsed,
376
+ initial: false,
377
+ animate: {
378
+ opacity: collapsed ? 0 : 1,
379
+ marginTop: collapsed ? 0 : 16
380
+ },
381
+ transition: bodyTransition,
382
+ children: /* @__PURE__ */ jsx("div", { ref: bodyContentRef, className: "aporiaPanelBodyContent", children })
383
+ }
384
+ )
385
+ ]
386
+ }
387
+ )
388
+ ] });
17
389
  }
18
390
  const CategoryDisabledContext = createContext(false);
19
391
  function CategoryDisabledProvider({
@@ -90,8 +462,8 @@ function Category({
90
462
  {
91
463
  className: "categoryChevronIcon",
92
464
  "data-collapsed": collapsed ? "true" : "false",
93
- width: "12",
94
- height: "12",
465
+ width: "14",
466
+ height: "14",
95
467
  viewBox: "0 0 12 12",
96
468
  "aria-hidden": true,
97
469
  children: /* @__PURE__ */ jsx(
@@ -153,8 +525,9 @@ function Category({
153
525
  opacity: collapsed ? { duration: 0.09, ease: "easeIn" } : { duration: 0.15, ease: "easeOut" }
154
526
  },
155
527
  style: {
156
- /* visible so SliderRow edge stretch (width/margin motion) isn’t clipped; collapse hides via height + opacity + inert */
157
- overflow: "visible",
528
+ // Keep overdrag visible while open, but fully clip collapsed content so
529
+ // the last category cannot leak height into panel measurements.
530
+ overflow: collapsed ? "hidden" : "visible",
158
531
  minHeight: 0,
159
532
  boxSizing: "border-box"
160
533
  },
@@ -835,21 +1208,19 @@ function SliderRow({
835
1208
  if (Math.abs(leftExtra) < STRETCH_SNAP_EPS_PX) leftExtra = 0;
836
1209
  return Math.round(-leftExtra);
837
1210
  });
838
- const syncBaseWidthFromParent = () => {
1211
+ const syncBaseWidthFromWrap = () => {
839
1212
  const wrap = wrapRef.current;
840
- const parent = wrap == null ? void 0 : wrap.parentElement;
841
- if (!parent) return;
842
- baseW.set(parent.offsetWidth);
1213
+ if (!wrap) return;
1214
+ baseW.set(wrap.offsetWidth);
843
1215
  };
844
1216
  useLayoutEffect(() => {
845
1217
  const wrap = wrapRef.current;
846
- const parent = wrap == null ? void 0 : wrap.parentElement;
847
- if (!parent) return;
848
- syncBaseWidthFromParent();
1218
+ if (!wrap) return;
1219
+ syncBaseWidthFromWrap();
849
1220
  const ro = new ResizeObserver(() => {
850
- syncBaseWidthFromParent();
1221
+ syncBaseWidthFromWrap();
851
1222
  });
852
- ro.observe(parent);
1223
+ ro.observe(wrap);
853
1224
  return () => ro.disconnect();
854
1225
  }, [baseW]);
855
1226
  useEffect(() => {
@@ -932,7 +1303,7 @@ function SliderRow({
932
1303
  const onEnd = () => {
933
1304
  resetPull();
934
1305
  requestAnimationFrame(() => {
935
- requestAnimationFrame(syncBaseWidthFromParent);
1306
+ requestAnimationFrame(syncBaseWidthFromWrap);
936
1307
  });
937
1308
  };
938
1309
  window.addEventListener("pointermove", onMove);
@@ -999,7 +1370,7 @@ function SliderRow({
999
1370
  if (disabled) return;
1000
1371
  clearDocumentSelection();
1001
1372
  lastPointerXRef.current = e.clientX;
1002
- syncBaseWidthFromParent();
1373
+ syncBaseWidthFromWrap();
1003
1374
  try {
1004
1375
  e.currentTarget.setPointerCapture(e.pointerId);
1005
1376
  } catch {
@@ -1019,7 +1390,7 @@ function SliderRow({
1019
1390
  resetPull();
1020
1391
  setDragging(false);
1021
1392
  requestAnimationFrame(() => {
1022
- requestAnimationFrame(syncBaseWidthFromParent);
1393
+ requestAnimationFrame(syncBaseWidthFromWrap);
1023
1394
  });
1024
1395
  };
1025
1396
  return /* @__PURE__ */ jsx(
@@ -1137,7 +1508,7 @@ function SliderRow({
1137
1508
  resetPull();
1138
1509
  setDragging(false);
1139
1510
  requestAnimationFrame(() => {
1140
- requestAnimationFrame(syncBaseWidthFromParent);
1511
+ requestAnimationFrame(syncBaseWidthFromWrap);
1141
1512
  });
1142
1513
  },
1143
1514
  onLostPointerCapture: () => {
@@ -1146,7 +1517,7 @@ function SliderRow({
1146
1517
  resetPull();
1147
1518
  setDragging(false);
1148
1519
  requestAnimationFrame(() => {
1149
- requestAnimationFrame(syncBaseWidthFromParent);
1520
+ requestAnimationFrame(syncBaseWidthFromWrap);
1150
1521
  });
1151
1522
  }
1152
1523
  }
@@ -1212,6 +1583,14 @@ function normalizeHex(raw) {
1212
1583
  }
1213
1584
  return `#${h2.toUpperCase()}`;
1214
1585
  }
1586
+ function sanitizeHexDigits(raw) {
1587
+ return raw.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
1588
+ }
1589
+ function expandHexDigitsToSix(raw) {
1590
+ const digits = sanitizeHexDigits(raw);
1591
+ if (!digits) return null;
1592
+ return digits.repeat(Math.ceil(6 / digits.length)).slice(0, 6).toUpperCase();
1593
+ }
1215
1594
  function hslToHex(h2, s, l2) {
1216
1595
  const [r, g, b2] = hslToRgbBytes(h2, s, l2);
1217
1596
  const toHex = (v2) => v2.toString(16).padStart(2, "0");
@@ -1449,12 +1828,6 @@ function clampHueDeg(h2) {
1449
1828
  function clampInt(n2, min, max) {
1450
1829
  return Math.max(min, Math.min(max, Math.round(n2)));
1451
1830
  }
1452
- function normalizeHexDigits(raw) {
1453
- return raw.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
1454
- }
1455
- function isValidSixHex$2(d2) {
1456
- return /^[0-9a-fA-F]{6}$/.test(d2);
1457
- }
1458
1831
  function clampHsv01(base) {
1459
1832
  return { h: clampHueDeg(base.h), s: clamp01(base.s), v: clamp01(base.v) };
1460
1833
  }
@@ -1798,11 +2171,11 @@ function ColorPicker({ value, onChange }) {
1798
2171
  value: hex.slice(1),
1799
2172
  prefix: "#",
1800
2173
  onCommit: (val) => {
1801
- const d2 = normalizeHexDigits(val);
1802
- if (isValidSixHex$2(d2)) onChange(`#${d2.toUpperCase()}`);
2174
+ const expanded = expandHexDigitsToSix(val);
2175
+ if (expanded) onChange(`#${expanded}`);
1803
2176
  },
1804
- sanitize: normalizeHexDigits,
1805
- validate: isValidSixHex$2,
2177
+ sanitize: sanitizeHexDigits,
2178
+ validate: (v2) => sanitizeHexDigits(v2).length > 0,
1806
2179
  maxLength: 6,
1807
2180
  inputMode: "text",
1808
2181
  ariaLabel: `Edit hex color, ${hex}`,
@@ -1813,7 +2186,7 @@ function ColorPicker({ value, onChange }) {
1813
2186
  onChange(parsed);
1814
2187
  return "";
1815
2188
  }
1816
- return normalizeHexDigits(pasted);
2189
+ return sanitizeHexDigits(pasted);
1817
2190
  }
1818
2191
  }
1819
2192
  ) : mode === "hsl" ? /* @__PURE__ */ jsxs("div", { className: "colorPickerTriplet", "aria-label": "HSL values", children: [
@@ -2050,20 +2423,15 @@ function hexDigits$1(hex) {
2050
2423
  const n2 = normalizeHex(hex);
2051
2424
  return n2.slice(1);
2052
2425
  }
2053
- function isValidSixHex$1(d2) {
2054
- return /^[0-9a-fA-F]{6}$/.test(d2);
2055
- }
2056
- function sanitizeHex$1(input) {
2057
- return input.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
2058
- }
2059
2426
  function ColorRow({ label = "Color", value, onChange, disabled: disabledProp }) {
2060
2427
  const categoryDisabled = useCategoryDisabled();
2061
2428
  const disabled = Boolean(disabledProp || categoryDisabled);
2062
2429
  const [hovered, setHovered] = useState(false);
2063
2430
  const hex = normalizeHex(value);
2064
2431
  const handleCommit = (sanitized) => {
2065
- if (isValidSixHex$1(sanitized)) {
2066
- onChange(`#${sanitized.toUpperCase()}`);
2432
+ const expanded = expandHexDigitsToSix(sanitized);
2433
+ if (expanded) {
2434
+ onChange(`#${expanded}`);
2067
2435
  }
2068
2436
  };
2069
2437
  const handlePaste = (pasted) => {
@@ -2085,8 +2453,8 @@ function ColorRow({ label = "Color", value, onChange, disabled: disabledProp })
2085
2453
  value: hexDigits$1(hex),
2086
2454
  prefix: "#",
2087
2455
  onCommit: handleCommit,
2088
- sanitize: sanitizeHex$1,
2089
- validate: isValidSixHex$1,
2456
+ sanitize: sanitizeHexDigits,
2457
+ validate: (v2) => sanitizeHexDigits(v2).length > 0,
2090
2458
  maxLength: 6,
2091
2459
  inputMode: "text",
2092
2460
  ariaLabel: `Edit hex color, ${hex}`,
@@ -2258,9 +2626,6 @@ function generateAestheticGradient(stopCount) {
2258
2626
  function hexDigits(hex) {
2259
2627
  return normalizeHex(hex).slice(1);
2260
2628
  }
2261
- function isValidSixHex(d2) {
2262
- return /^[0-9a-fA-F]{6}$/.test(d2);
2263
- }
2264
2629
  function stopsToGradient(stops, angle = 90) {
2265
2630
  if (stops.length === 0) return "linear-gradient(90deg, #808080, #808080)";
2266
2631
  if (stops.length === 1) {
@@ -2295,19 +2660,16 @@ const DEFAULT_GRADIENT_STOPS = [
2295
2660
  { color: "#42C0B0" },
2296
2661
  { color: "#BAC9C7" }
2297
2662
  ];
2298
- function sanitizeHex(input) {
2299
- return input.replace(/[^0-9a-fA-F]/g, "").slice(0, 6);
2300
- }
2301
2663
  function StopRow({ index, stop, canDelete, onColorChange, onDelete }) {
2302
2664
  const hex = normalizeHex(stop.color);
2303
2665
  const handleCommit = (val) => {
2304
- const d2 = sanitizeHex(val);
2305
- if (isValidSixHex(d2)) {
2306
- onColorChange(`#${d2.toUpperCase()}`);
2666
+ const expanded = expandHexDigitsToSix(val);
2667
+ if (expanded) {
2668
+ onColorChange(`#${expanded}`);
2307
2669
  }
2308
2670
  };
2309
2671
  const handlePaste = (pasted) => {
2310
- return sanitizeHex(pasted);
2672
+ return sanitizeHexDigits(pasted);
2311
2673
  };
2312
2674
  return /* @__PURE__ */ jsxs("div", { className: "gradientPickerStop", children: [
2313
2675
  /* @__PURE__ */ jsxs("div", { className: "gradientPickerStopColorHex", children: [
@@ -2338,8 +2700,8 @@ function StopRow({ index, stop, canDelete, onColorChange, onDelete }) {
2338
2700
  value: hexDigits(hex),
2339
2701
  prefix: "#",
2340
2702
  onCommit: handleCommit,
2341
- sanitize: sanitizeHex,
2342
- validate: isValidSixHex,
2703
+ sanitize: sanitizeHexDigits,
2704
+ validate: (v2) => sanitizeHexDigits(v2).length > 0,
2343
2705
  maxLength: 6,
2344
2706
  inputMode: "text",
2345
2707
  ariaLabel: "Hex color without hash",