gridsmith-ui 0.22.0 → 0.24.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.
Files changed (43) hide show
  1. package/dist/Breadcrumbs-CGS8GKl-.d.ts +33 -0
  2. package/dist/chat.js +1 -1
  3. package/dist/chunk-2PWGJ4PK.js +45 -0
  4. package/dist/chunk-2PWGJ4PK.js.map +1 -0
  5. package/dist/chunk-2YSEME33.js +118 -0
  6. package/dist/chunk-2YSEME33.js.map +1 -0
  7. package/dist/chunk-3BRQRKDO.js +72 -0
  8. package/dist/chunk-3BRQRKDO.js.map +1 -0
  9. package/dist/{chunk-2RV23OBK.js → chunk-6XAU44U4.js} +25 -10
  10. package/dist/chunk-6XAU44U4.js.map +1 -0
  11. package/dist/{chunk-RT7RYGC3.js → chunk-JXANCXK6.js} +4 -39
  12. package/dist/chunk-JXANCXK6.js.map +1 -0
  13. package/dist/{chunk-AAYCQXZQ.js → chunk-MV7JXWXK.js} +630 -411
  14. package/dist/chunk-MV7JXWXK.js.map +1 -0
  15. package/dist/{chunk-EYMGLK4M.js → chunk-SVP2IG4S.js} +2 -2
  16. package/dist/{chunk-EYMGLK4M.js.map → chunk-SVP2IG4S.js.map} +1 -1
  17. package/dist/chunk-WOVYHFUD.js +50 -0
  18. package/dist/chunk-WOVYHFUD.js.map +1 -0
  19. package/dist/chunk-XVB55M3N.js +42 -0
  20. package/dist/chunk-XVB55M3N.js.map +1 -0
  21. package/dist/data-grid.js +5 -3
  22. package/dist/data-grid.js.map +1 -1
  23. package/dist/editor.js +11 -3
  24. package/dist/editor.js.map +1 -1
  25. package/dist/index.d.ts +227 -45
  26. package/dist/index.js +843 -586
  27. package/dist/index.js.map +1 -1
  28. package/dist/kanban.js +23 -4
  29. package/dist/kanban.js.map +1 -1
  30. package/dist/resizable.d.ts +17 -2
  31. package/dist/resizable.js +102 -24
  32. package/dist/resizable.js.map +1 -1
  33. package/dist/roots.css +19 -4
  34. package/dist/tokens.css +20 -3
  35. package/dist/virtualized.js +5 -2
  36. package/dist/virtualized.js.map +1 -1
  37. package/dist-static/tokens.css +20 -3
  38. package/package.json +1 -1
  39. package/dist/chunk-2RV23OBK.js.map +0 -1
  40. package/dist/chunk-AAYCQXZQ.js.map +0 -1
  41. package/dist/chunk-RG4KUCRS.js +0 -9
  42. package/dist/chunk-RG4KUCRS.js.map +0 -1
  43. package/dist/chunk-RT7RYGC3.js.map +0 -1
@@ -1,11 +1,13 @@
1
1
  import { PreferencesContext, Button } from './chunk-7TOJ4G7S.js';
2
2
  import { useBreakpoint, readBreakpointPx } from './chunk-CRQ6KCRD.js';
3
- import { FormChangeCtx, useReturnFocus, focusInitial, useOverlayLayer, useFocusTrap, OVERLAY_CLOSE_ATTR, useNonModalDialog } from './chunk-RT7RYGC3.js';
4
- import { useTextOverflow, Tooltip } from './chunk-CXMV7SXJ.js';
3
+ import { FormChangeCtx } from './chunk-XVB55M3N.js';
4
+ import { SurfaceBoundary } from './chunk-2PWGJ4PK.js';
5
+ import { usePopupFocus, useOverlayLayer, useReturnFocus, useFocusTrap, focusInitial, useNonModalDialog, OVERLAY_CLOSE_ATTR } from './chunk-JXANCXK6.js';
6
+ import { usePosition, useTextOverflow, Tooltip } from './chunk-CXMV7SXJ.js';
5
7
  import { useLatest } from './chunk-WH2FUFAC.js';
6
8
  import { Icon } from './chunk-4LDYR7QP.js';
7
9
  import { cn, lockBodyScroll, unlockBodyScroll, OVERLAY_SURFACE, GLASS_OVERLAY } from './chunk-M2QRNBDQ.js';
8
- import { createContext, version, forwardRef, useState, useCallback, useRef, useEffect, isValidElement, useMemo, useId, useContext, useLayoutEffect } from 'react';
10
+ import { createContext, version, forwardRef, useState, useCallback, useRef, useEffect, isValidElement, useContext, useMemo, useId, useLayoutEffect } from 'react';
9
11
  import { createPortal } from 'react-dom';
10
12
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
13
 
@@ -54,6 +56,126 @@ function useCloseSettle(open, elRef, onSettled) {
54
56
  };
55
57
  }, [open, elRef, cb]);
56
58
  }
59
+ var STEP = 16;
60
+ var BIG_STEP = 64;
61
+ function clamp(v, min, max) {
62
+ return Math.min(Math.max(v, min), max);
63
+ }
64
+ function ResizeEdge({ edge, width, min, max, defaultWidth, onResize, label, onDraggingChange, className }) {
65
+ const [dragging, setDragging] = useState(false);
66
+ const start = useRef(null);
67
+ const latestOnResize = useLatest(onResize);
68
+ const direction = edge === "left" ? -1 : 1;
69
+ const onPointerDown = useCallback(
70
+ (e) => {
71
+ if (e.button !== 0) return;
72
+ e.preventDefault();
73
+ start.current = { x: e.clientX, width };
74
+ setDragging(true);
75
+ e.currentTarget.setPointerCapture(e.pointerId);
76
+ },
77
+ [width]
78
+ );
79
+ const onPointerMove = useCallback(
80
+ (e) => {
81
+ if (!start.current) return;
82
+ const next = clamp(start.current.width + direction * (e.clientX - start.current.x), min, max());
83
+ latestOnResize.current(next);
84
+ },
85
+ [direction, min, max, latestOnResize]
86
+ );
87
+ const endDrag = useCallback(() => {
88
+ start.current = null;
89
+ setDragging(false);
90
+ }, []);
91
+ const latestOnDraggingChange = useLatest(onDraggingChange);
92
+ useEffect(() => {
93
+ latestOnDraggingChange.current?.(dragging);
94
+ }, [dragging, latestOnDraggingChange]);
95
+ useEffect(() => {
96
+ if (!dragging) return;
97
+ const onKey = (e) => {
98
+ if (e.key !== "Escape" || !start.current) return;
99
+ latestOnResize.current(start.current.width);
100
+ endDrag();
101
+ };
102
+ document.addEventListener("keydown", onKey);
103
+ return () => document.removeEventListener("keydown", onKey);
104
+ }, [dragging, endDrag, latestOnResize]);
105
+ const onKeyDown = (e) => {
106
+ const step = e.shiftKey ? BIG_STEP : STEP;
107
+ let next = null;
108
+ if (e.key === "ArrowLeft") next = width - direction * step;
109
+ else if (e.key === "ArrowRight") next = width + direction * step;
110
+ else if (e.key === "Home") next = min;
111
+ else if (e.key === "End") next = max();
112
+ else if (e.key === "Enter") next = defaultWidth;
113
+ if (next === null) return;
114
+ e.preventDefault();
115
+ onResize(clamp(next, min, max()));
116
+ };
117
+ return /* @__PURE__ */ jsx(
118
+ "div",
119
+ {
120
+ role: "separator",
121
+ tabIndex: 0,
122
+ "aria-orientation": "vertical",
123
+ "aria-label": label,
124
+ "aria-valuemin": min,
125
+ "aria-valuemax": max(),
126
+ "aria-valuenow": Math.round(width),
127
+ "data-resize-edge": edge,
128
+ "data-dragging": dragging || void 0,
129
+ onPointerDown,
130
+ onPointerMove,
131
+ onPointerUp: endDrag,
132
+ onPointerCancel: endDrag,
133
+ onDoubleClick: () => onResize(clamp(defaultWidth, min, max())),
134
+ onKeyDown,
135
+ className: cn(
136
+ // A narrow absolutely-positioned strip over the edge — never a flex child that takes width.
137
+ "group/edge absolute top-0 bottom-0 z-10 w-ds-2 cursor-col-resize touch-none select-none outline-none",
138
+ edge === "left" ? "-left-ds-1" : "-right-ds-1",
139
+ className
140
+ ),
141
+ children: /* @__PURE__ */ jsx(
142
+ "div",
143
+ {
144
+ "aria-hidden": "true",
145
+ className: cn(
146
+ "absolute top-0 bottom-0 left-1/2 w-px -translate-x-1/2 transition-[width,background-color] duration-[var(--ds-transition-speed)]",
147
+ "group-hover/edge:w-[3px] group-hover/edge:bg-[var(--ds-accent-primary)]",
148
+ "group-focus-visible/edge:w-[3px] group-focus-visible/edge:bg-[var(--ds-accent-primary)]",
149
+ dragging && "w-[3px] bg-[var(--ds-accent-primary)]"
150
+ )
151
+ }
152
+ )
153
+ }
154
+ );
155
+ }
156
+ var remembered = /* @__PURE__ */ new Map();
157
+ function useRememberedWidth(key) {
158
+ const [width, setWidth] = useState(() => key ? remembered.get(key) ?? null : null);
159
+ const remember = useCallback(
160
+ (w) => {
161
+ setWidth(w);
162
+ if (!key) return;
163
+ if (w === null) remembered.delete(key);
164
+ else remembered.set(key, w);
165
+ },
166
+ [key]
167
+ );
168
+ return [width, remember];
169
+ }
170
+ function pxOf(length) {
171
+ if (!length) return null;
172
+ const m = /^(\d+(?:\.\d+)?)px$/.exec(length.trim());
173
+ return m ? parseFloat(m[1]) : null;
174
+ }
175
+ function defaultMaxWidth(container) {
176
+ const base = container instanceof HTMLElement ? container.clientWidth : typeof window !== "undefined" ? window.innerWidth : 1200;
177
+ return Math.round(base * 0.6);
178
+ }
57
179
  var OVERLAY_ROOT_ATTR = "data-overlay-root";
