gridsmith-ui 0.9.0 → 0.9.1

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.
@@ -0,0 +1,950 @@
1
+ import { useReturnFocus, focusInitial, useOverlayLayer, useFocusTrap, OVERLAY_CLOSE_ATTR, useNonModalDialog } from './chunk-RLUK575N.js';
2
+ import { useBreakpoint, readBreakpointPx } from './chunk-CRQ6KCRD.js';
3
+ import { Tooltip, useTextOverflow } from './chunk-CQADS7JZ.js';
4
+ import { useLatest } from './chunk-WH2FUFAC.js';
5
+ import { Icon } from './chunk-X4CNZFWC.js';
6
+ import { cn, lockBodyScroll, unlockBodyScroll, GLASS_OVERLAY } from './chunk-4KVFH5KC.js';
7
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
8
+ import { createContext, forwardRef, useContext, useState, useCallback, useRef, useEffect, isValidElement, useMemo, useId, useLayoutEffect } from 'react';
9
+ import { createPortal } from 'react-dom';
10
+
11
+ var sizes = {
12
+ sm: "h-4 w-4 border-[1.5px]",
13
+ md: "h-6 w-6 border-2",
14
+ lg: "h-8 w-8 border-[2.5px]"
15
+ };
16
+ function Spinner({ size = "md", color = "default", label = "Loading", className }) {
17
+ return /* @__PURE__ */ jsx(
18
+ "span",
19
+ {
20
+ role: "status",
21
+ "aria-label": label,
22
+ className: cn(
23
+ "inline-block rounded-[var(--ds-radius-full)] animate-spin",
24
+ color === "inherit" ? "border-current/30 border-t-current" : "border-[var(--ds-bg-emphasis)] border-t-[var(--ds-accent-primary)]",
25
+ sizes[size],
26
+ className
27
+ )
28
+ }
29
+ );
30
+ }
31
+ var ControlSizeContext = createContext(null);
32
+ var ButtonSizeContext = ControlSizeContext;
33
+ var variantStyles = {
34
+ default: "bg-[var(--ds-accent-primary)] [background-image:var(--ds-gradient)] [background-origin:border-box] text-[var(--ds-text-on-primary)] border-transparent hover:bg-[var(--ds-accent-primary-hover)] hover:brightness-110",
35
+ secondary: "bg-[var(--ds-bg-surface)] text-[var(--ds-text-primary)] border-[var(--ds-border-default)] hover:bg-[var(--ds-bg-muted)]",
36
+ tertiary: "bg-[var(--ds-bg-muted)] text-[var(--ds-text-primary)] border-transparent hover:brightness-95",
37
+ outline: "bg-[var(--ds-input-bg)] text-[var(--ds-text-primary)] border-[var(--ds-border-default)] hover:bg-[var(--ds-bg-muted)]",
38
+ ghost: "bg-transparent text-[var(--ds-text-primary)] border-transparent hover:bg-[var(--ds-bg-muted)] shadow-none",
39
+ destructive: "bg-[var(--ds-status-error)] text-[var(--ds-text-on-error)] border-transparent hover:opacity-90",
40
+ success: "bg-[var(--ds-status-success)] text-[var(--ds-text-on-success)] border-transparent hover:opacity-90",
41
+ warning: "bg-[var(--ds-status-warning)] text-[var(--ds-text-on-warning)] border-transparent hover:opacity-90",
42
+ link: "bg-transparent text-[var(--ds-accent-primary)] hover:text-[var(--ds-accent-primary-hover)] hover:underline"
43
+ };
44
+ var sizeStyles = {
45
+ // xs / icon-xs: dense tool chrome (builder toolbars, inline row actions).
46
+ xs: "h-ds-6 px-ds-3 py-0 text-xs",
47
+ sm: "h-[var(--ds-control-height-sm)] px-ds-4 py-ds-1 text-xs",
48
+ md: "h-[var(--ds-control-height-md)] px-ds-6 py-ds-2 text-sm",
49
+ lg: "h-[var(--ds-control-height-lg)] px-ds-8 py-ds-3 text-base",
50
+ "icon-xs": "h-ds-6 w-ds-6 p-0",
51
+ "icon-sm": "h-[var(--ds-control-height-sm)] w-[var(--ds-control-height-sm)] p-0",
52
+ icon: "h-[var(--ds-control-height-md)] w-[var(--ds-control-height-md)] p-0",
53
+ "icon-lg": "h-[var(--ds-control-height-lg)] w-[var(--ds-control-height-lg)] p-0"
54
+ };
55
+ var linkSizeStyles = {
56
+ xs: "text-xs",
57
+ sm: "text-xs",
58
+ md: "text-sm",
59
+ lg: "text-base",
60
+ "icon-xs": "text-xs",
61
+ "icon-sm": "text-xs",
62
+ icon: "text-sm",
63
+ "icon-lg": "text-base"
64
+ };
65
+ var iconPaddingReduce = {
66
+ xs: 2,
67
+ sm: 2,
68
+ md: 4,
69
+ lg: 6
70
+ };
71
+ var HOVER_SCALE_CAP = { "--_btn-scale": "min(var(--ds-hover-scale), 1.02)" };
72
+ var Button = forwardRef(function Button2({ variant = "default", size: sizeProp, iconLeft, iconRight, loading, className, children, disabled, style, onClick, ...props }, ref) {
73
+ const contextSize = useContext(ControlSizeContext);
74
+ const size = sizeProp ?? contextSize ?? "md";
75
+ const isLink = variant === "link";
76
+ const spinnerSize = size === "lg" || size === "icon-lg" ? "md" : "sm";
77
+ let paddingStyle;
78
+ const isIconSize = size === "icon" || size === "icon-xs" || size === "icon-sm" || size === "icon-lg";
79
+ if (!isIconSize && !isLink && (iconLeft || iconRight)) {
80
+ const reduce = iconPaddingReduce[size];
81
+ paddingStyle = {};
82
+ if (iconLeft) paddingStyle.paddingLeft = `calc(${getCssPadding(size)} - ${reduce}px)`;
83
+ if (iconRight) paddingStyle.paddingRight = `calc(${getCssPadding(size)} - ${reduce}px)`;
84
+ }
85
+ const isLoading = loading && !disabled;
86
+ const isDisabled = !!disabled;
87
+ const handleClick = isLoading ? (e) => {
88
+ e.preventDefault();
89
+ } : onClick;
90
+ const ariaLabel = props["aria-label"];
91
+ const btn = /* @__PURE__ */ jsxs(
92
+ "button",
93
+ {
94
+ ref,
95
+ "data-variant": variant,
96
+ className: cn(
97
+ "relative inline-flex items-center justify-center gap-ds-2 whitespace-nowrap font-medium",
98
+ isLink ? cn(
99
+ "px-0 py-ds-0.5 h-auto",
100
+ linkSizeStyles[size],
101
+ "transition-colors duration-[var(--ds-transition-speed)]",
102
+ "focus-visible:outline-2 focus-visible:outline-[var(--ds-accent-primary)] focus-visible:outline-offset-2",
103
+ "cursor-pointer",
104
+ "active:text-[var(--ds-accent-primary-active)]"
105
+ ) : cn(
106
+ "border-[length:var(--ds-border-width)]",
107
+ "rounded-[var(--ds-radius-md)]",
108
+ "shadow-[var(--ds-shadow-button)]",
109
+ "hover:scale-[var(--_btn-scale)] hover:-translate-y-[var(--ds-hover-lift)]",
110
+ "active:shadow-[var(--ds-shadow-button-pressed)] active:translate-x-[var(--ds-press-depth)] active:translate-y-[var(--ds-press-depth)]",
111
+ "[transition:var(--ds-transition-interactive)]",
112
+ "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
113
+ "cursor-pointer disabled:cursor-not-allowed disabled:opacity-disabled",
114
+ sizeStyles[size]
115
+ ),
116
+ variantStyles[variant],
117
+ isLoading && !isLink && "cursor-wait pointer-events-none",
118
+ className
119
+ ),
120
+ disabled: isDisabled,
121
+ "aria-busy": isLoading || void 0,
122
+ "aria-disabled": isLoading && !disabled ? true : void 0,
123
+ onClick: handleClick,
124
+ style: isLink ? style : { ...HOVER_SCALE_CAP, ...paddingStyle, ...style },
125
+ ...props,
126
+ children: [
127
+ loading && !isLink && /* @__PURE__ */ jsx("span", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsx(Spinner, { size: spinnerSize, color: "inherit" }) }),
128
+ /* @__PURE__ */ jsxs("span", { className: cn("inline-flex items-center gap-ds-2 min-w-0", loading && !isLink && "invisible"), children: [
129
+ iconLeft,
130
+ children,
131
+ iconRight
132
+ ] })
133
+ ]
134
+ }
135
+ );
136
+ if (isIconSize && ariaLabel) {
137
+ return /* @__PURE__ */ jsx(Tooltip, { content: ariaLabel, children: btn });
138
+ }
139
+ return btn;
140
+ });
141
+ function getCssPadding(size) {
142
+ switch (size) {
143
+ case "xs":
144
+ return "var(--ds-space-3)";
145
+ case "sm":
146
+ return "var(--ds-space-4)";
147
+ case "md":
148
+ return "var(--ds-space-6)";
149
+ case "lg":
150
+ return "var(--ds-space-8)";
151
+ }
152
+ }
153
+ function transitionMs(el) {
154
+ const cs = getComputedStyle(el);
155
+ const parse = (list) => list.split(",").map((raw) => {
156
+ const s = raw.trim();
157
+ if (!s) return 0;
158
+ const n = parseFloat(s);
159
+ if (Number.isNaN(n)) return 0;
160
+ return s.endsWith("ms") ? n : n * 1e3;
161
+ });
162
+ const durations = parse(cs.transitionDuration || "0s");
163
+ const delays = parse(cs.transitionDelay || "0s");
164
+ let max = 0;
165
+ durations.forEach((d, i) => {
166
+ const total = d + (delays[i] ?? delays[0] ?? 0);
167
+ if (total > max) max = total;
168
+ });
169
+ return max;
170
+ }
171
+ function useCloseSettle(open, elRef, onSettled) {
172
+ const cb = useLatest(onSettled);
173
+ useEffect(() => {
174
+ if (open) return;
175
+ const el = elRef.current;
176
+ const ms = el ? transitionMs(el) : 0;
177
+ let done = false;
178
+ const finish = () => {
179
+ if (done) return;
180
+ done = true;
181
+ cb.current();
182
+ };
183
+ if (!el || ms === 0) {
184
+ const t2 = setTimeout(finish, 0);
185
+ return () => clearTimeout(t2);
186
+ }
187
+ const onEnd = (e) => {
188
+ if (e.target === el) finish();
189
+ };
190
+ el.addEventListener("transitionend", onEnd);
191
+ const t = setTimeout(finish, ms + 50);
192
+ return () => {
193
+ el.removeEventListener("transitionend", onEnd);
194
+ clearTimeout(t);
195
+ };
196
+ }, [open, elRef, cb]);
197
+ }
198
+ function Sheet({ open, onClose, title, children, footer, confirm, snapPoints = [0.5, 0.92], container, flush, keepMounted = false, id, labelledBy }) {
199
+ const titleId = useId();
200
+ const panelRef = useRef(null);
201
+ const [mounted, setMounted] = useState(open);
202
+ const [hidden, setHidden] = useState(!open);
203
+ const [snapIdx, setSnapIdx] = useState(0);
204
+ const [dragOffset, setDragOffset] = useState(0);
205
+ const [dragging, setDragging] = useState(false);
206
+ const dragStartY = useRef(0);
207
+ const startHeight = useRef(0);
208
+ const onCloseRef = useLatest(onClose);
209
+ const [prevOpen, setPrevOpen] = useState(open);
210
+ if (open !== prevOpen) {
211
+ setPrevOpen(open);
212
+ if (open) {
213
+ setMounted(true);
214
+ setHidden(false);
215
+ setSnapIdx(0);
216
+ setDragOffset(0);
217
+ }
218
+ }
219
+ const viewportHeight = useCallback(
220
+ () => container instanceof HTMLElement ? container.clientHeight : window.innerHeight,
221
+ [container]
222
+ );
223
+ useCloseSettle(open, panelRef, () => {
224
+ if (keepMounted) setHidden(true);
225
+ else setMounted(false);
226
+ });
227
+ useReturnFocus(open);
228
+ useEffect(() => {
229
+ if (!open || !mounted) return;
230
+ const raf = requestAnimationFrame(() => focusInitial(panelRef.current));
231
+ return () => cancelAnimationFrame(raf);
232
+ }, [open, mounted]);
233
+ const isTopLayer = useOverlayLayer(open);
234
+ useFocusTrap(panelRef, open, isTopLayer);
235
+ useEffect(() => {
236
+ if (!open) return;
237
+ const onKeyDown = (e) => {
238
+ if (e.key === "Escape" && isTopLayer()) onCloseRef.current();
239
+ };
240
+ document.addEventListener("keydown", onKeyDown);
241
+ if (!container) lockBodyScroll();
242
+ return () => {
243
+ document.removeEventListener("keydown", onKeyDown);
244
+ if (!container) unlockBodyScroll();
245
+ };
246
+ }, [open, container, onCloseRef, isTopLayer]);
247
+ const handlePointerDown = useCallback((e) => {
248
+ setDragging(true);
249
+ dragStartY.current = e.clientY;
250
+ startHeight.current = (snapPoints[snapIdx] ?? snapPoints[0]) * viewportHeight();
251
+ setDragOffset(0);
252
+ e.target.setPointerCapture(e.pointerId);
253
+ }, [snapIdx, snapPoints, viewportHeight]);
254
+ const handlePointerMove = useCallback((e) => {
255
+ if (!dragging) return;
256
+ const delta = dragStartY.current - e.clientY;
257
+ setDragOffset(delta);
258
+ }, [dragging]);
259
+ const handlePointerUp = useCallback(() => {
260
+ if (!dragging) return;
261
+ setDragging(false);
262
+ const currentHeightPx = startHeight.current + dragOffset;
263
+ const currentFraction = currentHeightPx / viewportHeight();
264
+ if (currentFraction < snapPoints[0] * 0.6) {
265
+ onClose();
266
+ setDragOffset(0);
267
+ return;
268
+ }
269
+ let bestIdx = 0;
270
+ let bestDist = Math.abs(currentFraction - snapPoints[0]);
271
+ for (let i = 1; i < snapPoints.length; i++) {
272
+ const dist = Math.abs(currentFraction - snapPoints[i]);
273
+ if (dist < bestDist) {
274
+ bestDist = dist;
275
+ bestIdx = i;
276
+ }
277
+ }
278
+ setSnapIdx(bestIdx);
279
+ setDragOffset(0);
280
+ }, [dragging, dragOffset, snapPoints, onClose, viewportHeight]);
281
+ if (!mounted) return null;
282
+ const baseHeight = (snapPoints[snapIdx] ?? snapPoints[0]) * 100;
283
+ const heightUnit = container ? "%" : "vh";
284
+ const dragPx = dragging ? dragOffset : 0;
285
+ const positionClass = container ? "absolute" : "fixed";
286
+ return createPortal(
287
+ /* @__PURE__ */ jsxs(
288
+ "div",
289
+ {
290
+ hidden: keepMounted && hidden ? true : void 0,
291
+ className: cn(
292
+ `${positionClass} inset-0 z-[var(--ds-z-modal)] transition-opacity duration-[var(--ds-transition-speed)]`,
293
+ open ? "pointer-events-auto" : "pointer-events-none"
294
+ ),
295
+ children: [
296
+ /* @__PURE__ */ jsx(
297
+ "div",
298
+ {
299
+ className: cn(
300
+ "absolute inset-0 bg-black/50 transition-opacity duration-[var(--ds-transition-speed)]",
301
+ open ? "opacity-100" : "opacity-0"
302
+ ),
303
+ onClick: onClose
304
+ }
305
+ ),
306
+ /* @__PURE__ */ jsxs(
307
+ "div",
308
+ {
309
+ ref: panelRef,
310
+ id,
311
+ role: "dialog",
312
+ "data-gs": "sheet",
313
+ "aria-modal": "true",
314
+ "aria-labelledby": title ? titleId : labelledBy,
315
+ "data-ds-sheet": "",
316
+ className: cn(
317
+ `${positionClass} bottom-0 left-0 right-0 z-10 flex flex-col`,
318
+ "bg-[var(--ds-bg-surface)] text-[var(--ds-text-primary)]",
319
+ GLASS_OVERLAY,
320
+ "border-t-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
321
+ "rounded-t-[var(--ds-radius-xl)]",
322
+ !dragging && "transition-[transform,height] duration-[var(--ds-transition-speed)] ease-[var(--ds-transition-ease)]",
323
+ open ? "translate-y-0" : "translate-y-full"
324
+ ),
325
+ style: {
326
+ containerType: "inline-size",
327
+ height: dragPx ? `calc(${baseHeight}${heightUnit} + ${dragPx}px)` : `${baseHeight}${heightUnit}`
328
+ },
329
+ children: [
330
+ /* @__PURE__ */ jsx(
331
+ "div",
332
+ {
333
+ className: "flex justify-center pt-ds-2 pb-ds-1 cursor-grab active:cursor-grabbing touch-none",
334
+ onPointerDown: handlePointerDown,
335
+ onPointerMove: handlePointerMove,
336
+ onPointerUp: handlePointerUp,
337
+ onPointerCancel: handlePointerUp,
338
+ children: /* @__PURE__ */ jsx("div", { className: "w-10 h-1 rounded-[var(--ds-radius-full)] bg-[var(--ds-border-default)]" })
339
+ }
340
+ ),
341
+ title && /* @__PURE__ */ jsxs(
342
+ "div",
343
+ {
344
+ className: "flex items-center justify-between shrink-0 border-b-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
345
+ style: { padding: "var(--ds-space-2) var(--ds-space-5)" },
346
+ children: [
347
+ /* @__PURE__ */ jsx("h2", { id: titleId, className: "font-[family-name:var(--ds-font-display)] text-lg font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)] truncate", children: title }),
348
+ /* @__PURE__ */ jsx(
349
+ "button",
350
+ {
351
+ onClick: onClose,
352
+ ...{ [OVERLAY_CLOSE_ATTR]: "" },
353
+ className: "text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] transition-colors cursor-pointer",
354
+ "aria-label": "Close",
355
+ children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 18 })
356
+ }
357
+ )
358
+ ]
359
+ }
360
+ ),
361
+ /* @__PURE__ */ jsx(
362
+ "div",
363
+ {
364
+ className: "flex-1 overflow-y-auto",
365
+ style: flush ? void 0 : { padding: "var(--ds-space-5)" },
366
+ children
367
+ }
368
+ ),
369
+ confirm,
370
+ footer && /* @__PURE__ */ jsx(
371
+ "div",
372
+ {
373
+ className: "shrink-0 flex items-center justify-end gap-ds-2 border-t-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
374
+ style: { padding: "var(--ds-space-3) var(--ds-space-5)" },
375
+ children: footer
376
+ }
377
+ )
378
+ ]
379
+ }
380
+ )
381
+ ]
382
+ }
383
+ ),
384
+ container ?? document.body
385
+ );
386
+ }
387
+ var STEP = 16;
388
+ var BIG_STEP = 64;
389
+ function clamp(v, min, max) {
390
+ return Math.min(Math.max(v, min), max);
391
+ }
392
+ function ResizeEdge({ edge, width, min, max, defaultWidth, onResize, label, onDraggingChange, className }) {
393
+ const [dragging, setDragging] = useState(false);
394
+ const start = useRef(null);
395
+ const latestOnResize = useLatest(onResize);
396
+ const direction = edge === "left" ? -1 : 1;
397
+ const onPointerDown = useCallback(
398
+ (e) => {
399
+ if (e.button !== 0) return;
400
+ e.preventDefault();
401
+ start.current = { x: e.clientX, width };
402
+ setDragging(true);
403
+ e.currentTarget.setPointerCapture(e.pointerId);
404
+ },
405
+ [width]
406
+ );
407
+ const onPointerMove = useCallback(
408
+ (e) => {
409
+ if (!start.current) return;
410
+ const next = clamp(start.current.width + direction * (e.clientX - start.current.x), min, max());
411
+ latestOnResize.current(next);
412
+ },
413
+ [direction, min, max, latestOnResize]
414
+ );
415
+ const endDrag = useCallback(() => {
416
+ start.current = null;
417
+ setDragging(false);
418
+ }, []);
419
+ const latestOnDraggingChange = useLatest(onDraggingChange);
420
+ useEffect(() => {
421
+ latestOnDraggingChange.current?.(dragging);
422
+ }, [dragging, latestOnDraggingChange]);
423
+ useEffect(() => {
424
+ if (!dragging) return;
425
+ const onKey = (e) => {
426
+ if (e.key !== "Escape" || !start.current) return;
427
+ latestOnResize.current(start.current.width);
428
+ endDrag();
429
+ };
430
+ document.addEventListener("keydown", onKey);
431
+ return () => document.removeEventListener("keydown", onKey);
432
+ }, [dragging, endDrag, latestOnResize]);
433
+ const onKeyDown = (e) => {
434
+ const step = e.shiftKey ? BIG_STEP : STEP;
435
+ let next = null;
436
+ if (e.key === "ArrowLeft") next = width - direction * step;
437
+ else if (e.key === "ArrowRight") next = width + direction * step;
438
+ else if (e.key === "Home") next = min;
439
+ else if (e.key === "End") next = max();
440
+ else if (e.key === "Enter") next = defaultWidth;
441
+ if (next === null) return;
442
+ e.preventDefault();
443
+ onResize(clamp(next, min, max()));
444
+ };
445
+ return /* @__PURE__ */ jsx(
446
+ "div",
447
+ {
448
+ role: "separator",
449
+ tabIndex: 0,
450
+ "aria-orientation": "vertical",
451
+ "aria-label": label,
452
+ "aria-valuemin": min,
453
+ "aria-valuemax": max(),
454
+ "aria-valuenow": Math.round(width),
455
+ "data-resize-edge": edge,
456
+ "data-dragging": dragging || void 0,
457
+ onPointerDown,
458
+ onPointerMove,
459
+ onPointerUp: endDrag,
460
+ onPointerCancel: endDrag,
461
+ onDoubleClick: () => onResize(clamp(defaultWidth, min, max())),
462
+ onKeyDown,
463
+ className: cn(
464
+ // A narrow absolutely-positioned strip over the edge — never a flex child that takes width.
465
+ "group/edge absolute top-0 bottom-0 z-10 w-ds-2 cursor-col-resize touch-none select-none outline-none",
466
+ edge === "left" ? "-left-ds-1" : "-right-ds-1",
467
+ className
468
+ ),
469
+ children: /* @__PURE__ */ jsx(
470
+ "div",
471
+ {
472
+ "aria-hidden": "true",
473
+ className: cn(
474
+ "absolute top-0 bottom-0 left-1/2 w-px -translate-x-1/2 transition-[width,background-color] duration-[var(--ds-transition-speed)]",
475
+ "group-hover/edge:w-[3px] group-hover/edge:bg-[var(--ds-accent-primary)]",
476
+ "group-focus-visible/edge:w-[3px] group-focus-visible/edge:bg-[var(--ds-accent-primary)]",
477
+ dragging && "w-[3px] bg-[var(--ds-accent-primary)]"
478
+ )
479
+ }
480
+ )
481
+ }
482
+ );
483
+ }
484
+ var remembered = /* @__PURE__ */ new Map();
485
+ function useRememberedWidth(key) {
486
+ const [width, setWidth] = useState(() => key ? remembered.get(key) ?? null : null);
487
+ const remember = useCallback(
488
+ (w) => {
489
+ setWidth(w);
490
+ if (!key) return;
491
+ if (w === null) remembered.delete(key);
492
+ else remembered.set(key, w);
493
+ },
494
+ [key]
495
+ );
496
+ return [width, remember];
497
+ }
498
+ function pxOf(length) {
499
+ if (!length) return null;
500
+ const m = /^(\d+(?:\.\d+)?)px$/.exec(length.trim());
501
+ return m ? parseFloat(m[1]) : null;
502
+ }
503
+ function defaultMaxWidth(container) {
504
+ const base = container instanceof HTMLElement ? container.clientWidth : typeof window !== "undefined" ? window.innerWidth : 1200;
505
+ return Math.round(base * 0.6);
506
+ }
507
+ function useDirtyGuard(dirty, onClose, onDiscard) {
508
+ const [wantsConfirm, setWantsConfirm] = useState(false);
509
+ const confirming = wantsConfirm && !!dirty;
510
+ const requestClose = useCallback(() => {
511
+ if (!dirty) {
512
+ onClose();
513
+ return;
514
+ }
515
+ setWantsConfirm((c) => !c);
516
+ }, [dirty, onClose]);
517
+ const keep = useCallback(() => setWantsConfirm(false), []);
518
+ const discard = useCallback(() => {
519
+ setWantsConfirm(false);
520
+ onDiscard?.();
521
+ onClose();
522
+ }, [onClose, onDiscard]);
523
+ return { confirming, requestClose, keep, discard };
524
+ }
525
+ function DiscardConfirm({ open, onKeep, onDiscard }) {
526
+ const keepRef = useRef(null);
527
+ const returnTo = useRef(null);
528
+ const overlay = useRef(null);
529
+ useEffect(() => {
530
+ if (!open) return;
531
+ const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;
532
+ overlay.current = keepRef.current?.closest("[role='dialog'], [role='alertdialog']") ?? null;
533
+ returnTo.current = active && active !== document.body && overlay.current?.contains(active) ? active : null;
534
+ keepRef.current?.focus();
535
+ return () => {
536
+ if (document.activeElement !== document.body) return;
537
+ const back = returnTo.current;
538
+ if (back && back.isConnected) {
539
+ back.focus();
540
+ return;
541
+ }
542
+ const first = overlay.current?.querySelector("button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])");
543
+ (first ?? overlay.current)?.focus();
544
+ };
545
+ }, [open]);
546
+ if (!open) return null;
547
+ return /* @__PURE__ */ jsxs(
548
+ "div",
549
+ {
550
+ role: "group",
551
+ "aria-label": "Unsaved changes",
552
+ "data-discard-confirm": "",
553
+ className: "shrink-0 flex flex-wrap items-center justify-between gap-ds-3 border-t-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)] bg-[var(--ds-status-warning-bg)]",
554
+ style: { padding: "var(--ds-space-3) var(--ds-space-5)" },
555
+ children: [
556
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-[var(--ds-text-primary)]", children: "You have unsaved changes." }),
557
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2", children: [
558
+ /* @__PURE__ */ jsx(Button, { ref: keepRef, variant: "secondary", size: "sm", onClick: onKeep, children: "Keep editing" }),
559
+ /* @__PURE__ */ jsx(Button, { variant: "destructive", size: "sm", onClick: onDiscard, children: "Discard changes" })
560
+ ] })
561
+ ]
562
+ }
563
+ );
564
+ }
565
+ var DrawerFooterCtx = createContext(null);
566
+ var slideClasses = {
567
+ right: {
568
+ panel: "top-0 right-0 h-full",
569
+ open: "translate-x-0",
570
+ closed: "translate-x-full"
571
+ },
572
+ left: {
573
+ panel: "top-0 left-0 h-full",
574
+ open: "translate-x-0",
575
+ closed: "-translate-x-full"
576
+ },
577
+ bottom: {
578
+ panel: "bottom-0 left-0 right-0",
579
+ open: "translate-y-0",
580
+ closed: "translate-y-full"
581
+ }
582
+ };
583
+ var borderByPosition = {
584
+ right: "border-l-[length:var(--ds-border-width)] border-l-[var(--ds-border-default)]",
585
+ left: "border-r-[length:var(--ds-border-width)] border-r-[var(--ds-border-default)]",
586
+ bottom: "border-t-[length:var(--ds-border-width)] border-t-[var(--ds-border-default)]"
587
+ };
588
+ function Drawer(props) {
589
+ const { position = "right", mobile, snapPoints, open, onClose, title, children, footer, container, flush, keepMounted, id, labelledBy, dirty, onDiscard } = props;
590
+ const isPhone = useBreakpoint("sm");
591
+ const mobileMode = mobile ?? (position === "left" ? "panel" : "sheet");
592
+ const guard = useDirtyGuard(dirty, onClose, onDiscard);
593
+ const confirm = /* @__PURE__ */ jsx(DiscardConfirm, { open: guard.confirming, onKeep: guard.keep, onDiscard: guard.discard });
594
+ const [actionsMap, setActionsMap] = useState(/* @__PURE__ */ new Map());
595
+ const registerActions = useCallback((formId, acts) => {
596
+ setActionsMap((prev) => new Map(prev).set(formId, acts));
597
+ }, []);
598
+ const unregisterActions = useCallback((formId) => {
599
+ setActionsMap((prev) => {
600
+ const next = new Map(prev);
601
+ next.delete(formId);
602
+ return next;
603
+ });
604
+ }, []);
605
+ const registeredActions = actionsMap.size > 0 ? /* @__PURE__ */ jsx(Fragment, { children: Array.from(actionsMap.values()) }) : null;
606
+ const effectiveFooter = footer ?? registeredActions;
607
+ const footerCtx = useMemo(
608
+ () => ({ registerActions, unregisterActions }),
609
+ [registerActions, unregisterActions]
610
+ );
611
+ if (isPhone && mobileMode === "sheet") {
612
+ return /* @__PURE__ */ jsx(DrawerFooterCtx, { value: footerCtx, children: /* @__PURE__ */ jsx(Sheet, { open, onClose: guard.requestClose, title, footer: effectiveFooter, confirm, container, flush, snapPoints, keepMounted, id, labelledBy, children }) });
613
+ }
614
+ return /* @__PURE__ */ jsx(DrawerFooterCtx, { value: footerCtx, children: /* @__PURE__ */ jsx(DrawerPanel, { ...props, footer: effectiveFooter, onClose: guard.requestClose, confirm }) });
615
+ }
616
+ function DrawerPanel({ open, onClose, position = "right", width = "400px", title, children, footer, container, flush, lightBackdrop, topOffset, modal = true, keepMounted = false, id, labelledBy, resizable = false, maxWidth, confirm }) {
617
+ const nonModal = !modal;
618
+ const [chosen, remember] = useRememberedWidth(id ?? labelledBy);
619
+ const canResize = nonModal && resizable && position !== "bottom";
620
+ const effectiveWidth = canResize && chosen !== null ? `${chosen}px` : width;
621
+ const minPx = pxOf(width) ?? 320;
622
+ const maxPx = () => pxOf(maxWidth) ?? defaultMaxWidth(container instanceof Element ? container : null);
623
+ const titleId = useId();
624
+ const { ref: titleOverflowRef, isTruncated: titleTruncated } = useTextOverflow({ lines: 1 });
625
+ const isMobile = useBreakpoint("sm");
626
+ const panelRef = useRef(null);
627
+ const [mounted, setMounted] = useState(open);
628
+ const [hidden, setHidden] = useState(!open);
629
+ const onCloseRef = useLatest(onClose);
630
+ if (open && !mounted) setMounted(true);
631
+ if (open && hidden) setHidden(false);
632
+ const isTopLayer = useOverlayLayer(open);
633
+ useEffect(() => {
634
+ if (!open || nonModal) return;
635
+ const onKeyDown = (e) => {
636
+ if (e.key === "Escape" && isTopLayer()) onCloseRef.current();
637
+ };
638
+ document.addEventListener("keydown", onKeyDown);
639
+ if (!container) lockBodyScroll();
640
+ return () => {
641
+ document.removeEventListener("keydown", onKeyDown);
642
+ if (!container) unlockBodyScroll();
643
+ };
644
+ }, [open, nonModal, container, onCloseRef, isTopLayer]);
645
+ useReturnFocus(open && !nonModal);
646
+ useFocusTrap(panelRef, open && !topOffset && !nonModal, isTopLayer);
647
+ useEffect(() => {
648
+ if (!open || !mounted || nonModal) return;
649
+ const raf = requestAnimationFrame(() => focusInitial(panelRef.current));
650
+ return () => cancelAnimationFrame(raf);
651
+ }, [open, mounted, nonModal]);
652
+ useNonModalDialog(panelRef, open && mounted && nonModal, onClose, isTopLayer);
653
+ useCloseSettle(open, panelRef, () => {
654
+ if (keepMounted) setHidden(true);
655
+ else setMounted(false);
656
+ });
657
+ if (!mounted) return null;
658
+ const slide = slideClasses[position];
659
+ const isHorizontal = position === "left" || position === "right";
660
+ const positionClass = container ? "absolute" : "fixed";
661
+ return createPortal(
662
+ /* @__PURE__ */ jsxs(
663
+ "div",
664
+ {
665
+ hidden: keepMounted && hidden ? true : void 0,
666
+ className: cn(
667
+ `${positionClass} transition-opacity duration-[var(--ds-transition-speed)]`,
668
+ // A non-modal panel never intercepts the page: the wrapper is inert to the
669
+ // pointer and only the panel itself accepts events.
670
+ nonModal ? "z-[var(--ds-z-overlay)] pointer-events-none" : cn("z-[var(--ds-z-modal)]", open ? "pointer-events-auto" : "pointer-events-none"),
671
+ topOffset ? "left-0 right-0 bottom-0" : "inset-0"
672
+ ),
673
+ style: topOffset ? { top: topOffset } : void 0,
674
+ children: [
675
+ !nonModal && /* @__PURE__ */ jsx(
676
+ "div",
677
+ {
678
+ className: cn(
679
+ "absolute inset-0 transition-opacity duration-[var(--ds-transition-speed)]",
680
+ lightBackdrop ? "bg-black/15" : cn("bg-black/50", "[backdrop-filter:blur(calc(var(--ds-glass-blur)*0.3))] [-webkit-backdrop-filter:blur(calc(var(--ds-glass-blur)*0.3))]"),
681
+ open ? "opacity-100" : "opacity-0"
682
+ ),
683
+ onClick: onClose
684
+ }
685
+ ),
686
+ /* @__PURE__ */ jsxs(
687
+ "div",
688
+ {
689
+ ref: panelRef,
690
+ id,
691
+ role: "dialog",
692
+ "data-gs": "drawer",
693
+ "aria-modal": nonModal ? void 0 : "true",
694
+ "aria-labelledby": title ? titleId : labelledBy,
695
+ style: {
696
+ containerType: "inline-size",
697
+ width: isHorizontal ? effectiveWidth : void 0,
698
+ maxWidth: isHorizontal ? "100%" : void 0,
699
+ height: isMobile ? "100%" : void 0,
700
+ maxHeight: !isMobile && !isHorizontal ? "80vh" : void 0
701
+ },
702
+ className: cn(
703
+ `${positionClass} z-10 flex flex-col`,
704
+ nonModal && "pointer-events-auto",
705
+ "bg-[var(--ds-bg-surface)] text-[var(--ds-text-primary)]",
706
+ GLASS_OVERLAY,
707
+ // flush drops the edge border for children that draw their own (the mobile
708
+ // menu's Sidebar); a non-modal panel has no backdrop to separate it from
709
+ // the page, so it always keeps the border.
710
+ (!flush || nonModal) && borderByPosition[position],
711
+ "transition-transform duration-[var(--ds-transition-speed)] ease-[var(--ds-transition-ease)]",
712
+ isHorizontal && position === "left" && "rounded-r-[var(--ds-radius-lg)] overflow-hidden",
713
+ isHorizontal && position === "right" && "rounded-l-[var(--ds-radius-lg)] overflow-hidden",
714
+ !isHorizontal && "rounded-t-[var(--ds-radius-lg)] overflow-hidden",
715
+ (lightBackdrop || nonModal) && "shadow-[var(--ds-shadow-overlay)]",
716
+ slide.panel,
717
+ open ? slide.open : slide.closed
718
+ ),
719
+ children: [
720
+ canResize && open && /* @__PURE__ */ jsx(
721
+ ResizeEdge,
722
+ {
723
+ edge: position === "right" ? "left" : "right",
724
+ width: chosen ?? minPx,
725
+ min: minPx,
726
+ max: maxPx,
727
+ defaultWidth: minPx,
728
+ onResize: (w) => remember(w === minPx ? null : w),
729
+ label: "Resize panel"
730
+ }
731
+ ),
732
+ title && /* @__PURE__ */ jsxs(
733
+ "div",
734
+ {
735
+ className: "flex items-center justify-between shrink-0 border-b-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
736
+ style: {
737
+ padding: "var(--ds-space-3) var(--ds-space-6)",
738
+ minHeight: "calc(var(--ds-space-unit) * 6 + 1.75rem + 2px + var(--ds-border-width))"
739
+ },
740
+ children: [
741
+ titleTruncated ? /* @__PURE__ */ jsx(Tooltip, { content: title, children: /* @__PURE__ */ jsx("h2", { ref: titleOverflowRef, id: titleId, className: "font-[family-name:var(--ds-font-display)] text-lg font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)] truncate", children: title }) }) : /* @__PURE__ */ jsx("h2", { ref: titleOverflowRef, id: titleId, className: "font-[family-name:var(--ds-font-display)] text-lg font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)] truncate", children: title }),
742
+ /* @__PURE__ */ jsx(
743
+ "button",
744
+ {
745
+ onClick: onClose,
746
+ ...{ [OVERLAY_CLOSE_ATTR]: "" },
747
+ className: "text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] transition-colors cursor-pointer",
748
+ "aria-label": "Close",
749
+ children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 18 })
750
+ }
751
+ )
752
+ ]
753
+ }
754
+ ),
755
+ /* @__PURE__ */ jsx(
756
+ "div",
757
+ {
758
+ className: "flex-1 overflow-y-auto",
759
+ style: flush ? void 0 : { padding: "var(--ds-space-5)" },
760
+ children
761
+ }
762
+ ),
763
+ confirm,
764
+ footer && /* @__PURE__ */ jsx(
765
+ "div",
766
+ {
767
+ className: "shrink-0 flex items-center justify-end gap-ds-2 border-t-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
768
+ style: { padding: "var(--ds-space-3) var(--ds-space-5)" },
769
+ children: footer
770
+ }
771
+ )
772
+ ]
773
+ }
774
+ )
775
+ ]
776
+ }
777
+ ),
778
+ container ?? document.body
779
+ );
780
+ }
781
+ function SkipLink({ href = "#main", children = "Skip to content", className }) {
782
+ return /* @__PURE__ */ jsx(
783
+ "a",
784
+ {
785
+ href,
786
+ className: cn(
787
+ "sr-only focus:not-sr-only focus:fixed focus:top-[var(--ds-space-2)] focus:left-[var(--ds-space-2)] focus:z-[var(--ds-z-tooltip)]",
788
+ "focus:px-[var(--ds-space-3)] focus:py-[var(--ds-space-2)] focus:rounded-[var(--ds-radius-md)] focus:text-sm focus:font-medium",
789
+ "focus:bg-[var(--ds-bg-surface-elevated)] focus:text-[var(--ds-text-primary)] focus:shadow-[var(--ds-shadow-md)]",
790
+ "focus:outline-none focus:ring-2 focus:ring-[var(--ds-accent-primary)]",
791
+ className
792
+ ),
793
+ children
794
+ }
795
+ );
796
+ }
797
+ var AppShellContext = createContext(null);
798
+ function useAppShell() {
799
+ return useContext(AppShellContext);
800
+ }
801
+ var AppShell = forwardRef(function AppShell2({
802
+ layout = "sidebar",
803
+ sidebar,
804
+ mobileMenu,
805
+ mobileMenuWidth,
806
+ mobileMenuLightBackdrop,
807
+ mobileMenuFlush = true,
808
+ navbar,
809
+ mobileNav,
810
+ footer,
811
+ skipLink = true,
812
+ mainId = "main",
813
+ aside,
814
+ asideSide = "right",
815
+ launcher,
816
+ children,
817
+ className,
818
+ scrollable = true,
819
+ theme,
820
+ currentPath,
821
+ homePath = "/"
822
+ }, ref) {
823
+ const [sidebarOpen, setSidebarOpen] = useState(false);
824
+ const [asideOpen, setAsideOpen] = useState(false);
825
+ const [asideFloating, setAsideFloating] = useState(false);
826
+ const isMobile = useBreakpoint("md");
827
+ const isSmallMobile = useBreakpoint("sm");
828
+ const isTablet = isMobile && !isSmallMobile;
829
+ const autoCollapsed = useBreakpoint("lg");
830
+ const [userCollapsed, setUserCollapsed] = useState(null);
831
+ const sidebarCollapsed = userCollapsed ?? autoCollapsed;
832
+ const hasSidebar = (layout === "sidebar" || layout === "sidebar-header") && sidebar != null;
833
+ const hasMobileMenu = mobileMenu != null;
834
+ const hasMobileNav = mobileNav != null;
835
+ const showMobileDrawer = hasMobileMenu && !(isSmallMobile && hasMobileNav);
836
+ const [shellEl, setShellEl] = useState(null);
837
+ const [chrome, setChrome] = useState({});
838
+ const registerChrome = useCallback((kind, position) => {
839
+ setChrome((c) => c[kind] === position ? c : { ...c, [kind]: position });
840
+ return () => setChrome((c) => {
841
+ if (!(kind in c)) return c;
842
+ const next = { ...c };
843
+ delete next[kind];
844
+ return next;
845
+ });
846
+ }, []);
847
+ const navbarFloating = (chrome.navbar ?? theme?.navbarPosition) === "floating";
848
+ const sidebarFloating = hasSidebar && !isMobile && (chrome.sidebar ?? theme?.sidebarPosition) === "floating";
849
+ const isHome = currentPath != null && currentPath === homePath;
850
+ const hasFooter = footer != null && scrollable;
851
+ const mainRef = useRef(null);
852
+ useEffect(() => {
853
+ const main = mainRef.current;
854
+ if (!hasFooter || !shellEl || !main || typeof ResizeObserver === "undefined") {
855
+ shellEl?.style.removeProperty("--ds-main-height");
856
+ return;
857
+ }
858
+ const ro = new ResizeObserver(() => shellEl.style.setProperty("--ds-main-height", `${main.clientHeight}px`));
859
+ ro.observe(main);
860
+ return () => {
861
+ ro.disconnect();
862
+ shellEl.style.removeProperty("--ds-main-height");
863
+ };
864
+ }, [hasFooter, shellEl]);
865
+ const hasLauncher = !!launcher && !isSmallMobile;
866
+ const launcherProps = isValidElement(launcher) ? launcher.props : null;
867
+ const launcherCorner = !hasLauncher ? null : launcherProps?.variant === "ribbon" ? null : launcherProps?.edge === "bottom-left" ? "bottom-left" : launcherProps?.edge === "right" || launcherProps?.edge === "left" || launcherProps?.edge === "bottom" ? null : "bottom-right";
868
+ return /* @__PURE__ */ jsx(
869
+ AppShellContext,
870
+ {
871
+ value: { hasSidebar, hasMobileMenu, hasMobileNav, sidebarOpen, setSidebarOpen, isMobile, isTablet, sidebarCollapsed, setSidebarCollapsed: setUserCollapsed, asideOpen, setAsideOpen, asideSide, asideFloating, setAsideFloating, container: shellEl, theme: theme ?? null, isHome, navbarFloating, sidebarFloating, registerChrome, hasLauncher, launcherCorner, hasFooter },
872
+ children: /* @__PURE__ */ jsxs("div", { ref: (el) => {
873
+ setShellEl(el);
874
+ if (typeof ref === "function") ref(el);
875
+ else if (ref) ref.current = el;
876
+ }, className: cn("relative h-screen flex flex-col bg-[var(--ds-bg-canvas)] overflow-hidden", className), children: [
877
+ skipLink && /* @__PURE__ */ jsx(SkipLink, { href: `#${mainId}` }),
878
+ layout === "sidebar-header" && navbar,
879
+ layout === "multi-column" && navbar,
880
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 min-h-0", children: [
881
+ hasSidebar && !isMobile && /* @__PURE__ */ jsx("div", { className: "shrink-0 h-full", children: sidebar }),
882
+ aside && asideSide === "left" && aside,
883
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col min-w-0 min-h-0", children: [
884
+ layout === "sidebar" && navbar,
885
+ layout === "stacked" && navbar,
886
+ /* @__PURE__ */ jsx(
887
+ "main",
888
+ {
889
+ ref: mainRef,
890
+ id: mainId,
891
+ tabIndex: -1,
892
+ className: cn(
893
+ "flex-1 min-h-0 focus:outline-none",
894
+ scrollable ? "overflow-y-auto" : "relative",
895
+ layout === "multi-column" && "flex"
896
+ ),
897
+ style: { containerType: "inline-size" },
898
+ children: scrollable ? footer ? /* @__PURE__ */ jsxs("div", { className: "min-h-full flex flex-col", children: [
899
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children }),
900
+ footer
901
+ ] }) : children : /* @__PURE__ */ jsx("div", { className: "absolute inset-0", children })
902
+ }
903
+ )
904
+ ] }),
905
+ aside && asideSide === "right" && aside
906
+ ] }),
907
+ showMobileDrawer && isMobile && /* @__PURE__ */ jsx(
908
+ Drawer,
909
+ {
910
+ open: sidebarOpen,
911
+ onClose: () => setSidebarOpen(false),
912
+ position: "left",
913
+ width: mobileMenuWidth ?? "var(--ds-drawer-width)",
914
+ container: shellEl ?? void 0,
915
+ flush: mobileMenuFlush,
916
+ lightBackdrop: mobileMenuLightBackdrop,
917
+ topOffset: layout !== "stacked" && navbar != null ? "var(--ds-navbar-height)" : void 0,
918
+ children: mobileMenu
919
+ }
920
+ ),
921
+ isSmallMobile && mobileNav && /* @__PURE__ */ jsx("div", { className: "sticky bottom-0 z-[var(--ds-z-sticky)]", children: mobileNav }),
922
+ !isSmallMobile && launcher
923
+ ] })
924
+ }
925
+ );
926
+ });
927
+ function useCompact(ref, breakpoint) {
928
+ const px = typeof breakpoint === "number" ? breakpoint : readBreakpointPx(breakpoint);
929
+ const width = useElementWidth(ref);
930
+ return width > 0 && width < px;
931
+ }
932
+ function useElementWidth(ref) {
933
+ const [width, setWidth] = useState(0);
934
+ const check = useCallback(() => {
935
+ const next = ref.current?.offsetWidth ?? 0;
936
+ if (next > 0) setWidth(next);
937
+ }, [ref]);
938
+ useLayoutEffect(() => {
939
+ check();
940
+ if (!ref.current || typeof ResizeObserver === "undefined") return;
941
+ const observer = new ResizeObserver(check);
942
+ observer.observe(ref.current);
943
+ return () => observer.disconnect();
944
+ }, [check, ref]);
945
+ return width;
946
+ }
947
+
948
+ export { AppShell, Button, ButtonSizeContext, ControlSizeContext, DiscardConfirm, Drawer, DrawerFooterCtx, ResizeEdge, Sheet, SkipLink, Spinner, defaultMaxWidth, pxOf, useAppShell, useCloseSettle, useCompact, useDirtyGuard, useElementWidth, useRememberedWidth };
949
+ //# sourceMappingURL=chunk-Z37HO5NU.js.map
950
+ //# sourceMappingURL=chunk-Z37HO5NU.js.map