58
180
  function controlsOf(root) {
59
181
  if (!root) return "";
@@ -177,371 +299,69 @@ function useDirtyGuard(dirty, onClose, onDiscard) {
177
299
  const then = pending.current;
178
300
  pending.current = void 0;
179
301
  runAfter(then);
180
- }, [onClose, onDiscard, runAfter]);
181
- return { confirming, requestClose, keep, discard };
182
- }
183
- function DiscardConfirm({ open, onKeep, onDiscard }) {
184
- const keepRef = useRef(null);
185
- const returnTo = useRef(null);
186
- const overlay = useRef(null);
187
- useEffect(() => {
188
- if (!open) return;
189
- const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;
190
- overlay.current = keepRef.current?.closest("[role='dialog'], [role='alertdialog']") ?? null;
191
- returnTo.current = active && active !== document.body && overlay.current?.contains(active) ? active : null;
192
- keepRef.current?.focus();
193
- return () => {
194
- if (document.activeElement !== document.body) return;
195
- const back = returnTo.current;
196
- if (back && back.isConnected) {
197
- back.focus();
198
- return;
199
- }
200
- const first = overlay.current?.querySelector("button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])");
201
- (first ?? overlay.current)?.focus();
202
- };
203
- }, [open]);
204
- if (!open) return null;
205
- return /* @__PURE__ */ jsxs(
206
- "div",
207
- {
208
- role: "group",
209
- "aria-label": "Unsaved changes",
210
- "data-discard-confirm": "",
211
- 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)]",
212
- style: { padding: "var(--ds-space-3) var(--ds-space-5)" },
213
- children: [
214
- /* @__PURE__ */ jsx("p", { className: "text-sm text-[var(--ds-text-primary)]", children: "You have unsaved changes." }),
215
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2", children: [
216
- /* @__PURE__ */ jsx(Button, { type: "button", ref: keepRef, variant: "secondary", size: "sm", onClick: onKeep, children: "Keep editing" }),
217
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", size: "sm", onClick: onDiscard, children: "Discard changes" })
218
- ] })
219
- ]
220
- }
221
- );
222
- }
223
- function Sheet({ open, onClose: onCloseProp, title, children, footer, confirm, dirty, onDiscard, snapPoints = [0.5, 0.92], container, flush, keepMounted = false, id, labelledBy }) {
224
- const standalone = confirm === void 0;
225
- const { marked, mark } = useOverlayMark(open);
226
- const guards = standalone || dirty !== void 0;
227
- const guard = useDirtyGuard(standalone ? dirty ?? marked : dirty, onCloseProp, onDiscard);
228
- const onClose = guard.requestClose;
229
- const titleId = useId();
230
- const panelRef = useRef(null);
231
- const [mounted, setMounted] = useState(open);
232
- const [hidden, setHidden] = useState(!open);
233
- const [snapIdx, setSnapIdx] = useState(0);
234
- const [dragOffset, setDragOffset] = useState(0);
235
- const [dragging, setDragging] = useState(false);
236
- const dragStartY = useRef(0);
237
- const startHeight = useRef(0);
238
- const onCloseRef = useLatest(onClose);
239
- const [prevOpen, setPrevOpen] = useState(open);
240
- if (open !== prevOpen) {
241
- setPrevOpen(open);
242
- if (open) {
243
- setMounted(true);
244
- setHidden(false);
245
- setSnapIdx(0);
246
- setDragOffset(0);
247
- }
248
- }
249
- if (open && !mounted) setMounted(true);
250
- if (open && hidden) setHidden(false);
251
- const viewportHeight = useCallback(
252
- () => container instanceof HTMLElement ? container.clientHeight : window.innerHeight,
253
- [container]
254
- );
255
- useCloseSettle(open, panelRef, () => {
256
- if (keepMounted) setHidden(true);
257
- else setMounted(false);
258
- });
259
- useReturnFocus(open, panelRef);
260
- useEffect(() => {
261
- if (!open || !mounted) return;
262
- const raf = requestAnimationFrame(() => focusInitial(panelRef.current));
263
- return () => cancelAnimationFrame(raf);
264
- }, [open, mounted]);
265
- const isTopLayer = useOverlayLayer(open);
266
- useFocusTrap(panelRef, open, isTopLayer);
267
- useEffect(() => {
268
- if (!open) return;
269
- const onKeyDown = (e) => {
270
- if (e.key !== "Escape" || e.defaultPrevented || !isTopLayer()) return;
271
- e.preventDefault();
272
- onCloseRef.current();
273
- };
274
- document.addEventListener("keydown", onKeyDown);
275
- if (!container) lockBodyScroll();
276
- return () => {
277
- document.removeEventListener("keydown", onKeyDown);
278
- if (!container) unlockBodyScroll();
279
- };
280
- }, [open, container, onCloseRef, isTopLayer]);
281
- const handlePointerDown = useCallback((e) => {
282
- setDragging(true);
283
- dragStartY.current = e.clientY;
284
- startHeight.current = (snapPoints[snapIdx] ?? snapPoints[0]) * viewportHeight();
285
- setDragOffset(0);
286
- e.target.setPointerCapture(e.pointerId);
287
- }, [snapIdx, snapPoints, viewportHeight]);
288
- const handlePointerMove = useCallback((e) => {
289
- if (!dragging) return;
290
- const delta = dragStartY.current - e.clientY;
291
- setDragOffset(delta);
292
- }, [dragging]);
293
- const handlePointerUp = useCallback(() => {
294
- if (!dragging) return;
295
- setDragging(false);
296
- const currentHeightPx = startHeight.current + dragOffset;
297
- const currentFraction = currentHeightPx / viewportHeight();
298
- if (currentFraction < snapPoints[0] * 0.6) {
299
- onClose();
300
- setDragOffset(0);
301
- return;
302
- }
303
- let bestIdx = 0;
304
- let bestDist = Math.abs(currentFraction - snapPoints[0]);
305
- for (let i = 1; i < snapPoints.length; i++) {
306
- const dist = Math.abs(currentFraction - snapPoints[i]);
307
- if (dist < bestDist) {
308
- bestDist = dist;
309
- bestIdx = i;
310
- }
311
- }
312
- setSnapIdx(bestIdx);
313
- setDragOffset(0);
314
- }, [dragging, dragOffset, snapPoints, onClose, viewportHeight]);
315
- if (!mounted) return null;
316
- const baseHeight = (snapPoints[snapIdx] ?? snapPoints[0]) * 100;
317
- const heightUnit = container ? "%" : "vh";
318
- const dragPx = dragging ? dragOffset : 0;
319
- const positionClass = container ? "absolute" : "fixed";
320
- return createPortal(
321
- /* @__PURE__ */ jsxs(
322
- "div",
323
- {
324
- hidden: keepMounted && hidden ? true : void 0,
325
- className: cn(
326
- `${positionClass} inset-0 z-[var(--ds-z-modal)] transition-opacity duration-[var(--ds-transition-speed)]`,
327
- open ? "pointer-events-auto" : "pointer-events-none"
328
- ),
329
- children: [
330
- /* @__PURE__ */ jsx(
331
- "div",
332
- {
333
- className: cn(
334
- "absolute inset-0 bg-black/50 transition-opacity duration-[var(--ds-transition-speed)]",
335
- open ? "opacity-100" : "opacity-0"
336
- ),
337
- onClick: onClose
338
- }
339
- ),
340
- /* @__PURE__ */ jsxs(
341
- "div",
342
- {
343
- ref: panelRef,
344
- id,
345
- role: "dialog",
346
- "data-gs": "sheet",
347
- ...{ [OVERLAY_ROOT_ATTR]: "" },
348
- "aria-modal": "true",
349
- "aria-labelledby": title ? titleId : labelledBy,
350
- "data-ds-sheet": "",
351
- className: cn(
352
- `${positionClass} bottom-0 left-0 right-0 z-10 flex flex-col`,
353
- OVERLAY_SURFACE,
354
- "text-[var(--ds-text-primary)]",
355
- GLASS_OVERLAY,
356
- "border-t-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
357
- "rounded-t-[var(--ds-radius-xl)]",
358
- !dragging && "transition-[transform,height] duration-[var(--ds-transition-speed)] ease-[var(--ds-transition-ease)]",
359
- open ? "translate-y-0" : "translate-y-full"
360
- ),
361
- style: {
362
- containerType: "inline-size",
363
- height: dragPx ? `calc(${baseHeight}${heightUnit} + ${dragPx}px)` : `${baseHeight}${heightUnit}`
364
- },
365
- children: [
366
- /* @__PURE__ */ jsx(
367
- "div",
368
- {
369
- className: "flex justify-center pt-ds-2 pb-ds-1 cursor-grab active:cursor-grabbing touch-none",
370
- onPointerDown: handlePointerDown,
371
- onPointerMove: handlePointerMove,
372
- onPointerUp: handlePointerUp,
373
- onPointerCancel: handlePointerUp,
374
- children: /* @__PURE__ */ jsx("div", { className: "w-10 h-1 rounded-[var(--ds-radius-full)] bg-[var(--ds-border-default)]" })
375
- }
376
- ),
377
- title && /* @__PURE__ */ jsxs(
378
- "div",
379
- {
380
- className: "flex items-center justify-between shrink-0 border-b-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
381
- style: { padding: "var(--ds-space-2) var(--ds-space-5)" },
382
- children: [
383
- /* @__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 }),
384
- /* @__PURE__ */ jsx(
385
- "button",
386
- {
387
- type: "button",
388
- onClick: onClose,
389
- ...{ [OVERLAY_CLOSE_ATTR]: "" },
390
- className: "text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] transition-colors cursor-pointer",
391
- "aria-label": "Close",
392
- children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 18 })
393
- }
394
- )
395
- ]
396
- }
397
- ),
398
- /* @__PURE__ */ jsx(
399
- "div",
400
- {
401
- className: "flex-1 overflow-y-auto",
402
- style: flush ? void 0 : { padding: "var(--ds-space-5)" },
403
- children: guards ? /* @__PURE__ */ jsx(OverlayCloseCtx, { value: guard.requestClose, children: standalone ? /* @__PURE__ */ jsx(OverlayMarkScope, { mark, children }) : children }) : children
404
- }
405
- ),
406
- confirm,
407
- guards && /* @__PURE__ */ jsx(DiscardConfirm, { open: guard.confirming, onKeep: guard.keep, onDiscard: guard.discard }),
408
- footer && /* @__PURE__ */ jsx(
409
- "div",
410
- {
411
- className: "shrink-0 flex items-center justify-end gap-ds-2 border-t-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
412
- style: { padding: "var(--ds-space-3) var(--ds-space-5)" },
413
- children: footer
414
- }
415
- )
416
- ]
417
- }
418
- )
419
- ]
420
- }
421
- ),
422
- container ?? document.body
423
- );
424
- }
425
- var STEP = 16;
426
- var BIG_STEP = 64;
427
- function clamp(v, min, max) {
428
- return Math.min(Math.max(v, min), max);
429
- }
430
- function ResizeEdge({ edge, width, min, max, defaultWidth, onResize, label, onDraggingChange, className }) {
431
- const [dragging, setDragging] = useState(false);
432
- const start = useRef(null);
433
- const latestOnResize = useLatest(onResize);
434
- const direction = edge === "left" ? -1 : 1;
435
- const onPointerDown = useCallback(
436
- (e) => {
437
- if (e.button !== 0) return;
438
- e.preventDefault();
439
- start.current = { x: e.clientX, width };
440
- setDragging(true);
441
- e.currentTarget.setPointerCapture(e.pointerId);
442
- },
443
- [width]
444
- );
445
- const onPointerMove = useCallback(
446
- (e) => {
447
- if (!start.current) return;
448
- const next = clamp(start.current.width + direction * (e.clientX - start.current.x), min, max());
449
- latestOnResize.current(next);
450
- },
451
- [direction, min, max, latestOnResize]
452
- );
453
- const endDrag = useCallback(() => {
454
- start.current = null;
455
- setDragging(false);
456
- }, []);
457
- const latestOnDraggingChange = useLatest(onDraggingChange);
458
- useEffect(() => {
459
- latestOnDraggingChange.current?.(dragging);
460
- }, [dragging, latestOnDraggingChange]);
302
+ }, [onClose, onDiscard, runAfter]);
303
+ return { confirming, requestClose, keep, discard };
304
+ }
305
+ function DiscardConfirm({ open, onKeep, onDiscard }) {
306
+ const keepRef = useRef(null);
307
+ const returnTo = useRef(null);
308
+ const overlay = useRef(null);
461
309
  useEffect(() => {
462
- if (!dragging) return;
463
- const onKey = (e) => {
464
- if (e.key !== "Escape" || !start.current) return;
465
- latestOnResize.current(start.current.width);
466
- endDrag();
310
+ if (!open) return;
311
+ const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;
312
+ overlay.current = keepRef.current?.closest("[role='dialog'], [role='alertdialog']") ?? null;
313
+ returnTo.current = active && active !== document.body && overlay.current?.contains(active) ? active : null;
314
+ keepRef.current?.focus();
315
+ return () => {
316
+ if (document.activeElement !== document.body) return;
317
+ const back = returnTo.current;
318
+ if (back && back.isConnected) {
319
+ back.focus();
320
+ return;
321
+ }
322
+ const first = overlay.current?.querySelector("button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])");
323
+ (first ?? overlay.current)?.focus();
467
324
  };
468
- document.addEventListener("keydown", onKey);
469
- return () => document.removeEventListener("keydown", onKey);
470
- }, [dragging, endDrag, latestOnResize]);
471
- const onKeyDown = (e) => {
472
- const step = e.shiftKey ? BIG_STEP : STEP;
473
- let next = null;
474
- if (e.key === "ArrowLeft") next = width - direction * step;
475
- else if (e.key === "ArrowRight") next = width + direction * step;
476
- else if (e.key === "Home") next = min;
477
- else if (e.key === "End") next = max();
478
- else if (e.key === "Enter") next = defaultWidth;
479
- if (next === null) return;
480
- e.preventDefault();
481
- onResize(clamp(next, min, max()));
482
- };
483
- return /* @__PURE__ */ jsx(
325
+ }, [open]);
326
+ if (!open) return null;
327
+ return /* @__PURE__ */ jsxs(
484
328
  "div",
485
329
  {
486
- role: "separator",
487
- tabIndex: 0,
488
- "aria-orientation": "vertical",
489
- "aria-label": label,
490
- "aria-valuemin": min,
491
- "aria-valuemax": max(),
492
- "aria-valuenow": Math.round(width),
493
- "data-resize-edge": edge,
494
- "data-dragging": dragging || void 0,
495
- onPointerDown,
496
- onPointerMove,
497
- onPointerUp: endDrag,
498
- onPointerCancel: endDrag,
499
- onDoubleClick: () => onResize(clamp(defaultWidth, min, max())),
500
- onKeyDown,
501
- className: cn(
502
- // A narrow absolutely-positioned strip over the edge — never a flex child that takes width.
503
- "group/edge absolute top-0 bottom-0 z-10 w-ds-2 cursor-col-resize touch-none select-none outline-none",
504
- edge === "left" ? "-left-ds-1" : "-right-ds-1",
505
- className
506
- ),
507
- children: /* @__PURE__ */ jsx(
508
- "div",
509
- {
510
- "aria-hidden": "true",
511
- className: cn(
512
- "absolute top-0 bottom-0 left-1/2 w-px -translate-x-1/2 transition-[width,background-color] duration-[var(--ds-transition-speed)]",
513
- "group-hover/edge:w-[3px] group-hover/edge:bg-[var(--ds-accent-primary)]",
514
- "group-focus-visible/edge:w-[3px] group-focus-visible/edge:bg-[var(--ds-accent-primary)]",
515
- dragging && "w-[3px] bg-[var(--ds-accent-primary)]"
516
- )
517
- }
518
- )
330
+ role: "group",
331
+ "aria-label": "Unsaved changes",
332
+ "data-discard-confirm": "",
333
+ 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)]",
334
+ style: { padding: "var(--ds-space-3) var(--ds-page-gutter)" },
335
+ children: [
336
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-[var(--ds-text-primary)]", children: "You have unsaved changes." }),
337
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2", children: [
338
+ /* @__PURE__ */ jsx(Button, { type: "button", ref: keepRef, variant: "secondary", size: "sm", onClick: onKeep, children: "Keep editing" }),
339
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", size: "sm", onClick: onDiscard, children: "Discard changes" })
340
+ ] })
341
+ ]
519
342
  }
520
343
  );
521
344
  }
522
- var remembered = /* @__PURE__ */ new Map();
523
- function useRememberedWidth(key) {
524
- const [width, setWidth] = useState(() => key ? remembered.get(key) ?? null : null);
525
- const remember = useCallback(
526
- (w) => {
527
- setWidth(w);
528
- if (!key) return;
529
- if (w === null) remembered.delete(key);
530
- else remembered.set(key, w);
531
- },
532
- [key]
533
- );
534
- return [width, remember];
535
- }
536
- function pxOf(length) {
537
- if (!length) return null;
538
- const m = /^(\d+(?:\.\d+)?)px$/.exec(length.trim());
539
- return m ? parseFloat(m[1]) : null;
540
- }
541
- function defaultMaxWidth(container) {
542
- const base = container instanceof HTMLElement ? container.clientWidth : typeof window !== "undefined" ? window.innerWidth : 1200;
543
- return Math.round(base * 0.6);
345
+ var DrawerWidthCtx = createContext(null);
346
+ function useScrollbarInset() {
347
+ const observer = useRef(null);
348
+ return useCallback((el) => {
349
+ observer.current?.disconnect();
350
+ observer.current = null;
351
+ if (!el) return;
352
+ const measure = () => {
353
+ const style = getComputedStyle(el);
354
+ const borders = (parseFloat(style.borderLeftWidth) || 0) + (parseFloat(style.borderRightWidth) || 0);
355
+ el.style.setProperty("--gs-scrollbar", `${Math.max(0, el.offsetWidth - el.clientWidth - borders)}px`);
356
+ };
357
+ measure();
358
+ if (typeof ResizeObserver === "undefined") return;
359
+ const o = new ResizeObserver(measure);
360
+ o.observe(el);
361
+ observer.current = o;
362
+ }, []);
544
363
  }
364
+ var insetLessScrollbar = (gutter) => `max(0px, calc(${gutter} - var(--gs-scrollbar, 0px)))`;
545
365
  var DrawerFooterCtx = createContext(null);
546
366
  var slideClasses = {
547
367
  right: {
@@ -566,7 +386,8 @@ var borderByPosition = {
566
386
  bottom: "border-t-[length:var(--ds-border-width)] border-t-[var(--ds-border-default)]"
567
387
  };
568
388
  function Drawer(props) {
569
- const { position = "right", mobile, snapPoints, open, onClose, title, children, footer, container, flush, keepMounted, id, labelledBy, dirty, onDiscard } = props;
389
+ const { position = "right", mobile, open, onClose, footer, dirty, onDiscard } = props;
390
+ const shellWidth = useContext(DrawerWidthCtx) ?? void 0;
570
391
  const isPhone = useBreakpoint("sm");
571
392
  const mobileMode = mobile ?? (position === "left" ? "panel" : "sheet");
572
393
  const { marked, mark } = useOverlayMark(open);
@@ -589,15 +410,16 @@ function Drawer(props) {
589
410
  () => ({ registerActions, unregisterActions }),
590
411
  [registerActions, unregisterActions]
591
412
  );
592
- if (isPhone && mobileMode === "sheet") {
593
- return /* @__PURE__ */ jsx(DrawerFooterCtx, { value: footerCtx, children: /* @__PURE__ */ jsx(OverlayMarkScope, { mark, children: /* @__PURE__ */ jsx(OverlayCloseCtx, { value: guard.requestClose, children: /* @__PURE__ */ jsx(Sheet, { open, onClose: guard.requestClose, title, footer: effectiveFooter, confirm, container, flush, snapPoints, keepMounted, id, labelledBy, children }) }) }) });
594
- }
595
- return /* @__PURE__ */ jsx(DrawerFooterCtx, { value: footerCtx, children: /* @__PURE__ */ jsx(OverlayMarkScope, { mark, children: /* @__PURE__ */ jsx(OverlayCloseCtx, { value: guard.requestClose, children: /* @__PURE__ */ jsx(DrawerPanel, { ...props, footer: effectiveFooter, onClose: guard.requestClose, confirm }) }) }) });
413
+ return /* @__PURE__ */ jsx(DrawerFooterCtx, { value: footerCtx, children: /* @__PURE__ */ jsx(OverlayMarkScope, { mark, children: /* @__PURE__ */ jsx(OverlayCloseCtx, { value: guard.requestClose, children: /* @__PURE__ */ jsx(DrawerPanel, { ...props, width: props.width ?? shellWidth, form: isPhone && mobileMode === "sheet" ? "sheet" : "panel", footer: effectiveFooter, onClose: guard.requestClose, confirm }) }) }) });
596
414
  }
597
- 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 }) {
415
+ var INSET = "var(--ds-page-gutter)";
416
+ var DEFAULT_SNAPS = [0.5, 0.92];
417
+ function DrawerPanel({ form, open, onClose, position = "right", width = "400px", title, children, footer, container, flush, lightBackdrop, topOffset, modal = true, keepMounted = false, id, labelledBy, resizable = false, maxWidth, snapPoints = DEFAULT_SNAPS, confirm }) {
418
+ const sheet = form === "sheet";
598
419
  const nonModal = !modal;
420
+ const blocking = !nonModal || sheet;
599
421
  const [chosen, remember] = useRememberedWidth(id ?? labelledBy);
600
- const canResize = nonModal && resizable && position !== "bottom";
422
+ const canResize = !sheet && nonModal && resizable && position !== "bottom";
601
423
  const effectiveWidth = canResize && chosen !== null ? `${chosen}px` : width;
602
424
  const minPx = pxOf(width) ?? 320;
603
425
  const maxPx = () => pxOf(maxWidth) ?? defaultMaxWidth(container instanceof Element ? container : null);
@@ -607,40 +429,104 @@ function DrawerPanel({ open, onClose, position = "right", width = "400px", title
607
429
  const panelRef = useRef(null);
608
430
  const [mounted, setMounted] = useState(open);
609
431
  const [hidden, setHidden] = useState(!open);
432
+ const [snapIdx, setSnapIdx] = useState(0);
433
+ const [dragOffset, setDragOffset] = useState(0);
434
+ const [dragging, setDragging] = useState(false);
435
+ const dragStartY = useRef(0);
436
+ const startHeight = useRef(0);
610
437
  const onCloseRef = useLatest(onClose);
438
+ const [prevOpen, setPrevOpen] = useState(open);
439
+ if (open !== prevOpen) {
440
+ setPrevOpen(open);
441
+ if (open) {
442
+ setSnapIdx(0);
443
+ setDragOffset(0);
444
+ }
445
+ }
611
446
  if (open && !mounted) setMounted(true);
612
447
  if (open && hidden) setHidden(false);
613
448
  const isTopLayer = useOverlayLayer(open);
614
449
  useEffect(() => {
615
- if (!open || nonModal) return;
450
+ if (!open || !blocking) return;
616
451
  const onKeyDown = (e) => {
617
452
  if (e.key !== "Escape" || e.defaultPrevented || !isTopLayer()) return;
618
453
  e.preventDefault();
619
454
  onCloseRef.current();
620
455
  };
621
456
  document.addEventListener("keydown", onKeyDown);
622
- if (!container) lockBodyScroll();
623
- return () => {
624
- document.removeEventListener("keydown", onKeyDown);
625
- if (!container) unlockBodyScroll();
626
- };
627
- }, [open, nonModal, container, onCloseRef, isTopLayer]);
457
+ return () => document.removeEventListener("keydown", onKeyDown);
458
+ }, [open, blocking, onCloseRef, isTopLayer]);
459
+ useEffect(() => {
460
+ if (!open || !blocking || container) return;
461
+ lockBodyScroll();
462
+ return () => unlockBodyScroll();
463
+ }, [open, blocking, container]);
628
464
  useReturnFocus(open && !nonModal, panelRef);
629
- useFocusTrap(panelRef, open && !topOffset && !nonModal, isTopLayer);
465
+ useFocusTrap(panelRef, open && blocking && (sheet || !topOffset), isTopLayer);
630
466
  useEffect(() => {
631
467
  if (!open || !mounted || nonModal) return;
632
468
  const raf = requestAnimationFrame(() => focusInitial(panelRef.current));
633
469
  return () => cancelAnimationFrame(raf);
634
470
  }, [open, mounted, nonModal]);
635
471
  useNonModalDialog(panelRef, open && mounted && nonModal, onClose, isTopLayer);
472
+ const wasBlocking = useRef(blocking);
473
+ useEffect(() => {
474
+ const was = wasBlocking.current;
475
+ wasBlocking.current = blocking;
476
+ if (!open || !mounted || !blocking || was) return;
477
+ const panel = panelRef.current;
478
+ if (panel && !panel.contains(document.activeElement)) focusInitial(panel);
479
+ }, [open, mounted, blocking]);
480
+ const bodyRef = useScrollbarInset();
636
481
  useCloseSettle(open, panelRef, () => {
637
482
  if (keepMounted) setHidden(true);
638
483
  else setMounted(false);
639
484
  });
485
+ const viewportHeight = useCallback(
486
+ () => container instanceof HTMLElement ? container.clientHeight : window.innerHeight,
487
+ [container]
488
+ );
489
+ const handlePointerDown = useCallback((e) => {
490
+ setDragging(true);
491
+ dragStartY.current = e.clientY;
492
+ startHeight.current = (snapPoints[snapIdx] ?? snapPoints[0]) * viewportHeight();
493
+ setDragOffset(0);
494
+ e.target.setPointerCapture(e.pointerId);
495
+ }, [snapIdx, snapPoints, viewportHeight]);
496
+ const handlePointerMove = useCallback((e) => {
497
+ if (!dragging) return;
498
+ setDragOffset(dragStartY.current - e.clientY);
499
+ }, [dragging]);
500
+ const handlePointerUp = useCallback(() => {
501
+ if (!dragging) return;
502
+ setDragging(false);
503
+ const currentFraction = (startHeight.current + dragOffset) / viewportHeight();
504
+ if (currentFraction < snapPoints[0] * 0.6) {
505
+ onClose();
506
+ setDragOffset(0);
507
+ return;
508
+ }
509
+ let bestIdx = 0;
510
+ let bestDist = Math.abs(currentFraction - snapPoints[0]);
511
+ for (let i = 1; i < snapPoints.length; i++) {
512
+ const dist = Math.abs(currentFraction - snapPoints[i]);
513
+ if (dist < bestDist) {
514
+ bestDist = dist;
515
+ bestIdx = i;
516
+ }
517
+ }
518
+ setSnapIdx(bestIdx);
519
+ setDragOffset(0);
520
+ }, [dragging, dragOffset, snapPoints, onClose, viewportHeight]);
640
521
  if (!mounted) return null;
641
522
  const slide = slideClasses[position];
642
523
  const isHorizontal = position === "left" || position === "right";
643
524
  const positionClass = container ? "absolute" : "fixed";
525
+ const offset = sheet ? void 0 : topOffset;
526
+ const baseHeight = (snapPoints[snapIdx] ?? snapPoints[0]) * 100;
527
+ const heightUnit = container ? "%" : "vh";
528
+ const dragPx = dragging ? dragOffset : 0;
529
+ const heading = "font-[family-name:var(--ds-font-display)] text-lg font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)] truncate";
644
530
  return createPortal(
645
531
  /* @__PURE__ */ jsxs(
646
532
  "div",
@@ -650,17 +536,17 @@ function DrawerPanel({ open, onClose, position = "right", width = "400px", title
650
536
  `${positionClass} transition-opacity duration-[var(--ds-transition-speed)]`,
651
537
  // A non-modal panel never intercepts the page: the wrapper is inert to the
652
538
  // pointer and only the panel itself accepts events.
653
- nonModal ? "z-[var(--ds-z-overlay)] pointer-events-none" : cn("z-[var(--ds-z-modal)]", open ? "pointer-events-auto" : "pointer-events-none"),
654
- topOffset ? "left-0 right-0 bottom-0" : "inset-0"
539
+ blocking ? cn("z-[var(--ds-z-modal)]", open ? "pointer-events-auto" : "pointer-events-none") : "z-[var(--ds-z-overlay)] pointer-events-none",
540
+ offset ? "left-0 right-0 bottom-0" : "inset-0"
655
541
  ),
656
- style: topOffset ? { top: topOffset } : void 0,
542
+ style: offset ? { top: offset } : void 0,
657
543
  children: [
658
- !nonModal && /* @__PURE__ */ jsx(
544
+ blocking && /* @__PURE__ */ jsx(
659
545
  "div",
660
546
  {
661
547
  className: cn(
662
548
  "absolute inset-0 transition-opacity duration-[var(--ds-transition-speed)]",
663
- 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))]"),
549
+ sheet ? "bg-black/50" : 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))]"),
664
550
  open ? "opacity-100" : "opacity-0"
665
551
  ),
666
552
  onClick: onClose
@@ -672,12 +558,18 @@ function DrawerPanel({ open, onClose, position = "right", width = "400px", title
672
558
  ref: panelRef,
673
559
  id,
674
560
  role: "dialog",
675
- "data-gs": "drawer",
561
+ "data-gs": sheet ? "sheet" : "drawer",
562
+ "data-ds-sheet": sheet ? "" : void 0,
676
563
  ...{ [OVERLAY_ROOT_ATTR]: "" },
677
- "aria-modal": nonModal ? void 0 : "true",
564
+ "aria-modal": blocking ? "true" : void 0,
678
565
  "aria-labelledby": title ? titleId : labelledBy,
679
- style: {
566
+ style: sheet ? {
567
+ containerType: "inline-size",
568
+ "--ds-form-max": "none",
569
+ height: dragPx ? `calc(${baseHeight}${heightUnit} + ${dragPx}px)` : `${baseHeight}${heightUnit}`
570
+ } : {
680
571
  containerType: "inline-size",
572
+ "--ds-form-max": "none",
681
573
  width: isHorizontal ? effectiveWidth : void 0,
682
574
  maxWidth: isHorizontal ? "100%" : void 0,
683
575
  height: isMobile ? "100%" : void 0,
@@ -685,23 +577,42 @@ function DrawerPanel({ open, onClose, position = "right", width = "400px", title
685
577
  },
686
578
  className: cn(
687
579
  `${positionClass} z-10 flex flex-col`,
688
- nonModal && "pointer-events-auto",
580
+ !blocking && "pointer-events-auto",
689
581
  OVERLAY_SURFACE,
690
582
  "text-[var(--ds-text-primary)]",
691
583
  GLASS_OVERLAY,
692
- // flush drops the edge border for children that draw their own (the mobile
693
- // menu's Sidebar); a non-modal panel has no backdrop to separate it from
694
- // the page, so it always keeps the border.
695
- (!flush || nonModal) && borderByPosition[position],
696
- "transition-transform duration-[var(--ds-transition-speed)] ease-[var(--ds-transition-ease)]",
697
- isHorizontal && position === "left" && "rounded-r-[var(--ds-radius-lg)] overflow-hidden",
698
- isHorizontal && position === "right" && "rounded-l-[var(--ds-radius-lg)] overflow-hidden",
699
- !isHorizontal && "rounded-t-[var(--ds-radius-lg)] overflow-hidden",
700
- (lightBackdrop || nonModal) && "shadow-[var(--ds-shadow-overlay)]",
701
- slide.panel,
702
- open ? slide.open : slide.closed
584
+ sheet ? cn(
585
+ "bottom-0 left-0 right-0",
586
+ "border-t-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
587
+ "rounded-t-[var(--ds-radius-xl)]",
588
+ !dragging && "transition-[transform,height] duration-[var(--ds-transition-speed)] ease-[var(--ds-transition-ease)]",
589
+ open ? "translate-y-0" : "translate-y-full"
590
+ ) : cn(
591
+ // flush drops the edge border for children that draw their own (the mobile
592
+ // menu's Sidebar); a non-modal panel has no backdrop to separate it from
593
+ // the page, so it always keeps the border.
594
+ (!flush || nonModal) && borderByPosition[position],
595
+ "transition-transform duration-[var(--ds-transition-speed)] ease-[var(--ds-transition-ease)]",
596
+ isHorizontal && position === "left" && "rounded-r-[var(--ds-radius-lg)] overflow-hidden",
597
+ isHorizontal && position === "right" && "rounded-l-[var(--ds-radius-lg)] overflow-hidden",
598
+ !isHorizontal && "rounded-t-[var(--ds-radius-lg)] overflow-hidden",
599
+ (lightBackdrop || nonModal) && "shadow-[var(--ds-shadow-overlay)]",
600
+ slide.panel,
601
+ open ? slide.open : slide.closed
602
+ )
703
603
  ),
704
604
  children: [
605
+ sheet && /* @__PURE__ */ jsx(
606
+ "div",
607
+ {
608
+ className: "flex justify-center pt-ds-2 pb-ds-1 cursor-grab active:cursor-grabbing touch-none",
609
+ onPointerDown: handlePointerDown,
610
+ onPointerMove: handlePointerMove,
611
+ onPointerUp: handlePointerUp,
612
+ onPointerCancel: handlePointerUp,
613
+ children: /* @__PURE__ */ jsx("div", { className: "w-10 h-1 rounded-[var(--ds-radius-full)] bg-[var(--ds-border-default)]" })
614
+ }
615
+ ),
705
616
  canResize && open && /* @__PURE__ */ jsx(
706
617
  ResizeEdge,
707
618
  {
@@ -717,13 +628,13 @@ function DrawerPanel({ open, onClose, position = "right", width = "400px", title
717
628
  title && /* @__PURE__ */ jsxs(
718
629
  "div",
719
630
  {
720
- className: "flex items-center justify-between shrink-0 border-b-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
721
- style: {
722
- padding: "var(--ds-space-3) var(--ds-space-6)",
631
+ className: "flex items-center justify-between gap-ds-3 shrink-0 border-b-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
632
+ style: sheet ? { padding: `var(--ds-space-2) ${INSET}` } : {
633
+ padding: `var(--ds-space-3) ${INSET}`,
723
634
  minHeight: "calc(var(--ds-space-unit) * 6 + 1.75rem + 2px + var(--ds-border-width))"
724
635
  },
725
636
  children: [
726
- 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 }),
637
+ !sheet && titleTruncated ? /* @__PURE__ */ jsx(Tooltip, { content: title, children: /* @__PURE__ */ jsx("h2", { ref: titleOverflowRef, id: titleId, className: heading, children: title }) }) : /* @__PURE__ */ jsx("h2", { ref: titleOverflowRef, id: titleId, className: heading, children: title }),
727
638
  /* @__PURE__ */ jsx(
728
639
  "button",
729
640
  {
@@ -741,9 +652,10 @@ function DrawerPanel({ open, onClose, position = "right", width = "400px", title
741
652
  /* @__PURE__ */ jsx(
742
653
  "div",
743
654
  {
655
+ ref: bodyRef,
744
656
  className: "flex-1 overflow-y-auto",
745
- style: flush ? void 0 : { padding: "var(--ds-space-5)" },
746
- children
657
+ style: flush ? void 0 : { padding: `var(--ds-space-5) ${insetLessScrollbar(INSET)} var(--ds-space-5) ${INSET}` },
658
+ children: /* @__PURE__ */ jsx(SurfaceBoundary, { children })
747
659
  }
748
660
  ),
749
661
  confirm,
@@ -751,7 +663,7 @@ function DrawerPanel({ open, onClose, position = "right", width = "400px", title
751
663
  "div",
752
664
  {
753
665
  className: "shrink-0 flex items-center justify-end gap-ds-2 border-t-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
754
- style: { padding: "var(--ds-space-3) var(--ds-space-5)" },
666
+ style: { padding: `var(--ds-space-3) ${INSET}` },
755
667
  children: footer
756
668
  }
757
669
  )
@@ -819,7 +731,9 @@ var AppShell = forwardRef(function AppShell2({
819
731
  preferences,
820
732
  currentPath,
821
733
  homePath = "/",
822
- focus
734
+ navigate,
735
+ focus,
736
+ drawerWidth = "520px"
823
737
  }, ref) {
824
738
  if (REACT_VERSION_PROBLEM) throw new Error(REACT_VERSION_PROBLEM);
825
739
  const [sidebarOpen, setSidebarOpen] = useState(false);
@@ -845,6 +759,16 @@ var AppShell = forwardRef(function AppShell2({
845
759
  setUnsavedCount((n) => n - 1);
846
760
  };
847
761
  }, []);
762
+ const [leftCount, setLeftCount] = useState(0);
763
+ const markFlowLeft = useCallback(() => {
764
+ setLeftCount((n) => n + 1);
765
+ let released = false;
766
+ return () => {
767
+ if (released) return;
768
+ released = true;
769
+ setLeftCount((n) => n - 1);
770
+ };
771
+ }, []);
848
772
  const showMobileDrawer = hasMobileMenu && !(isSmallMobile && hasMobileNav);
849
773
  const [shellEl, setShellEl] = useState(null);
850
774
  const [chrome, setChrome] = useState({});
@@ -878,15 +802,20 @@ var AppShell = forwardRef(function AppShell2({
878
802
  const hasLauncher = !!launcher && !isSmallMobile;
879
803
  const launcherProps = isValidElement(launcher) ? launcher.props : null;
880
804
  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";
881
- return /* @__PURE__ */ jsx(PreferencesContext, { value: preferences ?? null, children: /* @__PURE__ */ jsx(
805
+ return /* @__PURE__ */ jsx(PreferencesContext, { value: preferences ?? null, children: /* @__PURE__ */ jsx(DrawerWidthCtx, { value: drawerWidth, children: /* @__PURE__ */ jsx(
882
806
  AppShellContext,
883
807
  {
884
- 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, focus: focus ?? null, hasUnsavedChanges: unsavedCount > 0, addUnsavedChanges },
808
+ 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, focus: focus ?? null, hasUnsavedChanges: unsavedCount > 0, addUnsavedChanges, drawerWidth, navigate: navigate ?? null, flowLeft: leftCount > 0, markFlowLeft },
885
809
  children: /* @__PURE__ */ jsxs("div", { ref: (el) => {
886
810
  setShellEl(el);
887
811
  if (typeof ref === "function") ref(el);
888
812
  else if (ref) ref.current = el;
889
- }, className: cn("relative h-screen flex flex-col bg-[var(--ds-bg-canvas)] overflow-hidden", className), children: [
813
+ }, className: cn(
814
+ // Sprint 25.16 A42: the dynamic viewport where the browser has it, so nothing hides under a phone browser's
815
+ // toolbar (100vh is the height with the toolbar hidden)
816
+ "relative h-screen supports-[height:100dvh]:h-dvh flex flex-col bg-[var(--ds-bg-canvas)] overflow-hidden",
817
+ className
818
+ ), children: [
890
819
  skipLink && /* @__PURE__ */ jsx(SkipLink, { href: `#${mainId}` }),
891
820
  layout === "sidebar-header" && navbar,
892
821
  layout === "multi-column" && navbar,
@@ -901,6 +830,7 @@ var AppShell = forwardRef(function AppShell2({
901
830
  {
902
831
  ref: mainRef,
903
832
  id: mainId,
833
+ "data-shell-main": "",
904
834
  tabIndex: -1,
905
835
  className: cn(
906
836
  "flex-1 min-h-0 focus:outline-none",
@@ -909,7 +839,7 @@ var AppShell = forwardRef(function AppShell2({
909
839
  ),
910
840
  style: { containerType: "inline-size" },
911
841
  children: scrollable ? footer ? /* @__PURE__ */ jsxs("div", { className: "min-h-full flex flex-col", children: [
912
- /* @__PURE__ */ jsx("div", { className: "flex-1", children }),
842
+ /* @__PURE__ */ jsx("div", { className: "flex-1", "data-shell-content": "", children }),
913
843
  footer
914
844
  ] }) : children : /* @__PURE__ */ jsx("div", { className: "absolute inset-0", children })
915
845
  }
@@ -935,7 +865,296 @@ var AppShell = forwardRef(function AppShell2({
935
865
  !isSmallMobile && launcher
936
866
  ] })
937
867
  }
938
- ) });
868
+ ) }) });
869
+ });
870
+ function useCrumbFollow() {
871
+ const navigate = useAppShell()?.navigate ?? null;
872
+ return useCallback((e, item) => {
873
+ if (item.href && (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)) return;
874
+ if (item.onClick) {
875
+ e.preventDefault();
876
+ item.onClick();
877
+ return;
878
+ }
879
+ if (!item.href) {
880
+ e.preventDefault();
881
+ return;
882
+ }
883
+ if (!navigate || e.defaultPrevented || !onThisSite(item.href)) return;
884
+ e.preventDefault();
885
+ navigate(item.href);
886
+ }, [navigate]);
887
+ }
888
+ function onThisSite(href) {
889
+ if (typeof window === "undefined") return false;
890
+ try {
891
+ return new URL(href, window.location.href).origin === window.location.origin;
892
+ } catch {
893
+ return false;
894
+ }
895
+ }
896
+ var crumbGoes = (item) => !!(item.href || item.onClick);
897
+ function SeparatorIcon({ type, size }) {
898
+ const iconSize = size === "sm" ? 10 : 12;
899
+ if (type === "chevron") return /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: iconSize });
900
+ if (type === "arrow") return /* @__PURE__ */ jsx(Icon, { name: "arrow-right", size: iconSize });
901
+ if (type === "dot") return /* @__PURE__ */ jsx("span", { children: "\xB7" });
902
+ return /* @__PURE__ */ jsx("span", { children: "/" });
903
+ }
904
+ var Breadcrumbs = forwardRef(function Breadcrumbs2({
905
+ items,
906
+ separator = "chevron",
907
+ variant = "inline",
908
+ itemVariant,
909
+ size = "md",
910
+ showIcons = false,
911
+ maxItems,
912
+ bleed = false,
913
+ contentMaxWidth,
914
+ className,
915
+ container,
916
+ "aria-label": label = "Breadcrumb"
917
+ }, ref) {
918
+ const [dropdownOpen, setDropdownOpen] = useState(false);
919
+ const follow = useCrumbFollow();
920
+ const [activeIdx, setActiveIdx] = useState(0);
921
+ const ellipsisBtnRef = useRef(null);
922
+ const dropdownRef = useRef(null);
923
+ const closeDropdown = useCallback(() => setDropdownOpen(false), []);
924
+ const { isTop, returnFocus } = usePopupFocus(dropdownOpen, { trigger: ellipsisBtnRef, panel: dropdownRef, close: closeDropdown, tabExits: "any" });
925
+ const pos = usePosition(ellipsisBtnRef, dropdownRef, dropdownOpen, { side: "bottom", align: "start", offset: 4, flip: true });
926
+ const effectiveMaxItems = maxItems ? Math.max(maxItems, 2) : void 0;
927
+ const shouldCollapse = effectiveMaxItems && items.length > effectiveMaxItems;
928
+ const collapsedItems = shouldCollapse ? items.slice(1, -(effectiveMaxItems - 1)) : [];
929
+ const visibleItems = shouldCollapse ? [...items.slice(0, 1), null, ...items.slice(-(effectiveMaxItems - 1))] : items;
930
+ useEffect(() => {
931
+ if (!dropdownOpen) return;
932
+ const handler = (e) => {
933
+ const target = e.target;
934
+ if (ellipsisBtnRef.current?.contains(target) || dropdownRef.current?.contains(target)) return;
935
+ setDropdownOpen(false);
936
+ };
937
+ const onKeyDown = (e) => {
938
+ if (e.key !== "Escape" || e.defaultPrevented || !isTop()) return;
939
+ e.preventDefault();
940
+ returnFocus();
941
+ setDropdownOpen(false);
942
+ };
943
+ document.addEventListener("mousedown", handler);
944
+ document.addEventListener("keydown", onKeyDown);
945
+ return () => {
946
+ document.removeEventListener("mousedown", handler);
947
+ document.removeEventListener("keydown", onKeyDown);
948
+ };
949
+ }, [dropdownOpen, isTop, returnFocus]);
950
+ useEffect(() => {
951
+ if (!dropdownOpen) return;
952
+ const id = requestAnimationFrame(() => dropdownRef.current?.querySelector(`[data-index="${activeIdx}"]`)?.focus());
953
+ return () => cancelAnimationFrame(id);
954
+ }, [dropdownOpen, activeIdx]);
955
+ function onMenuKeyDown(e) {
956
+ const last = collapsedItems.length - 1;
957
+ const next = e.key === "ArrowDown" ? activeIdx >= last ? 0 : activeIdx + 1 : e.key === "ArrowUp" ? activeIdx <= 0 ? last : activeIdx - 1 : e.key === "Home" ? 0 : e.key === "End" ? last : null;
958
+ if (next === null) return;
959
+ e.preventDefault();
960
+ setActiveIdx(next);
961
+ }
962
+ const textClass = size === "sm" ? "text-xs" : "text-sm";
963
+ const gapClass = size === "sm" ? "gap-ds-1" : "gap-ds-1.5";
964
+ const iconSize = size === "sm" ? 12 : 14;
965
+ const isContainedBleed = variant === "contained" && bleed;
966
+ const wrapperClasses = cn(
967
+ variant === "contained" && !bleed && cn(
968
+ "bg-[var(--ds-bg-muted)] rounded-[var(--ds-radius-md)]",
969
+ size === "sm" ? "py-ds-1 px-ds-2" : "py-ds-1.5 px-ds-3"
970
+ ),
971
+ !isContainedBleed && className
972
+ );
973
+ function renderItem(item, _index, isLast) {
974
+ const isPill = itemVariant === "pill" || variant === "pill" && !itemVariant;
975
+ const insideContained = variant === "contained";
976
+ const pillClasses = isPill ? cn(
977
+ "rounded-[var(--ds-radius-full)]",
978
+ insideContained ? "bg-[var(--ds-bg-surface)]" : "bg-[var(--ds-bg-muted)]",
979
+ size === "sm" ? "px-ds-2 py-[2px]" : "px-ds-2.5 py-ds-0.5"
980
+ ) : "";
981
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
982
+ showIcons && item.icon && /* @__PURE__ */ jsx(Icon, { name: item.icon, size: iconSize, className: "shrink-0" }),
983
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: item.label })
984
+ ] });
985
+ if (!crumbGoes(item)) {
986
+ return /* @__PURE__ */ jsx(
987
+ "span",
988
+ {
989
+ "aria-current": isLast ? "page" : void 0,
990
+ className: cn(
991
+ "flex items-center",
992
+ isLast ? "font-medium text-[var(--ds-text-primary)]" : "text-[var(--ds-text-secondary)]",
993
+ gapClass,
994
+ pillClasses
995
+ ),
996
+ children: content
997
+ }
998
+ );
999
+ }
1000
+ return /* @__PURE__ */ jsx(
1001
+ "a",
1002
+ {
1003
+ href: item.href ?? "#",
1004
+ onClick: (e) => follow(e, item),
1005
+ className: cn(
1006
+ "text-[var(--ds-text-secondary)] flex items-center",
1007
+ "hover:text-[var(--ds-text-primary)]",
1008
+ "transition-colors duration-[var(--ds-transition-speed)]",
1009
+ gapClass,
1010
+ pillClasses
1011
+ ),
1012
+ children: content
1013
+ }
1014
+ );
1015
+ }
1016
+ const listContent = /* @__PURE__ */ jsx("ol", { className: cn("flex items-center flex-wrap", gapClass, textClass), children: visibleItems.map((item, i) => {
1017
+ if (item === null) {
1018
+ return /* @__PURE__ */ jsxs("li", { className: cn("flex items-center", gapClass), children: [
1019
+ i > 0 && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "text-[var(--ds-text-secondary)] select-none flex items-center", children: /* @__PURE__ */ jsx(SeparatorIcon, { type: separator, size }) }),
1020
+ /* @__PURE__ */ jsx(
1021
+ "button",
1022
+ {
1023
+ type: "button",
1024
+ ref: ellipsisBtnRef,
1025
+ onClick: () => {
1026
+ if (!dropdownOpen) setActiveIdx(0);
1027
+ setDropdownOpen(!dropdownOpen);
1028
+ },
1029
+ "aria-label": "Show hidden breadcrumbs",
1030
+ "aria-expanded": dropdownOpen,
1031
+ "aria-haspopup": "menu",
1032
+ className: cn(
1033
+ "text-[var(--ds-text-secondary)] hover:text-[var(--ds-text-primary)]",
1034
+ "transition-colors duration-[var(--ds-transition-speed)] cursor-pointer",
1035
+ "flex items-center",
1036
+ (itemVariant === "pill" || variant === "pill" && !itemVariant) && cn(
1037
+ "rounded-[var(--ds-radius-full)]",
1038
+ variant === "contained" ? "bg-[var(--ds-bg-surface)]" : "bg-[var(--ds-bg-muted)]",
1039
+ size === "sm" ? "px-ds-2 py-[2px]" : "px-ds-2.5 py-ds-0.5"
1040
+ )
1041
+ ),
1042
+ children: "\xB7\xB7\xB7"
1043
+ }
1044
+ )
1045
+ ] }, "__ellipsis");
1046
+ }
1047
+ const isLast = i === visibleItems.length - 1;
1048
+ return /* @__PURE__ */ jsxs("li", { className: cn("flex items-center", gapClass), children: [
1049
+ i > 0 && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "text-[var(--ds-text-secondary)] select-none flex items-center", children: /* @__PURE__ */ jsx(SeparatorIcon, { type: separator, size }) }),
1050
+ renderItem(item, i, isLast)
1051
+ ] }, i);
1052
+ }) });
1053
+ const dropdown = dropdownOpen && collapsedItems.length > 0 && createPortal(
1054
+ /* @__PURE__ */ jsx(
1055
+ "div",
1056
+ {
1057
+ ref: dropdownRef,
1058
+ role: "menu",
1059
+ "aria-label": "Hidden breadcrumbs",
1060
+ onKeyDown: onMenuKeyDown,
1061
+ className: cn(
1062
+ "fixed z-[var(--ds-z-popover)] min-w-[160px] max-h-60 overflow-auto",
1063
+ "bg-[var(--ds-bg-surface-elevated)]",
1064
+ "[backdrop-filter:var(--ds-glass-filter)] [-webkit-backdrop-filter:var(--ds-glass-filter)]",
1065
+ "border-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
1066
+ "rounded-[var(--ds-radius-md)]",
1067
+ "shadow-[var(--ds-shadow-dropdown),var(--ds-glass-inset)]",
1068
+ "py-ds-1"
1069
+ ),
1070
+ style: { top: pos.top, left: pos.left },
1071
+ children: collapsedItems.map((ci, idx) => {
1072
+ const itemClass = cn(
1073
+ "flex items-center w-full text-left text-sm cursor-pointer",
1074
+ "px-ds-3 py-ds-1.5 transition-colors",
1075
+ "text-[var(--ds-text-secondary)]",
1076
+ "hover:bg-[var(--ds-bg-muted)] hover:text-[var(--ds-text-primary)]",
1077
+ "focus-visible:outline-none focus-visible:bg-[var(--ds-bg-muted)] focus-visible:text-[var(--ds-text-primary)] focus-visible:shadow-[inset_var(--ds-shadow-focus)]",
1078
+ gapClass
1079
+ );
1080
+ const label2 = /* @__PURE__ */ jsxs(Fragment, { children: [
1081
+ showIcons && ci.icon && /* @__PURE__ */ jsx(Icon, { name: ci.icon, size: iconSize, className: "shrink-0" }),
1082
+ ci.label
1083
+ ] });
1084
+ return ci.href && !ci.onClick ? /* @__PURE__ */ jsx(
1085
+ "a",
1086
+ {
1087
+ "data-index": idx,
1088
+ role: "menuitem",
1089
+ href: ci.href,
1090
+ tabIndex: idx === activeIdx ? 0 : -1,
1091
+ className: itemClass,
1092
+ onClick: (e) => {
1093
+ setDropdownOpen(false);
1094
+ follow(e, ci);
1095
+ },
1096
+ onKeyDown: (e) => {
1097
+ if (e.key === " ") {
1098
+ e.preventDefault();
1099
+ e.currentTarget.click();
1100
+ }
1101
+ },
1102
+ children: label2
1103
+ },
1104
+ idx
1105
+ ) : /* @__PURE__ */ jsx(
1106
+ "button",
1107
+ {
1108
+ type: "button",
1109
+ "data-index": idx,
1110
+ role: "menuitem",
1111
+ tabIndex: idx === activeIdx ? 0 : -1,
1112
+ "aria-disabled": !crumbGoes(ci) || void 0,
1113
+ className: itemClass,
1114
+ onClick: () => {
1115
+ if (!ci.onClick) return;
1116
+ setDropdownOpen(false);
1117
+ ci.onClick();
1118
+ },
1119
+ children: label2
1120
+ },
1121
+ idx
1122
+ );
1123
+ })
1124
+ }
1125
+ ),
1126
+ container ?? document.body
1127
+ );
1128
+ if (isContainedBleed) {
1129
+ return /* @__PURE__ */ jsxs(
1130
+ "nav",
1131
+ {
1132
+ ref,
1133
+ "aria-label": label,
1134
+ className: cn(
1135
+ "w-full bg-[var(--ds-bg-muted)]",
1136
+ // Hairline under the band: it sits within ~1.2:1 of both canvas and a tinted header, so the edge needs a line.
1137
+ "border-b-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
1138
+ className
1139
+ ),
1140
+ children: [
1141
+ /* @__PURE__ */ jsx(
1142
+ "div",
1143
+ {
1144
+ className: cn("w-full mx-auto px-ds-6", size === "sm" ? "py-ds-1" : "py-ds-1.5"),
1145
+ style: { maxWidth: contentMaxWidth ?? "var(--ds-bp-xl)" },
1146
+ children: listContent
1147
+ }
1148
+ ),
1149
+ dropdown
1150
+ ]
1151
+ }
1152
+ );
1153
+ }
1154
+ return /* @__PURE__ */ jsxs("nav", { ref, "aria-label": label, className: wrapperClasses, children: [
1155
+ listContent,
1156
+ dropdown
1157
+ ] });
939
1158
  });
940
1159
  function useCompact(ref, breakpoint, band = 0) {
941
1160
  const px = typeof breakpoint === "number" ? breakpoint : readBreakpointPx(breakpoint);
@@ -964,6 +1183,6 @@ function useElementWidth(ref) {
964
1183
  return width;
965
1184
  }
966
1185
 
967
- export { AppShell, DiscardConfirm, Drawer, DrawerFooterCtx, OVERLAY_ROOT_ATTR, OverlayCloseCtx, OverlayMarkCtx, OverlayMarkScope, REACT_VERSION_PROBLEM, ResizeEdge, Sheet, SkipLink, defaultMaxWidth, pxOf, useAppShell, useBelow, useCloseSettle, useCompact, useDirtyGuard, useElementWidth, useOverlayMark, useRememberedWidth, useUnsavedChanges };
968
- //# sourceMappingURL=chunk-AAYCQXZQ.js.map
969
- //# sourceMappingURL=chunk-AAYCQXZQ.js.map
1186
+ export { AppShell, Breadcrumbs, DiscardConfirm, Drawer, DrawerFooterCtx, DrawerPanel, OVERLAY_ROOT_ATTR, OverlayCloseCtx, OverlayMarkCtx, OverlayMarkScope, REACT_VERSION_PROBLEM, ResizeEdge, SkipLink, crumbGoes, defaultMaxWidth, insetLessScrollbar, pxOf, useAppShell, useBelow, useCloseSettle, useCompact, useCrumbFollow, useDirtyGuard, useElementWidth, useOverlayMark, useRememberedWidth, useScrollbarInset, useUnsavedChanges };
1187
+ //# sourceMappingURL=chunk-MV7JXWXK.js.map
1188
+ //# sourceMappingURL=chunk-MV7JXWXK.js.map