gridsmith-ui 0.14.0 → 0.16.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 (60) hide show
  1. package/dist/charts.d.ts +20 -1
  2. package/dist/charts.js +65 -29
  3. package/dist/charts.js.map +1 -1
  4. package/dist/chat.js +5 -5
  5. package/dist/{chunk-JSQPVRHQ.js → chunk-2U2X6E2X.js} +39 -16
  6. package/dist/chunk-2U2X6E2X.js.map +1 -0
  7. package/dist/{chunk-X4CNZFWC.js → chunk-4LDYR7QP.js} +7 -7
  8. package/dist/chunk-4LDYR7QP.js.map +1 -0
  9. package/dist/chunk-5XOJZQIP.js +427 -0
  10. package/dist/chunk-5XOJZQIP.js.map +1 -0
  11. package/dist/{chunk-PKLVP5ZB.js → chunk-7Q46JXSK.js} +5 -4
  12. package/dist/chunk-7Q46JXSK.js.map +1 -0
  13. package/dist/{chunk-IYD6ZDM2.js → chunk-EMCSARQO.js} +7 -6
  14. package/dist/chunk-EMCSARQO.js.map +1 -0
  15. package/dist/{chunk-CR2KF73R.js → chunk-OENJCGTJ.js} +3 -3
  16. package/dist/{chunk-CR2KF73R.js.map → chunk-OENJCGTJ.js.map} +1 -1
  17. package/dist/{chunk-U2JT37GJ.js → chunk-PVTGMCVR.js} +4 -4
  18. package/dist/{chunk-U2JT37GJ.js.map → chunk-PVTGMCVR.js.map} +1 -1
  19. package/dist/{chunk-QJBCZ7BR.js → chunk-XGSQOMEY.js} +4 -4
  20. package/dist/{chunk-QJBCZ7BR.js.map → chunk-XGSQOMEY.js.map} +1 -1
  21. package/dist/{chunk-M3AARGLI.js → chunk-Z63OSN64.js} +9 -8
  22. package/dist/chunk-Z63OSN64.js.map +1 -0
  23. package/dist/data-grid.js +4 -4
  24. package/dist/editor.js +32 -8
  25. package/dist/editor.js.map +1 -1
  26. package/dist/{feather-B65FI6CY.js → feather-2JFSFLXN.js} +3 -2
  27. package/dist/feather-2JFSFLXN.js.map +1 -0
  28. package/dist/{heroicons-DIR7PXCR.js → heroicons-4AQ4YBVG.js} +3 -2
  29. package/dist/heroicons-4AQ4YBVG.js.map +1 -0
  30. package/dist/icons.d.ts +1 -1
  31. package/dist/icons.js +1 -1
  32. package/dist/index.d.ts +75 -6
  33. package/dist/index.js +1371 -603
  34. package/dist/index.js.map +1 -1
  35. package/dist/kanban.d.ts +11 -1
  36. package/dist/kanban.js +227 -115
  37. package/dist/kanban.js.map +1 -1
  38. package/dist/{lucide-JH2R3TFV.js → lucide-RZC2FEMS.js} +3 -2
  39. package/dist/lucide-RZC2FEMS.js.map +1 -0
  40. package/dist/{phosphor-HCVUMVT5.js → phosphor-CEN25FLM.js} +3 -2
  41. package/dist/phosphor-CEN25FLM.js.map +1 -0
  42. package/dist/resizable.d.ts +11 -1
  43. package/dist/resizable.js +81 -23
  44. package/dist/resizable.js.map +1 -1
  45. package/dist/roots.css +11 -8
  46. package/dist/{tabler-HHIXSM73.js → tabler-YXXC7Y6B.js} +3 -2
  47. package/dist/tabler-YXXC7Y6B.js.map +1 -0
  48. package/package.json +1 -1
  49. package/dist/chunk-IYD6ZDM2.js.map +0 -1
  50. package/dist/chunk-JSQPVRHQ.js.map +0 -1
  51. package/dist/chunk-M3AARGLI.js.map +0 -1
  52. package/dist/chunk-NHQCMOAN.js +0 -137
  53. package/dist/chunk-NHQCMOAN.js.map +0 -1
  54. package/dist/chunk-PKLVP5ZB.js.map +0 -1
  55. package/dist/chunk-X4CNZFWC.js.map +0 -1
  56. package/dist/feather-B65FI6CY.js.map +0 -1
  57. package/dist/heroicons-DIR7PXCR.js.map +0 -1
  58. package/dist/lucide-JH2R3TFV.js.map +0 -1
  59. package/dist/phosphor-HCVUMVT5.js.map +0 -1
  60. package/dist/tabler-HHIXSM73.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,25 +1,25 @@
1
- import { useAppShell, ControlSizeContext, Button, Spinner, OverlayCloseCtx, DrawerFooterCtx, useCompact, useElementWidth, Sheet, useCloseSettle, Drawer, useRememberedWidth, pxOf, ResizeEdge, useDirtyGuard, DiscardConfirm, defaultMaxWidth } from './chunk-JSQPVRHQ.js';
2
- export { AppShell, Button, ButtonSizeContext, ControlSizeContext, Drawer, Sheet, SkipLink, Spinner, useAppShell } from './chunk-JSQPVRHQ.js';
3
- import { Skeleton } from './chunk-M3AARGLI.js';
4
- export { Skeleton } from './chunk-M3AARGLI.js';
5
- import { useOverlayLayer, useCloseOnFocusOut, focusInitial, rovingTabStop, OVERLAY_CLOSE_ATTR, useNonModalDialog, useFocusTrap } from './chunk-NHQCMOAN.js';
6
- import { Link } from './chunk-QJBCZ7BR.js';
7
- export { CodeBlock, Divider, Link } from './chunk-QJBCZ7BR.js';
8
- import { Badge } from './chunk-U2JT37GJ.js';
9
- export { Badge } from './chunk-U2JT37GJ.js';
10
- import { inferColumnKinds, CardValue } from './chunk-CR2KF73R.js';
1
+ import { useAppShell, ControlSizeContext, Button, useDirtyGuard, OverlayCloseCtx, DiscardConfirm, Spinner, DrawerFooterCtx, useCompact, useElementWidth, Sheet, useCloseSettle, Drawer, useRememberedWidth, pxOf, ResizeEdge, defaultMaxWidth } from './chunk-2U2X6E2X.js';
2
+ export { AppShell, Button, ButtonSizeContext, ControlSizeContext, Drawer, Sheet, SkipLink, Spinner, useAppShell, useUnsavedChanges } from './chunk-2U2X6E2X.js';
3
+ import { Skeleton } from './chunk-Z63OSN64.js';
4
+ export { Skeleton } from './chunk-Z63OSN64.js';
5
+ import { FOCUSABLE, usePopupFocus, useOverlayLayer, useFocusTrap, useFocusBeforeOpen, rememberFocus, focusInitial, OVERLAY_CLOSE_ATTR, focusAfterRemoval, rovingTabStop, useGridKeyboard, useNonModalDialog, usePressMarks, useReturnFocus, useCloseOnFocusOut } from './chunk-5XOJZQIP.js';
6
+ import { Link } from './chunk-XGSQOMEY.js';
7
+ export { CodeBlock, Divider, Link } from './chunk-XGSQOMEY.js';
8
+ import { Badge } from './chunk-PVTGMCVR.js';
9
+ export { Badge } from './chunk-PVTGMCVR.js';
10
+ import { inferColumnKinds, CardValue } from './chunk-OENJCGTJ.js';
11
11
  import { SplitPaneCtx, useShellPreferences, TableCardCtx, PREFERENCE_HOUSE_DEFAULTS, preferenceActionKey } from './chunk-TKVTZWBN.js';
12
12
  export { PreferencesContext, preferenceActionKey, usePreferences } from './chunk-TKVTZWBN.js';
13
- import { useBreakpoint } from './chunk-CRQ6KCRD.js';
13
+ import { useBreakpoint, readBreakpointPx } from './chunk-CRQ6KCRD.js';
14
14
  export { BreakpointWidthProvider, useBreakpoint } from './chunk-CRQ6KCRD.js';
15
- import { useTextOverflow, Tooltip, usePosition } from './chunk-IYD6ZDM2.js';
16
- export { Tooltip, useTextOverflow } from './chunk-IYD6ZDM2.js';
17
- import { mergeRefs, Slot } from './chunk-PKLVP5ZB.js';
15
+ import { useTextOverflow, Tooltip, usePosition } from './chunk-EMCSARQO.js';
16
+ export { Tooltip, useTextOverflow } from './chunk-EMCSARQO.js';
17
+ import { mergeRefs, Slot } from './chunk-7Q46JXSK.js';
18
18
  import { useLatest } from './chunk-WH2FUFAC.js';
19
- import { Icon } from './chunk-X4CNZFWC.js';
20
- import { cn, GLASS_SURFACE, ACTIVE_RING, GLASS_BACKDROP, GLASS_DROPDOWN, ERROR_ACTIVE_RING, ERROR_FOCUS_RING, FOCUS_RING, ERROR_HAS_FOCUS_RING, HAS_FOCUS_RING, ERROR_FOCUS_WITHIN_RING, FOCUS_WITHIN_RING, lockBodyScroll, unlockBodyScroll, GLASS_OVERLAY } from './chunk-4KVFH5KC.js';
21
- import { createContext, forwardRef, useState, useContext, memo, useEffect, useRef, useCallback, useLayoutEffect, Children, useMemo, useId, isValidElement, cloneElement, Fragment as Fragment$1, useSyncExternalStore } from 'react';
22
- import { createPortal } from 'react-dom';
19
+ import { Icon } from './chunk-4LDYR7QP.js';
20
+ import { cn, GLASS_SURFACE, ACTIVE_RING, GLASS_BACKDROP, GLASS_DROPDOWN, lockBodyScroll, unlockBodyScroll, GLASS_OVERLAY, ERROR_ACTIVE_RING, ERROR_FOCUS_RING, FOCUS_RING, ERROR_HAS_FOCUS_RING, HAS_FOCUS_RING, ERROR_FOCUS_WITHIN_RING, FOCUS_WITHIN_RING } from './chunk-4KVFH5KC.js';
21
+ import { createContext, forwardRef, useState, useRef, useCallback, useLayoutEffect, useMemo, useContext, useId, memo, useEffect, Children, isValidElement, cloneElement, Fragment as Fragment$1, useSyncExternalStore } from 'react';
22
+ import { createPortal, flushSync } from 'react-dom';
23
23
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
24
24
 
25
25
  function isDivider(entry) {
@@ -40,15 +40,14 @@ function DropdownMenu({ trigger, items, align = "end", className, container }) {
40
40
  setPrevOpen(open);
41
41
  if (!open) setFocusedIdx(-1);
42
42
  }
43
- const isTopLayer = useOverlayLayer(open);
44
- useCloseOnFocusOut(open, triggerRef, menuRef, () => setOpen(false), isTopLayer);
43
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: menuRef, close: () => setOpen(false), tabExits: "any" });
45
44
  useEffect(() => {
46
45
  if (!open) return;
47
46
  const onKeyDown = (e) => {
48
- if (e.key === "Escape" && isTopLayer()) {
49
- setOpen(false);
50
- triggerRef.current?.focus();
51
- }
47
+ if (e.key !== "Escape" || e.defaultPrevented || !isTopLayer()) return;
48
+ e.preventDefault();
49
+ returnFocus();
50
+ setOpen(false);
52
51
  };
53
52
  const onClick = (e) => {
54
53
  const target = e.target;
@@ -61,11 +60,10 @@ function DropdownMenu({ trigger, items, align = "end", className, container }) {
61
60
  document.removeEventListener("keydown", onKeyDown);
62
61
  document.removeEventListener("mousedown", onClick);
63
62
  };
64
- }, [open, isTopLayer]);
63
+ }, [open, isTopLayer, returnFocus]);
65
64
  useEffect(() => {
66
65
  if (open && focusedIdx >= 0) {
67
- const buttons = menuRef.current?.querySelectorAll('[role="menuitem"]');
68
- buttons?.[focusedIdx]?.focus();
66
+ menuRef.current?.querySelector(`[data-index="${focusedIdx}"]`)?.focus();
69
67
  }
70
68
  }, [open, focusedIdx]);
71
69
  function handleMenuKeyDown(e) {
@@ -108,6 +106,7 @@ function DropdownMenu({ trigger, items, align = "end", className, container }) {
108
106
  className
109
107
  ),
110
108
  style: { top: pos.top, left: pos.left },
109
+ onClick: (e) => e.stopPropagation(),
111
110
  children: items.map((item, i) => {
112
111
  if (isDivider(item)) {
113
112
  return /* @__PURE__ */ jsx("div", { role: "separator", className: "my-ds-1 h-px bg-[var(--ds-border-default)]" }, i);
@@ -115,11 +114,13 @@ function DropdownMenu({ trigger, items, align = "end", className, container }) {
115
114
  return /* @__PURE__ */ jsxs(
116
115
  "button",
117
116
  {
117
+ "data-index": i,
118
118
  role: "menuitem",
119
119
  disabled: item.disabled,
120
120
  tabIndex: focusedIdx === i ? 0 : -1,
121
121
  onClick: () => {
122
122
  if (!item.disabled) {
123
+ returnFocus();
123
124
  item.onClick();
124
125
  setOpen(false);
125
126
  }
@@ -465,13 +466,7 @@ function Dropdown({
465
466
  useEffect(() => {
466
467
  if (!open) return;
467
468
  updatePos();
468
- requestAnimationFrame(() => {
469
- if (searchRef.current) {
470
- searchRef.current.focus();
471
- } else {
472
- const els = Array.from(menuRef.current?.querySelectorAll('[role="option"]') ?? []);
473
- (els.find((el) => el.getAttribute("aria-selected") === "true") ?? els[0])?.focus();
474
- }
469
+ const raf = requestAnimationFrame(() => {
475
470
  updatePos();
476
471
  setPositioned(true);
477
472
  });
@@ -479,10 +474,22 @@ function Dropdown({
479
474
  window.addEventListener("scroll", onScroll, true);
480
475
  window.addEventListener("resize", updatePos);
481
476
  return () => {
477
+ cancelAnimationFrame(raf);
482
478
  window.removeEventListener("scroll", onScroll, true);
483
479
  window.removeEventListener("resize", updatePos);
484
480
  };
485
481
  }, [open, updatePos]);
482
+ const moved = useRef(false);
483
+ useLayoutEffect(() => {
484
+ if (!open || !positioned) return;
485
+ moved.current = false;
486
+ if (searchRef.current) {
487
+ searchRef.current.focus();
488
+ return;
489
+ }
490
+ const els = Array.from(menuRef.current?.querySelectorAll('[role="option"]') ?? []);
491
+ (els.find((el) => el.getAttribute("aria-selected") === "true") ?? els[0])?.focus();
492
+ }, [open, positioned]);
486
493
  useEffect(() => {
487
494
  if (!open) return;
488
495
  const handler = (e) => {
@@ -493,7 +500,13 @@ function Dropdown({
493
500
  document.addEventListener("mousedown", handler);
494
501
  return () => document.removeEventListener("mousedown", handler);
495
502
  }, [open]);
496
- const isTopLayer = useOverlayLayer(open);
503
+ const commitMoved = () => {
504
+ if (multiple || !moved.current) return;
505
+ const el = document.activeElement;
506
+ const id = el instanceof HTMLElement && menuRef.current?.contains(el) ? el.getAttribute("data-option-id") : null;
507
+ if (id) onChange(id);
508
+ };
509
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: menuRef, close: () => setOpen(false), tabExits: "any", onTabOut: commitMoved });
497
510
  const typeahead = useRef({ buffer: "", at: 0 });
498
511
  useEffect(() => {
499
512
  if (!open) return;
@@ -502,6 +515,7 @@ function Dropdown({
502
515
  const els = optionButtons();
503
516
  if (els.length === 0) return;
504
517
  els[(idx % els.length + els.length) % els.length].focus();
518
+ moved.current = true;
505
519
  };
506
520
  const onKey = (e) => {
507
521
  const inMenu = menuRef.current?.contains(document.activeElement) || triggerRef.current === document.activeElement;
@@ -509,10 +523,10 @@ function Dropdown({
509
523
  const els = optionButtons();
510
524
  const current = els.findIndex((el) => el === document.activeElement);
511
525
  if (e.key === "Escape") {
512
- if (!isTopLayer()) return;
526
+ if (e.defaultPrevented || !isTopLayer()) return;
513
527
  e.preventDefault();
528
+ returnFocus();
514
529
  setOpen(false);
515
- triggerRef.current?.focus();
516
530
  return;
517
531
  }
518
532
  if (e.key === "ArrowDown") {
@@ -536,10 +550,6 @@ function Dropdown({
536
550
  focusOption(els.length - 1);
537
551
  return;
538
552
  }
539
- if (e.key === "Tab") {
540
- setOpen(false);
541
- return;
542
- }
543
553
  if (!searchable && e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
544
554
  const now = Date.now();
545
555
  typeahead.current.buffer = now - typeahead.current.at > 700 ? e.key : typeahead.current.buffer + e.key;
@@ -554,7 +564,7 @@ function Dropdown({
554
564
  };
555
565
  document.addEventListener("keydown", onKey);
556
566
  return () => document.removeEventListener("keydown", onKey);
557
- }, [open, searchable, isTopLayer]);
567
+ }, [open, searchable, isTopLayer, returnFocus]);
558
568
  const filtered = searchable && search ? options.filter((o) => o.label.toLowerCase().includes(search.toLowerCase())) : options;
559
569
  const groups = /* @__PURE__ */ new Map();
560
570
  for (const opt of filtered) {
@@ -568,10 +578,21 @@ function Dropdown({
568
578
  const arr = selectedValues.includes(id) ? selectedValues.filter((v) => v !== id) : [...selectedValues, id];
569
579
  onChange(arr);
570
580
  } else {
581
+ returnFocus();
571
582
  onChange(id);
572
583
  setOpen(false);
573
584
  }
574
585
  };
586
+ const chipsRef = useRef(null);
587
+ const pendingChipFocus = useRef(null);
588
+ useLayoutEffect(() => {
589
+ const pending = pendingChipFocus.current;
590
+ if (pending === null) return;
591
+ pendingChipFocus.current = null;
592
+ if (selectedValues.includes(pending.value)) return;
593
+ const chips = Array.from(chipsRef.current?.querySelectorAll("[data-chip-remove]") ?? []);
594
+ focusAfterRemoval(chips, pending.index, triggerRef.current);
595
+ });
575
596
  const displayLabel = multiple ? selectedValues.length > 0 ? `${selectedValues.length} selected` : placeholder : options.find((o) => o.id === selectedValues[0])?.label ?? placeholder;
576
597
  const { ref: overflowRef, isTruncated } = useTextOverflow({ lines: 1, content: displayLabel });
577
598
  return /* @__PURE__ */ jsxs("div", { ref, className: cn("relative", className), children: [
@@ -620,6 +641,7 @@ function Dropdown({
620
641
  "div",
621
642
  {
622
643
  ref: menuRef,
644
+ onClick: (e) => e.stopPropagation(),
623
645
  className: cn(
624
646
  "fixed z-[var(--ds-z-popover)]",
625
647
  "bg-[var(--ds-bg-surface-elevated)]",
@@ -670,7 +692,7 @@ function Dropdown({
670
692
  ),
671
693
  container ?? document.body
672
694
  ),
673
- multiple && showChips && selectedValues.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-ds-1 mt-ds-1.5", children: selectedValues.map((v) => {
695
+ multiple && showChips && selectedValues.length > 0 && /* @__PURE__ */ jsx("div", { ref: chipsRef, className: "flex flex-wrap gap-ds-1 mt-ds-1.5", children: selectedValues.map((v, i) => {
674
696
  const opt = options.find((o) => o.id === v);
675
697
  return opt ? /* @__PURE__ */ jsxs(
676
698
  "span",
@@ -681,7 +703,11 @@ function Dropdown({
681
703
  /* @__PURE__ */ jsx(
682
704
  "button",
683
705
  {
684
- onClick: () => handleSelect(v),
706
+ "data-chip-remove": "",
707
+ onClick: () => {
708
+ pendingChipFocus.current = { index: i, value: v };
709
+ handleSelect(v);
710
+ },
685
711
  className: "hover:text-[var(--ds-text-primary)] cursor-pointer",
686
712
  "aria-label": `Remove ${opt.label}`,
687
713
  children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 10 })
@@ -706,6 +732,7 @@ function OptionButton({
706
732
  {
707
733
  type: "button",
708
734
  role: "option",
735
+ "data-option-id": opt.id,
709
736
  "aria-selected": isSelected,
710
737
  tabIndex: -1,
711
738
  onClick: () => onSelect(opt.id),
@@ -750,6 +777,7 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
750
777
  useOverlayLayer(open);
751
778
  const [query, setQuery] = useState("");
752
779
  const [highlightIdx, setHighlightIdx] = useState(0);
780
+ const [moved, setMoved] = useState(false);
753
781
  const inputRef = useRef(null);
754
782
  const triggerRef = useRef(null);
755
783
  const [triggerWidth, setTriggerWidth] = useState(void 0);
@@ -766,6 +794,14 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
766
794
  setPrevQuery(query);
767
795
  setHighlightIdx(0);
768
796
  }
797
+ const [prevOpen, setPrevOpen] = useState(open);
798
+ if (open !== prevOpen) {
799
+ setPrevOpen(open);
800
+ if (open && query === "") {
801
+ setHighlightIdx(Math.max(0, filtered.findIndex((o) => o.value === value)));
802
+ setMoved(false);
803
+ }
804
+ }
769
805
  useEffect(() => {
770
806
  if (!open) return;
771
807
  const handler = (e) => {
@@ -781,7 +817,6 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
781
817
  onChange(val);
782
818
  setQuery("");
783
819
  setOpen(false);
784
- inputRef.current?.blur();
785
820
  },
786
821
  [onChange]
787
822
  );
@@ -798,6 +833,7 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
798
833
  return;
799
834
  }
800
835
  setHighlightIdx((prev) => Math.min(prev + 1, filtered.length - 1));
836
+ setMoved(true);
801
837
  } else if (e.key === "ArrowUp") {
802
838
  e.preventDefault();
803
839
  if (!open) {
@@ -805,15 +841,23 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
805
841
  return;
806
842
  }
807
843
  setHighlightIdx((prev) => Math.max(prev - 1, 0));
844
+ setMoved(true);
808
845
  } else if (e.key === "Enter" && open && filtered[highlightIdx]) {
809
846
  e.preventDefault();
810
847
  handleSelect(filtered[highlightIdx].value);
811
- } else if (e.key === "Escape") {
848
+ } else if (e.key === "Tab" && open) {
849
+ if (moved && filtered[highlightIdx]) onChange(filtered[highlightIdx].value);
850
+ flushSync(() => {
851
+ setOpen(false);
852
+ setQuery("");
853
+ });
854
+ } else if (e.key === "Escape" && open) {
855
+ e.preventDefault();
812
856
  setOpen(false);
813
857
  setQuery("");
814
858
  }
815
859
  },
816
- [open, filtered, highlightIdx, handleSelect, openList]
860
+ [open, moved, filtered, highlightIdx, handleSelect, openList, onChange]
817
861
  );
818
862
  const handleBlur = useCallback((e) => {
819
863
  const next = e.relatedTarget;
@@ -858,11 +902,13 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
858
902
  "aria-expanded": open,
859
903
  "aria-controls": listboxId,
860
904
  "aria-autocomplete": "list",
905
+ "aria-activedescendant": open && filtered[highlightIdx] ? `${listboxId}-opt-${highlightIdx}` : void 0,
861
906
  value: open ? query : selectedLabel,
862
907
  placeholder,
863
908
  disabled,
864
909
  onChange: (e) => {
865
910
  setQuery(e.target.value);
911
+ setMoved(true);
866
912
  if (!open) setOpen(true);
867
913
  },
868
914
  onFocus: (e) => e.currentTarget.select(),
@@ -917,8 +963,10 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
917
963
  children: /* @__PURE__ */ jsx("div", { style: { padding: "var(--ds-space-1)" }, children: loading ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-ds-4", children: /* @__PURE__ */ jsx(Spinner, { size: "sm", label: "Loading options" }) }) : filtered.length === 0 ? /* @__PURE__ */ jsx("div", { className: "px-ds-2 py-ds-3 text-sm text-[var(--ds-text-muted)] text-center", children: "No results" }) : filtered.map((opt, i) => /* @__PURE__ */ jsxs(
918
964
  "button",
919
965
  {
966
+ id: `${listboxId}-opt-${i}`,
920
967
  role: "option",
921
- "aria-selected": opt.value === value,
968
+ tabIndex: -1,
969
+ "aria-selected": i === highlightIdx,
922
970
  onMouseDown: (e) => e.preventDefault(),
923
971
  onClick: () => handleSelect(opt.value),
924
972
  onMouseEnter: () => setHighlightIdx(i),
@@ -940,10 +988,7 @@ function Combobox({ value, onChange, options, placeholder = "Search\u2026", disa
940
988
  container ?? document.body
941
989
  )
942
990
  ] });
943
- if (labelTruncated && !open) {
944
- return /* @__PURE__ */ jsx(Tooltip, { content: selectedLabel, children: triggerContent });
945
- }
946
- return triggerContent;
991
+ return /* @__PURE__ */ jsx(Tooltip, { content: selectedLabel, disabled: !labelTruncated || open, children: triggerContent });
947
992
  }
948
993
  var CHECKBOX_SIZES = {
949
994
  sm: { box: "h-[16px] w-[16px]", glyph: 10, label: "text-xs" },
@@ -1125,6 +1170,42 @@ function RadioGroup({ value, onChange, options, orientation = "vertical", classN
1125
1170
  ] });
1126
1171
  }
1127
1172
 
1173
+ // ../../showcase/src/lib/date-grid.ts
1174
+ function addDays(d, n) {
1175
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
1176
+ }
1177
+ function addMonthsClamped(d, n) {
1178
+ const first = new Date(d.getFullYear(), d.getMonth() + n, 1);
1179
+ const last = new Date(first.getFullYear(), first.getMonth() + 1, 0).getDate();
1180
+ return new Date(first.getFullYear(), first.getMonth(), Math.min(d.getDate(), last));
1181
+ }
1182
+ function dayKey(d) {
1183
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1184
+ }
1185
+ function moveDay(from, key, shift, weekStartsOn = 0) {
1186
+ const intoWeek = (from.getDay() - weekStartsOn + 7) % 7;
1187
+ switch (key) {
1188
+ case "ArrowLeft":
1189
+ return addDays(from, -1);
1190
+ case "ArrowRight":
1191
+ return addDays(from, 1);
1192
+ case "ArrowUp":
1193
+ return addDays(from, -7);
1194
+ case "ArrowDown":
1195
+ return addDays(from, 7);
1196
+ case "Home":
1197
+ return addDays(from, -intoWeek);
1198
+ case "End":
1199
+ return addDays(from, 6 - intoWeek);
1200
+ case "PageUp":
1201
+ return addMonthsClamped(from, shift ? -12 : -1);
1202
+ case "PageDown":
1203
+ return addMonthsClamped(from, shift ? 12 : 1);
1204
+ default:
1205
+ return null;
1206
+ }
1207
+ }
1208
+
1128
1209
  // ../../showcase/src/lib/segments.ts
1129
1210
  function stepValue(spec, current, delta) {
1130
1211
  if (current === null) return delta > 0 ? spec.min : spec.max;
@@ -1165,6 +1246,7 @@ var TEXT_SIZE2 = {
1165
1246
  lg: "text-base"
1166
1247
  };
1167
1248
  var DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
1249
+ var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
1168
1250
  var MONTHS = [
1169
1251
  "January",
1170
1252
  "February",
@@ -1236,7 +1318,9 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1236
1318
  const [hover, setHover] = useState(null);
1237
1319
  const [viewDate, setViewDate] = useState(() => value ?? range?.start ?? range?.end ?? /* @__PURE__ */ new Date());
1238
1320
  const triggerRef = useRef(null);
1321
+ const buttonRef = useRef(null);
1239
1322
  const panelRef = useRef(null);
1323
+ const gridRef = useRef(null);
1240
1324
  const generatedId = useId();
1241
1325
  const inputId = id ?? generatedId;
1242
1326
  const pos = usePosition(triggerRef, panelRef, open, { side: "bottom", align: "start", offset: 4, flip: true });
@@ -1245,18 +1329,48 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1245
1329
  const daysInMonth = getDaysInMonth(year, month);
1246
1330
  const firstDay = getFirstDayOfWeek(year, month);
1247
1331
  const today = useMemo(() => /* @__PURE__ */ new Date(), []);
1248
- const isTopLayer = useOverlayLayer(open);
1249
- const prevMonth = () => setViewDate(new Date(year, month - 1, 1));
1250
- const nextMonth = () => setViewDate(new Date(year, month + 1, 1));
1251
1332
  const startOf = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
1252
1333
  const isDisabledDay = (d) => !!min && d < startOf(min) || !!max && d > startOf(max);
1253
- useCloseOnFocusOut(open, triggerRef, panelRef, () => setOpen(false), isTopLayer);
1334
+ const [focusDate, setFocusDate] = useState(() => startOf(value ?? range?.start ?? range?.end ?? /* @__PURE__ */ new Date()));
1335
+ const [prevOpen, setPrevOpen] = useState(open);
1336
+ if (open !== prevOpen) {
1337
+ setPrevOpen(open);
1338
+ if (open) {
1339
+ const start = startOf(value ?? range?.start ?? range?.end ?? today);
1340
+ setFocusDate(start);
1341
+ setViewDate(new Date(start.getFullYear(), start.getMonth(), 1));
1342
+ }
1343
+ }
1344
+ const showDay = (d) => {
1345
+ setFocusDate(d);
1346
+ if (d.getFullYear() !== year || d.getMonth() !== month) setViewDate(new Date(d.getFullYear(), d.getMonth(), 1));
1347
+ };
1348
+ const grid = useGridKeyboard({
1349
+ container: gridRef,
1350
+ active: focusDate,
1351
+ setActive: showDay,
1352
+ keyOf: dayKey,
1353
+ move: (from, e) => moveDay(from, e.key, e.shiftKey, 0)
1354
+ });
1355
+ const prevMonth = () => {
1356
+ setViewDate(new Date(year, month - 1, 1));
1357
+ setFocusDate(addMonthsClamped(focusDate, -1));
1358
+ };
1359
+ const nextMonth = () => {
1360
+ setViewDate(new Date(year, month + 1, 1));
1361
+ setFocusDate(addMonthsClamped(focusDate, 1));
1362
+ };
1363
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: buttonRef, panel: panelRef, close: () => setOpen(false), tabExits: "edges" });
1364
+ const focusActiveDay = useLatest(grid.focusActive);
1254
1365
  useEffect(() => {
1255
- if (open) requestAnimationFrame(() => focusInitial(panelRef.current));
1256
- }, [open]);
1257
- const closeAndRefocus = () => {
1366
+ if (!open) return;
1367
+ const raf = requestAnimationFrame(() => focusActiveDay.current());
1368
+ return () => cancelAnimationFrame(raf);
1369
+ }, [open, focusActiveDay]);
1370
+ const choose = (d) => {
1371
+ returnFocus();
1372
+ onChange(d);
1258
1373
  setOpen(false);
1259
- focusSegment(0);
1260
1374
  };
1261
1375
  const dayFirst = useMemo(() => localeDayFirst(), []);
1262
1376
  const [segs, setSegs] = useState(() => value ? { day: value.getDate(), month: value.getMonth() + 1, year: value.getFullYear() } : { day: null, month: null, year: null });
@@ -1272,7 +1386,6 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1272
1386
  const focusSegment = (i) => {
1273
1387
  segmentRefs.current[Math.max(0, Math.min(specs.length - 1, i))]?.focus();
1274
1388
  };
1275
- const focusSegmentRef = useLatest(focusSegment);
1276
1389
  useEffect(() => {
1277
1390
  if (!open) return;
1278
1391
  const handler = (e) => {
@@ -1281,10 +1394,10 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1281
1394
  setOpen(false);
1282
1395
  };
1283
1396
  const keyHandler = (e) => {
1284
- if (e.key === "Escape" && isTopLayer()) {
1285
- setOpen(false);
1286
- focusSegmentRef.current(0);
1287
- }
1397
+ if (e.key !== "Escape" || e.defaultPrevented || !isTopLayer()) return;
1398
+ e.preventDefault();
1399
+ returnFocus();
1400
+ setOpen(false);
1288
1401
  };
1289
1402
  document.addEventListener("mousedown", handler);
1290
1403
  document.addEventListener("keydown", keyHandler);
@@ -1292,7 +1405,7 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1292
1405
  document.removeEventListener("mousedown", handler);
1293
1406
  document.removeEventListener("keydown", keyHandler);
1294
1407
  };
1295
- }, [open, isTopLayer, focusSegmentRef]);
1408
+ }, [open, isTopLayer, returnFocus]);
1296
1409
  const [typedInvalid, setTypedInvalid] = useState(false);
1297
1410
  const commitIfComplete = (next) => {
1298
1411
  if (next.day === null || next.month === null || next.year === null) {
@@ -1378,8 +1491,7 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1378
1491
  const selectDay = (day) => {
1379
1492
  const d = new Date(year, month, day);
1380
1493
  if (isDisabledDay(d)) return;
1381
- onChange(d);
1382
- closeAndRefocus();
1494
+ choose(d);
1383
1495
  };
1384
1496
  const rangeA = range?.start ?? null;
1385
1497
  const rangeB = range?.end ?? null;
@@ -1390,6 +1502,9 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1390
1502
  const cells = [];
1391
1503
  for (let i = 0; i < firstDay; i++) cells.push(null);
1392
1504
  for (let d = 1; d <= daysInMonth; d++) cells.push(d);
1505
+ while (cells.length % 7) cells.push(null);
1506
+ const weeks = [];
1507
+ for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
1393
1508
  return /* @__PURE__ */ jsxs("div", { ref, className: cn(className), children: [
1394
1509
  label && /* @__PURE__ */ jsx("label", { htmlFor: inputId, className: "block text-sm font-medium text-[var(--ds-text-primary)] mb-ds-1", children: label }),
1395
1510
  /* @__PURE__ */ jsxs(
@@ -1455,6 +1570,7 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1455
1570
  /* @__PURE__ */ jsx(
1456
1571
  "button",
1457
1572
  {
1573
+ ref: buttonRef,
1458
1574
  type: "button",
1459
1575
  "aria-label": "Choose a date",
1460
1576
  "aria-haspopup": "dialog",
@@ -1501,38 +1617,56 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1501
1617
  ]
1502
1618
  }
1503
1619
  ),
1504
- /* @__PURE__ */ jsxs("div", { style: { padding: "var(--ds-space-2) var(--ds-space-3)" }, children: [
1505
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 gap-ds-0.5 mb-ds-1 tabular-nums", children: DAYS.map((d) => /* @__PURE__ */ jsx("div", { className: "text-center text-2xs font-medium text-[var(--ds-text-muted)] py-ds-1", children: d }, d)) }),
1506
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 gap-ds-0.5 tabular-nums", onMouseLeave: () => setHover(null), children: cells.map((day, i) => {
1507
- if (day === null) return /* @__PURE__ */ jsx("div", {}, `empty-${i}`);
1508
- const dateObj = new Date(year, month, day);
1509
- const isSelected = !!value && isSameDay(dateObj, value);
1510
- const isOtherEnd = !!otherEnd && isSameDay(dateObj, otherEnd);
1511
- const isToday2 = isSameDay(dateObj, today);
1512
- const between = inBand(dateObj);
1513
- const dis = isDisabledDay(dateObj);
1514
- return /* @__PURE__ */ jsx(
1515
- "button",
1516
- {
1517
- type: "button",
1518
- disabled: dis,
1519
- onClick: () => selectDay(day),
1520
- onMouseEnter: () => setHover(dateObj),
1521
- "aria-pressed": isSelected,
1522
- "aria-label": formatDate(dateObj),
1523
- className: cn(
1524
- "w-full aspect-square flex items-center justify-center text-xs",
1525
- "transition-colors cursor-pointer",
1526
- "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
1527
- isSelected ? "bg-[var(--ds-accent-primary)] text-[var(--ds-text-on-primary)] font-bold rounded-[var(--ds-radius-sm)]" : isOtherEnd ? "bg-[var(--ds-accent-muted)] text-[var(--ds-accent-subtle-text)] font-semibold rounded-[var(--ds-radius-sm)]" : between ? "bg-[var(--ds-accent-subtle)] text-[var(--ds-accent-subtle-text)] rounded-none" : isToday2 ? "border-[length:var(--ds-border-width)] border-[var(--ds-accent-primary)] text-[var(--ds-accent-primary)] font-medium rounded-[var(--ds-radius-sm)]" : "text-[var(--ds-text-secondary)] hover:bg-[var(--ds-bg-emphasis)] rounded-[var(--ds-radius-sm)]",
1528
- dis && "opacity-disabled cursor-not-allowed hover:bg-transparent"
1529
- ),
1530
- children: day
1531
- },
1532
- day
1533
- );
1534
- }) })
1535
- ] }),
1620
+ /* @__PURE__ */ jsxs(
1621
+ "div",
1622
+ {
1623
+ ref: gridRef,
1624
+ role: "grid",
1625
+ "aria-label": `${MONTHS[month]} ${year}`,
1626
+ onKeyDown: grid.onKeyDown,
1627
+ className: "flex flex-col gap-ds-0.5 tabular-nums",
1628
+ style: { padding: "var(--ds-space-2) var(--ds-space-3)" },
1629
+ children: [
1630
+ /* @__PURE__ */ jsx("div", { role: "row", className: "grid grid-cols-7 gap-ds-0.5 mb-ds-0.5", children: DAYS.map((d, i) => /* @__PURE__ */ jsx("div", { role: "columnheader", "aria-label": DAY_NAMES[i], className: "text-center text-2xs font-medium text-[var(--ds-text-muted)] py-ds-1", children: d }, d)) }),
1631
+ weeks.map((week, w) => /* @__PURE__ */ jsx("div", { role: "row", className: "grid grid-cols-7 gap-ds-0.5", onMouseLeave: () => setHover(null), children: week.map((day, i) => {
1632
+ if (day === null) return /* @__PURE__ */ jsx("div", { role: "gridcell" }, `empty-${i}`);
1633
+ const dateObj = new Date(year, month, day);
1634
+ const isSelected = !!value && isSameDay(dateObj, value);
1635
+ const isOtherEnd = !!otherEnd && isSameDay(dateObj, otherEnd);
1636
+ const isToday2 = isSameDay(dateObj, today);
1637
+ const between = inBand(dateObj);
1638
+ const dis = isDisabledDay(dateObj);
1639
+ return /* @__PURE__ */ jsx(
1640
+ "button",
1641
+ {
1642
+ type: "button",
1643
+ role: "gridcell",
1644
+ "data-grid-key": dayKey(dateObj),
1645
+ tabIndex: grid.tabIndexOf(dateObj),
1646
+ "aria-disabled": dis || void 0,
1647
+ onClick: () => selectDay(day),
1648
+ onFocus: () => {
1649
+ if (!isSameDay(dateObj, focusDate)) setFocusDate(dateObj);
1650
+ },
1651
+ onMouseEnter: () => setHover(dateObj),
1652
+ "aria-selected": isSelected,
1653
+ "aria-current": isToday2 ? "date" : void 0,
1654
+ "aria-label": formatDate(dateObj),
1655
+ className: cn(
1656
+ "w-full aspect-square flex items-center justify-center text-xs",
1657
+ "transition-colors cursor-pointer",
1658
+ "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
1659
+ isSelected ? "bg-[var(--ds-accent-primary)] text-[var(--ds-text-on-primary)] font-bold rounded-[var(--ds-radius-sm)]" : isOtherEnd ? "bg-[var(--ds-accent-muted)] text-[var(--ds-accent-subtle-text)] font-semibold rounded-[var(--ds-radius-sm)]" : between ? "bg-[var(--ds-accent-subtle)] text-[var(--ds-accent-subtle-text)] rounded-none" : isToday2 ? "border-[length:var(--ds-border-width)] border-[var(--ds-accent-primary)] text-[var(--ds-accent-primary)] font-medium rounded-[var(--ds-radius-sm)]" : "text-[var(--ds-text-secondary)] hover:bg-[var(--ds-bg-emphasis)] rounded-[var(--ds-radius-sm)]",
1660
+ dis && "opacity-disabled cursor-not-allowed hover:bg-transparent"
1661
+ ),
1662
+ children: day
1663
+ },
1664
+ day
1665
+ );
1666
+ }) }, w))
1667
+ ]
1668
+ }
1669
+ ),
1536
1670
  /* @__PURE__ */ jsxs(
1537
1671
  "div",
1538
1672
  {
@@ -1547,8 +1681,7 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1547
1681
  onClick: () => {
1548
1682
  const now = /* @__PURE__ */ new Date();
1549
1683
  setViewDate(now);
1550
- onChange(now);
1551
- closeAndRefocus();
1684
+ choose(now);
1552
1685
  },
1553
1686
  children: "Today"
1554
1687
  }
@@ -1558,10 +1691,7 @@ function DatePicker({ value, onChange, placeholder = "Pick a date", disabled, cl
1558
1691
  {
1559
1692
  variant: "ghost",
1560
1693
  size: "sm",
1561
- onClick: () => {
1562
- onChange(null);
1563
- closeAndRefocus();
1564
- },
1694
+ onClick: () => choose(null),
1565
1695
  children: "Clear"
1566
1696
  }
1567
1697
  )
@@ -1645,7 +1775,7 @@ function endOfWeek(d, weekStartsOn = 0) {
1645
1775
  end.setHours(23, 59, 59, 999);
1646
1776
  return end;
1647
1777
  }
1648
- function addDays(d, n) {
1778
+ function addDays2(d, n) {
1649
1779
  const result = new Date(d);
1650
1780
  result.setDate(result.getDate() + n);
1651
1781
  return result;
@@ -1674,7 +1804,7 @@ function getWeekDays(date, weekStartsOn = 0) {
1674
1804
  const start = startOfWeek(date, weekStartsOn);
1675
1805
  const days = [];
1676
1806
  for (let i = 0; i < 7; i++) {
1677
- days.push(addDays(start, i));
1807
+ days.push(addDays2(start, i));
1678
1808
  }
1679
1809
  return days;
1680
1810
  }
@@ -1764,11 +1894,11 @@ var DEFAULT_DATE_RANGE_PRESETS = [
1764
1894
  } },
1765
1895
  { label: "Last 7 days", range: () => {
1766
1896
  const t = startOfDay(/* @__PURE__ */ new Date());
1767
- return { start: addDays(t, -6), end: t };
1897
+ return { start: addDays2(t, -6), end: t };
1768
1898
  } },
1769
1899
  { label: "Last 30 days", range: () => {
1770
1900
  const t = startOfDay(/* @__PURE__ */ new Date());
1771
- return { start: addDays(t, -29), end: t };
1901
+ return { start: addDays2(t, -29), end: t };
1772
1902
  } },
1773
1903
  { label: "This month", range: () => {
1774
1904
  const t = /* @__PURE__ */ new Date();
@@ -1808,11 +1938,42 @@ function DateRangePicker({
1808
1938
  const pos = usePosition(triggerRef, panelRef, open, { side: "bottom", align: "start", offset: 4, flip: true });
1809
1939
  const today = useMemo(() => startOfDay(/* @__PURE__ */ new Date()), []);
1810
1940
  const dayNames = getOrderedDayNamesShort(weekStartsOn);
1941
+ const gridRef = useRef(null);
1942
+ const [focusDate, setFocusDate] = useState(() => startOfDay(value.start ?? /* @__PURE__ */ new Date()));
1943
+ const [prevOpen, setPrevOpen] = useState(open);
1944
+ if (open !== prevOpen) {
1945
+ setPrevOpen(open);
1946
+ if (open) {
1947
+ const start = startOfDay(value.start ?? today);
1948
+ setFocusDate(start);
1949
+ setViewDate(new Date(start.getFullYear(), start.getMonth(), 1));
1950
+ }
1951
+ }
1952
+ const showDay = (d) => {
1953
+ setFocusDate(d);
1954
+ const monthIndex = (x) => x.getFullYear() * 12 + x.getMonth();
1955
+ const offset = monthIndex(d) - monthIndex(viewDate);
1956
+ if (offset < 0) setViewDate(new Date(d.getFullYear(), d.getMonth(), 1));
1957
+ else if (offset > 1) setViewDate(new Date(d.getFullYear(), d.getMonth() - 1, 1));
1958
+ };
1959
+ const grid = useGridKeyboard({
1960
+ container: gridRef,
1961
+ active: focusDate,
1962
+ setActive: showDay,
1963
+ keyOf: dayKey,
1964
+ move: (from, e) => moveDay(from, e.key, e.shiftKey, weekStartsOn)
1965
+ });
1966
+ const pageMonths = (n) => {
1967
+ setViewDate(addMonths(viewDate, n));
1968
+ setFocusDate(addMonthsClamped(focusDate, n));
1969
+ };
1970
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: panelRef, close: () => setOpen(false), tabExits: "edges" });
1971
+ const focusActiveDay = useLatest(grid.focusActive);
1811
1972
  useEffect(() => {
1812
- if (open) requestAnimationFrame(() => focusInitial(panelRef.current));
1813
- }, [open]);
1814
- const isTopLayer = useOverlayLayer(open);
1815
- useCloseOnFocusOut(open, triggerRef, panelRef, () => setOpen(false), isTopLayer);
1973
+ if (!open) return;
1974
+ const raf = requestAnimationFrame(() => focusActiveDay.current());
1975
+ return () => cancelAnimationFrame(raf);
1976
+ }, [open, focusActiveDay]);
1816
1977
  useEffect(() => {
1817
1978
  if (!open) return;
1818
1979
  const onMouse = (e) => {
@@ -1821,10 +1982,10 @@ function DateRangePicker({
1821
1982
  setOpen(false);
1822
1983
  };
1823
1984
  const onKey = (e) => {
1824
- if (e.key === "Escape" && isTopLayer()) {
1825
- setOpen(false);
1826
- triggerRef.current?.focus();
1827
- }
1985
+ if (e.key !== "Escape" || e.defaultPrevented || !isTopLayer()) return;
1986
+ e.preventDefault();
1987
+ returnFocus();
1988
+ setOpen(false);
1828
1989
  };
1829
1990
  document.addEventListener("mousedown", onMouse);
1830
1991
  document.addEventListener("keydown", onKey);
@@ -1832,7 +1993,7 @@ function DateRangePicker({
1832
1993
  document.removeEventListener("mousedown", onMouse);
1833
1994
  document.removeEventListener("keydown", onKey);
1834
1995
  };
1835
- }, [open, isTopLayer]);
1996
+ }, [open, isTopLayer, returnFocus]);
1836
1997
  const isDisabledDay = (d) => min && d < startOfDay(min) || max && d > startOfDay(max) || false;
1837
1998
  const selectDay = (d) => {
1838
1999
  if (isDisabledDay(d)) return;
@@ -1844,9 +2005,9 @@ function DateRangePicker({
1844
2005
  onChange({ start: d, end: null });
1845
2006
  return;
1846
2007
  }
2008
+ returnFocus();
1847
2009
  onChange({ start: value.start, end: d });
1848
2010
  setOpen(false);
1849
- triggerRef.current?.focus();
1850
2011
  };
1851
2012
  const previewEnd = value.start && !value.end && hover && hover >= value.start ? hover : null;
1852
2013
  const rangeStart = value.start;
@@ -1908,14 +2069,14 @@ function DateRangePicker({
1908
2069
  children: [
1909
2070
  presets.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-ds-0.5 p-ds-2 border-r-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)] min-w-[140px]", children: presets.map((p) => /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", className: "justify-start", onClick: () => {
1910
2071
  const r = p.range();
2072
+ returnFocus();
1911
2073
  onChange(r);
1912
2074
  setViewDate(r.start ?? /* @__PURE__ */ new Date());
1913
2075
  setOpen(false);
1914
- triggerRef.current?.focus();
1915
2076
  }, children: p.label }, p.label)) }),
1916
2077
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
1917
2078
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between bg-[var(--ds-bg-muted)] rounded-tr-[var(--ds-radius-md)] px-ds-3 py-ds-2", children: [
1918
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => setViewDate(addMonths(viewDate, -1)), "aria-label": "Previous month", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14, className: "rotate-180" }) }),
2079
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => pageMonths(-1), "aria-label": "Previous month", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14, className: "rotate-180" }) }),
1919
2080
  /* @__PURE__ */ jsxs("span", { className: "text-sm font-medium text-[var(--ds-text-primary)]", children: [
1920
2081
  MONTHS2[months[0].getMonth()],
1921
2082
  " ",
@@ -1925,12 +2086,12 @@ function DateRangePicker({
1925
2086
  " ",
1926
2087
  months[1].getFullYear()
1927
2088
  ] }),
1928
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => setViewDate(addMonths(viewDate, 1)), "aria-label": "Next month", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14 }) })
2089
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => pageMonths(1), "aria-label": "Next month", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14 }) })
1929
2090
  ] }),
1930
- /* @__PURE__ */ jsx("div", { className: "flex gap-ds-4 px-ds-3 py-ds-2", onMouseLeave: () => setHover(null), children: months.map((m) => /* @__PURE__ */ jsxs("div", { className: "w-[224px]", children: [
1931
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 gap-ds-0.5 mb-ds-1", children: dayNames.map((d) => /* @__PURE__ */ jsx("div", { className: "text-center text-2xs font-medium text-[var(--ds-text-muted)] py-ds-1", children: d }, d)) }),
1932
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 gap-ds-0.5", children: getMonthGrid(m.getFullYear(), m.getMonth(), weekStartsOn).flat().map((d) => {
1933
- if (!isSameMonth(d, m)) return /* @__PURE__ */ jsx("div", {}, d.toISOString());
2091
+ /* @__PURE__ */ jsx("div", { ref: gridRef, onKeyDown: grid.onKeyDown, className: "flex gap-ds-4 px-ds-3 py-ds-2", onMouseLeave: () => setHover(null), children: months.map((m) => /* @__PURE__ */ jsxs("div", { role: "grid", "aria-label": `${MONTHS2[m.getMonth()]} ${m.getFullYear()}`, className: "w-[224px] flex flex-col gap-ds-0.5", children: [
2092
+ /* @__PURE__ */ jsx("div", { role: "row", className: "grid grid-cols-7 gap-ds-0.5 mb-ds-0.5", children: dayNames.map((d) => /* @__PURE__ */ jsx("div", { role: "columnheader", className: "text-center text-2xs font-medium text-[var(--ds-text-muted)] py-ds-1", children: d }, d)) }),
2093
+ getMonthGrid(m.getFullYear(), m.getMonth(), weekStartsOn).map((week, w) => /* @__PURE__ */ jsx("div", { role: "row", className: "grid grid-cols-7 gap-ds-0.5", children: week.map((d) => {
2094
+ if (!isSameMonth(d, m)) return /* @__PURE__ */ jsx("div", { role: "gridcell" }, d.toISOString());
1934
2095
  const isStart = !!rangeStart && isSameDay2(d, rangeStart);
1935
2096
  const isEnd = !!rangeEnd && isSameDay2(d, rangeEnd);
1936
2097
  const between = inRange(d);
@@ -1939,10 +2100,17 @@ function DateRangePicker({
1939
2100
  "button",
1940
2101
  {
1941
2102
  type: "button",
1942
- disabled: dis,
2103
+ role: "gridcell",
2104
+ "data-grid-key": dayKey(d),
2105
+ tabIndex: grid.tabIndexOf(d),
2106
+ "aria-disabled": dis || void 0,
1943
2107
  onClick: () => selectDay(d),
2108
+ onFocus: () => {
2109
+ if (!isSameDay2(d, focusDate)) setFocusDate(d);
2110
+ },
1944
2111
  onMouseEnter: () => setHover(d),
1945
- "aria-pressed": isStart || isEnd,
2112
+ "aria-selected": isStart || isEnd || between,
2113
+ "aria-current": isSameDay2(d, today) ? "date" : void 0,
1946
2114
  "aria-label": fmt(d),
1947
2115
  className: cn(
1948
2116
  "w-full aspect-square flex items-center justify-center text-xs",
@@ -1955,7 +2123,7 @@ function DateRangePicker({
1955
2123
  },
1956
2124
  d.toISOString()
1957
2125
  );
1958
- }) })
2126
+ }) }, w))
1959
2127
  ] }, `${m.getFullYear()}-${m.getMonth()}`)) }),
1960
2128
  /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center bg-[var(--ds-bg-muted)] border-t-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)] rounded-br-[var(--ds-radius-md)] px-ds-3 py-ds-2", children: [
1961
2129
  /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--ds-text-muted)]", children: value.start && !value.end ? "Now pick an end date" : summary ?? "Pick a start date" }),
@@ -1972,6 +2140,96 @@ function DateRangePicker({
1972
2140
  )
1973
2141
  ] });
1974
2142
  }
2143
+
2144
+ // ../../showcase/src/lib/file-accept.ts
2145
+ var MIME_NAMES = {
2146
+ "text/plain": "TXT",
2147
+ "audio/mpeg": "MP3",
2148
+ "video/quicktime": "MOV",
2149
+ "application/msword": "DOC",
2150
+ "application/vnd.ms-excel": "XLS",
2151
+ "application/vnd.ms-powerpoint": "PPT",
2152
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX",
2153
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX",
2154
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "PPTX"
2155
+ };
2156
+ var EXT_NAMES = { jpeg: "JPG", tif: "TIFF", htm: "HTML", yml: "YAML" };
2157
+ var FAMILIES = {
2158
+ image: { one: "an image", many: "images" },
2159
+ video: { one: "a video", many: "videos" },
2160
+ audio: { one: "an audio file", many: "audio files" },
2161
+ text: { one: "a text file", many: "text files" }
2162
+ };
2163
+ var READ_AS_WORD = /* @__PURE__ */ new Set(["FLAC", "HEIC", "HEIF", "LOG", "MIDI", "MOV", "RAR", "RAW"]);
2164
+ var extName = (ext) => EXT_NAMES[ext] ?? ext.toUpperCase();
2165
+ function mimeName(mime) {
2166
+ if (MIME_NAMES[mime]) return MIME_NAMES[mime];
2167
+ const subtype = mime.slice(mime.indexOf("/") + 1).replace(/\+.*$/, "").replace(/^(?:x-|vnd\.)/, "");
2168
+ return extName(subtype.slice(subtype.lastIndexOf(".") + 1));
2169
+ }
2170
+ function parseAccept(accept) {
2171
+ return (accept ?? "").split(",").map((t) => t.trim().toLowerCase()).filter(Boolean).map((t) => {
2172
+ if (t === "*" || t === "*/*") return { kind: "any" };
2173
+ if (t.startsWith(".")) return { kind: "format", name: extName(t.slice(1)), suffix: t };
2174
+ if (t.endsWith("/*")) return { kind: "family", family: t.slice(0, -2) };
2175
+ return { kind: "format", name: mimeName(t), mime: t };
2176
+ });
2177
+ }
2178
+ function fileFormat(file) {
2179
+ const dot = file.name.lastIndexOf(".");
2180
+ return dot > 0 ? extName(file.name.slice(dot + 1).toLowerCase()) : null;
2181
+ }
2182
+ function matches(file, tokens) {
2183
+ if (tokens.length === 0 || tokens.some((t) => t.kind === "any")) return true;
2184
+ const name = file.name.toLowerCase();
2185
+ const format = fileFormat(file);
2186
+ const type = file.type.toLowerCase();
2187
+ return tokens.some((t) => {
2188
+ if (t.kind === "family") return type === "" || type.startsWith(`${t.family}/`);
2189
+ if (t.kind !== "format") return false;
2190
+ return t.name === format || t.suffix != null && name.endsWith(t.suffix) || t.mime != null && t.mime === type;
2191
+ });
2192
+ }
2193
+ var joinOr = (items) => items.length < 2 ? items.join("") : `${items.slice(0, -1).join(", ")} or ${items[items.length - 1]}`;
2194
+ var article = (name) => {
2195
+ const asWord = name.replace(/[^A-Z]/g, "").length >= 5 || READ_AS_WORD.has(name);
2196
+ return (asWord ? /^[AEIOU]/ : /^[AEFHILMNORSX]/).test(name) ? "an" : "a";
2197
+ };
2198
+ function typeWords(accept) {
2199
+ const tokens = parseAccept(accept);
2200
+ if (tokens.length === 0 || tokens.some((t) => t.kind === "any" || t.kind === "family" && !FAMILIES[t.family])) return null;
2201
+ const families = [...new Set(tokens.flatMap((t) => t.kind === "family" ? [t.family] : []))].map((f) => FAMILIES[f]);
2202
+ const formats = [...new Set(tokens.flatMap((t) => t.kind === "format" ? [t.name] : []))];
2203
+ const many = `${joinOr([...families.map((f) => f.many), ...formats])}${formats.length ? " files" : ""}`;
2204
+ const one = families.length === 0 ? `${article(formats[0])} ${joinOr(formats)}` : joinOr([...families.map((f) => f.one), ...formats.map((f) => `${article(f)} ${f}`)]);
2205
+ return { one, many };
2206
+ }
2207
+ function sizeWords(bytes) {
2208
+ const units = [["GB", 1024 ** 3], ["MB", 1024 ** 2], ["KB", 1024]];
2209
+ for (const [unit, size] of units) if (bytes >= size) return `${Math.round(bytes / size * 10) / 10} ${unit}`;
2210
+ return `${bytes} bytes`;
2211
+ }
2212
+ var capitalise = (s) => s.charAt(0).toUpperCase() + s.slice(1);
2213
+ function fileConstraints(accept, maxSize, multiple) {
2214
+ const types = typeWords(accept)?.many;
2215
+ const size = maxSize != null ? `up to ${sizeWords(maxSize)}${multiple ? " each" : ""}` : null;
2216
+ const line = [types, size].filter(Boolean).join(" ");
2217
+ return line ? capitalise(line) : null;
2218
+ }
2219
+ function checkFiles(files, { accept, maxSize, multiple }) {
2220
+ if (!multiple && files.length > 1) return { accepted: [], message: "You can only select one file" };
2221
+ const tokens = parseAccept(accept);
2222
+ const types = typeWords(accept);
2223
+ const accepted = [];
2224
+ const reasons = [];
2225
+ for (const file of files) {
2226
+ const subject = multiple ? file.name : "The selected file";
2227
+ if (!matches(file, tokens)) reasons.push(types ? `${subject} must be ${types.one}` : `${subject} is not a file type that can be added here`);
2228
+ else if (maxSize != null && file.size > maxSize) reasons.push(`${subject} must be smaller than ${sizeWords(maxSize)}`);
2229
+ else accepted.push(file);
2230
+ }
2231
+ return { accepted, message: reasons.length ? reasons.join("; ") : null };
2232
+ }
1975
2233
  var variantFills = {
1976
2234
  default: "bg-[var(--ds-accent-primary)] [background-image:var(--ds-gradient)]",
1977
2235
  success: "bg-[var(--ds-status-success)]",
@@ -2019,6 +2277,66 @@ function Progress({ value, max = 100, variant = "default", size = "md", indeterm
2019
2277
  ] })
2020
2278
  ] });
2021
2279
  }
2280
+ function InlineFieldError({ message, id, className }) {
2281
+ return /* @__PURE__ */ jsxs(
2282
+ "p",
2283
+ {
2284
+ id,
2285
+ role: "alert",
2286
+ className: cn(
2287
+ "flex items-center gap-ds-1.5 text-xs text-[var(--ds-status-error)]",
2288
+ "mt-ds-1.5",
2289
+ className
2290
+ ),
2291
+ children: [
2292
+ /* @__PURE__ */ jsx(Icon, { name: "error", size: 14, className: "shrink-0" }),
2293
+ /* @__PURE__ */ jsx("span", { children: message })
2294
+ ]
2295
+ }
2296
+ );
2297
+ }
2298
+ var FieldContext = createContext(null);
2299
+ function useField() {
2300
+ return useContext(FieldContext);
2301
+ }
2302
+ function Field({ label, children, hint, error, required, labelAction, id: idProp, className }) {
2303
+ const generated = useId();
2304
+ const id = idProp ?? `field-${generated}`;
2305
+ const hintId = `${id}-hint`;
2306
+ const errorId = `${id}-error`;
2307
+ const hasError = typeof error === "string" && error.length > 0;
2308
+ const describedBy = [hint ? hintId : null, hasError ? errorId : null].filter(Boolean).join(" ") || void 0;
2309
+ const labelId = `${id}-label`;
2310
+ const fieldContext = useMemo(() => ({ controlId: id, labelId, describedBy }), [id, labelId, describedBy]);
2311
+ const controlProps = {
2312
+ id,
2313
+ "aria-describedby": describedBy,
2314
+ ...hasError ? { "aria-invalid": true, error: true } : {},
2315
+ ...required ? { "aria-required": true, required: true } : {}
2316
+ };
2317
+ let control;
2318
+ if (typeof children === "function") {
2319
+ control = children(controlProps);
2320
+ } else {
2321
+ const child = Children.only(children);
2322
+ if (!isValidElement(child)) throw new Error("Field expects a single form control as its child");
2323
+ const isNative = typeof child.type === "string";
2324
+ const { error: errorFlag, ...ariaOnly } = controlProps;
2325
+ control = cloneElement(child, isNative ? ariaOnly : { ...ariaOnly, ...errorFlag ? { error: true } : {} });
2326
+ }
2327
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-ds-1.5", className), children: [
2328
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-ds-2", children: [
2329
+ /* @__PURE__ */ jsxs(Label, { id: labelId, htmlFor: id, children: [
2330
+ label,
2331
+ required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "ml-ds-0.5 text-[var(--ds-status-error)]", children: "*" })
2332
+ ] }),
2333
+ labelAction
2334
+ ] }),
2335
+ /* @__PURE__ */ jsx(FieldContext.Provider, { value: fieldContext, children: control }),
2336
+ hint && /* @__PURE__ */ jsx("p", { id: hintId, className: "text-xs text-[var(--ds-text-muted)]", children: hint }),
2337
+ hasError && /* @__PURE__ */ jsx(InlineFieldError, { id: errorId, message: error, className: "mt-0" })
2338
+ ] });
2339
+ }
2022
2340
  function TruncFileName({ children, className }) {
2023
2341
  const { ref, isTruncated } = useTextOverflow({ lines: 1 });
2024
2342
  const el = /* @__PURE__ */ jsx("p", { ref, className: cn("truncate", className), children });
@@ -2030,37 +2348,57 @@ function formatSize(bytes) {
2030
2348
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2031
2349
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2032
2350
  }
2033
- function FileUpload({ onFiles, accept, multiple = true, maxSize, className, error = false, label }) {
2351
+ function FileUpload({ onFiles, accept, multiple = true, maxSize, className, error = false, label, id }) {
2034
2352
  const [files, setFiles] = useState([]);
2035
2353
  const [dragOver, setDragOver] = useState(false);
2036
- const inputId = useId();
2354
+ const [rejected, setRejected] = useState(null);
2037
2355
  const inputRef = useRef(null);
2356
+ const nextEntryId = useRef(0);
2357
+ const timers = useRef(/* @__PURE__ */ new Map());
2358
+ useEffect(() => {
2359
+ const running = timers.current;
2360
+ return () => running.forEach(clearInterval);
2361
+ }, []);
2362
+ const field = useField();
2363
+ const ownId = useId();
2364
+ const inputId = id ?? `${ownId}-input`;
2365
+ const constraintsId = `${ownId}-constraints`;
2366
+ const rejectedId = `${ownId}-rejected`;
2367
+ const showLabel = label != null && !field;
2368
+ const constraints = fileConstraints(accept, maxSize, multiple);
2369
+ const invalid = error || rejected != null;
2370
+ const describedBy = [constraints ? constraintsId : null, field?.describedBy, rejected ? rejectedId : null].filter(Boolean).join(" ") || void 0;
2371
+ const stopTimer = (id2) => {
2372
+ clearInterval(timers.current.get(id2));
2373
+ timers.current.delete(id2);
2374
+ };
2038
2375
  const addFiles = useCallback(
2039
2376
  (fileList) => {
2040
- const arr = Array.from(fileList);
2041
- const valid = maxSize ? arr.filter((f) => f.size <= maxSize) : arr;
2042
- const entries = valid.map((f) => ({ file: f, progress: 0, status: "uploading" }));
2043
- setFiles((prev) => [...prev, ...entries]);
2044
- onFiles(valid);
2045
- entries.forEach((_, i) => {
2377
+ const { accepted, message } = checkFiles(Array.from(fileList), { accept, maxSize, multiple });
2378
+ setRejected(message);
2379
+ if (accepted.length === 0) return;
2380
+ const entries = accepted.map((file) => ({ id: nextEntryId.current++, file, progress: 0, status: "uploading" }));
2381
+ if (!multiple) {
2382
+ timers.current.forEach(clearInterval);
2383
+ timers.current.clear();
2384
+ }
2385
+ setFiles((prev) => multiple ? [...prev, ...entries] : entries);
2386
+ onFiles(accepted);
2387
+ for (const { id: id2 } of entries) {
2046
2388
  let progress = 0;
2047
- const interval = setInterval(() => {
2048
- progress += Math.random() * 30 + 10;
2049
- if (progress >= 100) {
2050
- progress = 100;
2051
- clearInterval(interval);
2052
- setFiles(
2053
- (prev) => prev.map((f, fi) => fi === prev.length - entries.length + i ? { ...f, progress: 100, status: "complete" } : f)
2054
- );
2055
- } else {
2056
- setFiles(
2057
- (prev) => prev.map((f, fi) => fi === prev.length - entries.length + i ? { ...f, progress } : f)
2058
- );
2389
+ const timer = setInterval(() => {
2390
+ progress = Math.min(100, progress + Math.random() * 30 + 10);
2391
+ const done = progress >= 100;
2392
+ if (done) {
2393
+ clearInterval(timer);
2394
+ timers.current.delete(id2);
2059
2395
  }
2396
+ setFiles((prev) => prev.map((f) => f.id === id2 ? { ...f, progress, status: done ? "complete" : "uploading" } : f));
2060
2397
  }, 300);
2061
- });
2398
+ timers.current.set(id2, timer);
2399
+ }
2062
2400
  },
2063
- [maxSize, onFiles]
2401
+ [accept, maxSize, multiple, onFiles]
2064
2402
  );
2065
2403
  const handleDrop = useCallback(
2066
2404
  (e) => {
@@ -2070,63 +2408,66 @@ function FileUpload({ onFiles, accept, multiple = true, maxSize, className, erro
2070
2408
  },
2071
2409
  [addFiles]
2072
2410
  );
2073
- const removeFile = (index) => {
2074
- setFiles((prev) => prev.filter((_, i) => i !== index));
2411
+ const removeFile = (id2) => {
2412
+ stopTimer(id2);
2413
+ setFiles((prev) => prev.filter((f) => f.id !== id2));
2075
2414
  };
2076
2415
  return /* @__PURE__ */ jsxs("div", { className: cn("space-y-ds-3", className), children: [
2077
- label && /* @__PURE__ */ jsx("label", { id: inputId, className: "block text-sm font-medium text-[var(--ds-text-primary)]", children: label }),
2078
- /* @__PURE__ */ jsxs(
2079
- "div",
2080
- {
2081
- "aria-labelledby": label ? inputId : void 0,
2082
- onDragOver: (e) => {
2083
- e.preventDefault();
2084
- setDragOver(true);
2085
- },
2086
- onDragLeave: () => setDragOver(false),
2087
- onDrop: handleDrop,
2088
- onClick: () => inputRef.current?.click(),
2089
- "aria-invalid": error || void 0,
2090
- className: cn(
2091
- "flex flex-col items-center justify-center gap-ds-2 cursor-pointer group",
2092
- "border-2 border-dashed rounded-[var(--ds-radius-md)]",
2093
- "transition-colors",
2094
- // The real file input is visually hidden inside; Tab lands on it and
2095
- // Enter/Space open the picker natively, so the zone shows its focus.
2096
- HAS_FOCUS_RING,
2097
- error ? "border-[var(--ds-status-error)]" : dragOver ? `${ACTIVE_RING} bg-[var(--ds-accent-whisper)]` : "border-[var(--ds-border-default)] hover:border-[var(--ds-accent-primary)] hover:bg-[var(--ds-accent-whisper)]"
2098
- ),
2099
- style: { padding: "var(--ds-space-8) var(--ds-space-4)" },
2100
- children: [
2101
- /* @__PURE__ */ jsx("div", { className: cn(
2102
- "flex items-center justify-center w-10 h-10 rounded-[var(--ds-radius-full)] transition-colors",
2103
- "bg-[var(--ds-bg-muted)] group-hover:bg-[var(--ds-accent-subtle)]"
2104
- ), children: /* @__PURE__ */ jsx(Icon, { name: "plus", size: 20, className: "text-[var(--ds-text-muted)] group-hover:text-[var(--ds-accent-primary)] transition-colors" }) }),
2105
- /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
2106
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-[var(--ds-text-primary)]", children: "Drop files here or click to browse" }),
2107
- /* @__PURE__ */ jsxs("p", { className: "text-xs text-[var(--ds-text-muted)] mt-ds-1", children: [
2108
- accept ? `Accepted: ${accept}` : "Any file type",
2109
- maxSize && ` \u2022 Max ${formatSize(maxSize)}`
2110
- ] })
2111
- ] }),
2112
- /* @__PURE__ */ jsx(
2113
- "input",
2114
- {
2115
- ref: inputRef,
2116
- type: "file",
2117
- accept,
2118
- multiple,
2119
- className: "sr-only",
2120
- onChange: (e) => {
2121
- if (e.target.files?.length) addFiles(e.target.files);
2122
- e.target.value = "";
2416
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-ds-1.5", children: [
2417
+ showLabel && /* @__PURE__ */ jsx(Label, { htmlFor: inputId, children: label }),
2418
+ /* @__PURE__ */ jsxs(
2419
+ "div",
2420
+ {
2421
+ onDragOver: (e) => {
2422
+ e.preventDefault();
2423
+ setDragOver(true);
2424
+ },
2425
+ onDragLeave: () => setDragOver(false),
2426
+ onDrop: handleDrop,
2427
+ onClick: () => inputRef.current?.click(),
2428
+ className: cn(
2429
+ "flex flex-col items-center justify-center gap-ds-2 cursor-pointer group",
2430
+ "border-2 border-dashed rounded-[var(--ds-radius-md)]",
2431
+ "transition-colors",
2432
+ // The real file input is visually hidden inside; Tab lands on it and
2433
+ // Enter/Space open the picker natively, so the zone shows its focus.
2434
+ invalid ? ERROR_HAS_FOCUS_RING : HAS_FOCUS_RING,
2435
+ dragOver ? `${ACTIVE_RING} bg-[var(--ds-accent-whisper)]` : invalid ? "border-[var(--ds-status-error)]" : "border-[var(--ds-border-default)] hover:border-[var(--ds-accent-primary)] hover:bg-[var(--ds-accent-whisper)]"
2436
+ ),
2437
+ style: { padding: "var(--ds-space-8) var(--ds-space-4)" },
2438
+ children: [
2439
+ /* @__PURE__ */ jsx("div", { className: cn(
2440
+ "flex items-center justify-center w-10 h-10 rounded-[var(--ds-radius-full)] transition-colors",
2441
+ "bg-[var(--ds-bg-muted)] group-hover:bg-[var(--ds-accent-subtle)]"
2442
+ ), children: /* @__PURE__ */ jsx(Icon, { name: "plus", size: 20, className: "text-[var(--ds-text-muted)] group-hover:text-[var(--ds-accent-primary)] transition-colors" }) }),
2443
+ /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
2444
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-[var(--ds-text-primary)]", children: multiple ? "Drop files here or browse" : "Drop a file here or browse" }),
2445
+ constraints && /* @__PURE__ */ jsx("p", { id: constraintsId, className: "text-xs text-[var(--ds-text-muted)] mt-ds-1", children: constraints })
2446
+ ] }),
2447
+ /* @__PURE__ */ jsx(
2448
+ "input",
2449
+ {
2450
+ ref: inputRef,
2451
+ id: inputId,
2452
+ type: "file",
2453
+ accept,
2454
+ multiple,
2455
+ className: "sr-only",
2456
+ "aria-labelledby": field ? field.labelId : void 0,
2457
+ "aria-describedby": describedBy,
2458
+ "aria-invalid": invalid || void 0,
2459
+ onChange: (e) => {
2460
+ if (e.target.files?.length) addFiles(e.target.files);
2461
+ e.target.value = "";
2462
+ }
2123
2463
  }
2124
- }
2125
- )
2126
- ]
2127
- }
2128
- ),
2129
- files.length > 0 && /* @__PURE__ */ jsx("ul", { className: "space-y-ds-2", children: files.map((entry, i) => /* @__PURE__ */ jsxs(
2464
+ )
2465
+ ]
2466
+ }
2467
+ ),
2468
+ rejected && /* @__PURE__ */ jsx(InlineFieldError, { id: rejectedId, message: rejected, className: "mt-0" })
2469
+ ] }),
2470
+ files.length > 0 && /* @__PURE__ */ jsx("ul", { className: "space-y-ds-2", children: files.map((entry) => /* @__PURE__ */ jsxs(
2130
2471
  "li",
2131
2472
  {
2132
2473
  className: cn(
@@ -2143,10 +2484,10 @@ function FileUpload({ onFiles, accept, multiple = true, maxSize, className, erro
2143
2484
  entry.status === "uploading" && /* @__PURE__ */ jsx(Progress, { value: entry.progress, size: "sm", className: "mt-ds-1" })
2144
2485
  ] }),
2145
2486
  entry.status === "complete" && /* @__PURE__ */ jsx(Icon, { name: "check", size: 16, className: "text-[var(--ds-status-success)] shrink-0" }),
2146
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => removeFile(i), "aria-label": "Remove file", children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 14 }) })
2487
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => removeFile(entry.id), "aria-label": "Remove file", children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 14 }) })
2147
2488
  ]
2148
2489
  },
2149
- `${entry.file.name}-${i}`
2490
+ entry.id
2150
2491
  )) })
2151
2492
  ] });
2152
2493
  }
@@ -2161,9 +2502,17 @@ function SliderRange({
2161
2502
  className,
2162
2503
  ref,
2163
2504
  error = false,
2164
- label = "Value",
2505
+ label,
2506
+ "aria-label": ariaLabel,
2507
+ "aria-describedby": describedByProp,
2165
2508
  trackStyle
2166
2509
  }) {
2510
+ const field = useField();
2511
+ const ownId = useId();
2512
+ const showLabel = label != null && !field;
2513
+ const nameId = field ? field.labelId : showLabel ? `${ownId}-label` : void 0;
2514
+ const describedBy = [...new Set([describedByProp, field?.describedBy].join(" ").split(" ").filter(Boolean))].join(" ") || void 0;
2515
+ const thumbName = (part) => nameId ? { "aria-labelledby": part ? `${nameId} ${ownId}-${part}` : nameId } : { "aria-label": ariaLabel ? part ? `${ariaLabel} ${part}` : ariaLabel : void 0 };
2167
2516
  const isRange = Array.isArray(value);
2168
2517
  const low = isRange ? value[0] : value;
2169
2518
  const high = isRange ? value[1] : value;
@@ -2219,7 +2568,14 @@ function SliderRange({
2219
2568
  [disabled, min, max, handleChange]
2220
2569
  );
2221
2570
  return /* @__PURE__ */ jsxs("div", { ref, className: cn("space-y-ds-1", disabled && "opacity-disabled", className), children: [
2222
- showValue && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end", children: /* @__PURE__ */ jsx("span", { className: "text-xs font-mono tabular-nums text-[var(--ds-text-muted)]", children: isRange ? `${low} \u2013 ${high}` : low }) }),
2571
+ (showLabel || showValue) && /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-ds-2", showLabel ? "justify-between" : "justify-end"), children: [
2572
+ showLabel && /* @__PURE__ */ jsx("span", { id: `${ownId}-label`, className: "text-sm font-medium leading-none text-[var(--ds-text-secondary)]", children: label }),
2573
+ showValue && /* @__PURE__ */ jsx("span", { className: "text-xs font-mono tabular-nums text-[var(--ds-text-muted)]", children: isRange ? `${low} \u2013 ${high}` : low })
2574
+ ] }),
2575
+ isRange && nameId && /* @__PURE__ */ jsxs(Fragment, { children: [
2576
+ /* @__PURE__ */ jsx("span", { id: `${ownId}-minimum`, hidden: true, children: "minimum" }),
2577
+ /* @__PURE__ */ jsx("span", { id: `${ownId}-maximum`, hidden: true, children: "maximum" })
2578
+ ] }),
2223
2579
  /* @__PURE__ */ jsxs(
2224
2580
  "div",
2225
2581
  {
@@ -2242,7 +2598,8 @@ function SliderRange({
2242
2598
  {
2243
2599
  role: "slider",
2244
2600
  tabIndex: disabled ? -1 : 0,
2245
- "aria-label": `${label} minimum`,
2601
+ ...thumbName("minimum"),
2602
+ "aria-describedby": describedBy,
2246
2603
  "aria-valuemin": min,
2247
2604
  "aria-valuemax": high,
2248
2605
  "aria-valuenow": low,
@@ -2266,7 +2623,8 @@ function SliderRange({
2266
2623
  {
2267
2624
  role: "slider",
2268
2625
  tabIndex: disabled ? -1 : 0,
2269
- "aria-label": isRange ? `${label} maximum` : label,
2626
+ ...thumbName(isRange ? "maximum" : void 0),
2627
+ "aria-describedby": describedBy,
2270
2628
  "aria-valuemin": isRange ? low : min,
2271
2629
  "aria-valuemax": max,
2272
2630
  "aria-valuenow": high,
@@ -2365,14 +2723,25 @@ function TimePicker({
2365
2723
  const size = sizeProp ?? contextSize ?? "md";
2366
2724
  const [open, setOpen] = useState(false);
2367
2725
  const triggerRef = useRef(null);
2726
+ const buttonRef = useRef(null);
2368
2727
  const panelRef = useRef(null);
2369
2728
  const listboxId = useId();
2370
2729
  const pos = usePosition(triggerRef, panelRef, open, { side: "bottom", align: "start", offset: 4, flip: true });
2371
2730
  const parsed = parseTime(value);
2372
2731
  const hours = generateHours(use24Hour);
2373
2732
  const minutes = generateMinutes(minuteStep);
2374
- const isTopLayer = useOverlayLayer(open);
2375
- useCloseOnFocusOut(open, triggerRef, panelRef, () => setOpen(false), isTopLayer);
2733
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: buttonRef, panel: panelRef, close: () => setOpen(false), tabExits: "edges" });
2734
+ const [columnStop, setColumnStop] = useState({});
2735
+ const [prevOpen, setPrevOpen] = useState(open);
2736
+ if (open !== prevOpen) {
2737
+ setPrevOpen(open);
2738
+ if (open) setColumnStop({});
2739
+ }
2740
+ const stopOf = (column, chosen) => columnStop[column] ?? Math.max(0, chosen);
2741
+ const holdStop = (column, index) => setColumnStop((s) => s[column] === index ? s : { ...s, [column]: index });
2742
+ const chosenHour = hours.findIndex((h) => use24Hour ? h === parseInt(value) : h === parsed.hour);
2743
+ const chosenMinute = minutes.findIndex((m) => m === parsed.minute);
2744
+ const chosenPeriod = parsed.period === "PM" ? 1 : 0;
2376
2745
  useEffect(() => {
2377
2746
  if (!open) return;
2378
2747
  const id2 = requestAnimationFrame(() => {
@@ -2424,7 +2793,6 @@ function TimePicker({
2424
2793
  const el = segmentRefs.current[Math.max(0, Math.min(segmentCount - 1, i))];
2425
2794
  el?.focus();
2426
2795
  };
2427
- const focusSegmentRef = useLatest(focusSegment);
2428
2796
  useEffect(() => {
2429
2797
  if (!open) return;
2430
2798
  const handler = (e) => {
@@ -2432,10 +2800,10 @@ function TimePicker({
2432
2800
  setOpen(false);
2433
2801
  };
2434
2802
  const keyHandler = (e) => {
2435
- if (e.key === "Escape" && isTopLayer()) {
2436
- setOpen(false);
2437
- focusSegmentRef.current(0);
2438
- }
2803
+ if (e.key !== "Escape" || e.defaultPrevented || !isTopLayer()) return;
2804
+ e.preventDefault();
2805
+ returnFocus();
2806
+ setOpen(false);
2439
2807
  };
2440
2808
  document.addEventListener("mousedown", handler);
2441
2809
  document.addEventListener("keydown", keyHandler);
@@ -2443,7 +2811,7 @@ function TimePicker({
2443
2811
  document.removeEventListener("mousedown", handler);
2444
2812
  document.removeEventListener("keydown", keyHandler);
2445
2813
  };
2446
- }, [open, isTopLayer, focusSegmentRef]);
2814
+ }, [open, isTopLayer, returnFocus]);
2447
2815
  const commitIfComplete = (next) => {
2448
2816
  if (next.hour === null || next.minute === null) return;
2449
2817
  let h24 = next.hour;
@@ -2619,6 +2987,7 @@ function TimePicker({
2619
2987
  /* @__PURE__ */ jsx(
2620
2988
  "button",
2621
2989
  {
2990
+ ref: buttonRef,
2622
2991
  type: "button",
2623
2992
  "aria-label": "Choose a time",
2624
2993
  "aria-haspopup": "dialog",
@@ -2651,10 +3020,12 @@ function TimePicker({
2651
3020
  ),
2652
3021
  style: { top: pos.top, left: pos.left },
2653
3022
  children: /* @__PURE__ */ jsxs("div", { className: "inline-flex p-ds-1 gap-ds-1 tabular-nums", children: [
2654
- /* @__PURE__ */ jsx("div", { "data-column": true, className: "max-h-48 overflow-y-auto w-ds-10", children: hours.map((h) => /* @__PURE__ */ jsx(
3023
+ /* @__PURE__ */ jsx("div", { "data-column": true, className: "max-h-48 overflow-y-auto w-ds-10", children: hours.map((h, i) => /* @__PURE__ */ jsx(
2655
3024
  "button",
2656
3025
  {
2657
3026
  type: "button",
3027
+ tabIndex: i === stopOf("hour", chosenHour) ? 0 : -1,
3028
+ onFocus: () => holdStop("hour", i),
2658
3029
  "data-selected": use24Hour ? h === parseInt(value) : h === parsed.hour,
2659
3030
  onMouseDown: (e) => e.preventDefault(),
2660
3031
  onClick: () => emitChange(h, parsed.minute, parsed.period),
@@ -2668,10 +3039,12 @@ function TimePicker({
2668
3039
  h
2669
3040
  )) }),
2670
3041
  /* @__PURE__ */ jsx("div", { className: "w-px bg-[var(--ds-border-subtle)]" }),
2671
- /* @__PURE__ */ jsx("div", { "data-column": true, className: "max-h-48 overflow-y-auto w-ds-10", children: minutes.map((m) => /* @__PURE__ */ jsx(
3042
+ /* @__PURE__ */ jsx("div", { "data-column": true, className: "max-h-48 overflow-y-auto w-ds-10", children: minutes.map((m, i) => /* @__PURE__ */ jsx(
2672
3043
  "button",
2673
3044
  {
2674
3045
  type: "button",
3046
+ tabIndex: i === stopOf("minute", chosenMinute) ? 0 : -1,
3047
+ onFocus: () => holdStop("minute", i),
2675
3048
  "data-selected": m === parsed.minute,
2676
3049
  onMouseDown: (e) => e.preventDefault(),
2677
3050
  onClick: () => emitChange(parsed.hour, m, parsed.period),
@@ -2686,10 +3059,12 @@ function TimePicker({
2686
3059
  )) }),
2687
3060
  !use24Hour && /* @__PURE__ */ jsxs(Fragment, { children: [
2688
3061
  /* @__PURE__ */ jsx("div", { className: "w-px bg-[var(--ds-border-subtle)]" }),
2689
- /* @__PURE__ */ jsx("div", { "data-column": true, className: "flex flex-col gap-ds-1 w-ds-10", children: ["AM", "PM"].map((p) => /* @__PURE__ */ jsx(
3062
+ /* @__PURE__ */ jsx("div", { "data-column": true, className: "flex flex-col gap-ds-1 w-ds-10", children: ["AM", "PM"].map((p, i) => /* @__PURE__ */ jsx(
2690
3063
  "button",
2691
3064
  {
2692
3065
  type: "button",
3066
+ tabIndex: i === stopOf("period", chosenPeriod) ? 0 : -1,
3067
+ onFocus: () => holdStop("period", i),
2693
3068
  "data-selected": p === parsed.period,
2694
3069
  onMouseDown: (e) => e.preventDefault(),
2695
3070
  onClick: () => emitChange(parsed.hour, parsed.minute, p),
@@ -3016,7 +3391,8 @@ var Tag = forwardRef(function Tag2({
3016
3391
  {
3017
3392
  type: "button",
3018
3393
  onClick: handleRemove,
3019
- "aria-label": "Remove",
3394
+ "data-tag-remove": "",
3395
+ "aria-label": typeof children === "string" ? `Remove ${children}` : "Remove",
3020
3396
  className: cn(
3021
3397
  "inline-flex items-center justify-center shrink-0",
3022
3398
  "rounded-[var(--ds-radius-sm)]",
@@ -3063,6 +3439,16 @@ function TagInput({
3063
3439
  },
3064
3440
  [value, onChange]
3065
3441
  );
3442
+ const containerRef = useRef(null);
3443
+ const pendingFocus = useRef(null);
3444
+ useLayoutEffect(() => {
3445
+ const pending = pendingFocus.current;
3446
+ if (pending === null) return;
3447
+ pendingFocus.current = null;
3448
+ if (value.includes(pending.tag)) return;
3449
+ const buttons = Array.from(containerRef.current?.querySelectorAll("[data-tag-remove]") ?? []);
3450
+ focusAfterRemoval(buttons, pending.index, inputRef.current);
3451
+ });
3066
3452
  const handleKeyDown = useCallback(
3067
3453
  (e) => {
3068
3454
  if ((e.key === "Enter" || e.key === ",") && input.trim()) {
@@ -3095,7 +3481,10 @@ function TagInput({
3095
3481
  return /* @__PURE__ */ jsxs(
3096
3482
  "div",
3097
3483
  {
3098
- onClick: () => inputRef.current?.focus(),
3484
+ ref: containerRef,
3485
+ onClick: (e) => {
3486
+ if (!(e.target instanceof Element && e.target.closest("button"))) inputRef.current?.focus();
3487
+ },
3099
3488
  className: cn(
3100
3489
  "flex flex-wrap items-center gap-[calc(var(--ds-space-1)*1.5)] min-h-[calc(var(--ds-space-1)*10.5)] px-ds-2 py-[calc(var(--ds-space-1)*1.5)]",
3101
3490
  "bg-[var(--ds-input-bg)] text-[var(--ds-text-primary)]",
@@ -3110,10 +3499,13 @@ function TagInput({
3110
3499
  className
3111
3500
  ),
3112
3501
  children: [
3113
- value.map((tag) => /* @__PURE__ */ jsx(
3502
+ value.map((tag, i) => /* @__PURE__ */ jsx(
3114
3503
  Tag,
3115
3504
  {
3116
- onRemove: disabled ? void 0 : () => removeTag(tag),
3505
+ onRemove: disabled ? void 0 : () => {
3506
+ pendingFocus.current = { index: i, tag };
3507
+ removeTag(tag);
3508
+ },
3117
3509
  disabled,
3118
3510
  children: tag
3119
3511
  },
@@ -3640,60 +4032,6 @@ function ColorPicker({
3640
4032
  }
3641
4033
  );
3642
4034
  }
3643
- function InlineFieldError({ message, id, className }) {
3644
- return /* @__PURE__ */ jsxs(
3645
- "p",
3646
- {
3647
- id,
3648
- role: "alert",
3649
- className: cn(
3650
- "flex items-center gap-ds-1.5 text-xs text-[var(--ds-status-error)]",
3651
- "mt-ds-1.5",
3652
- className
3653
- ),
3654
- children: [
3655
- /* @__PURE__ */ jsx(Icon, { name: "error", size: 14, className: "shrink-0" }),
3656
- /* @__PURE__ */ jsx("span", { children: message })
3657
- ]
3658
- }
3659
- );
3660
- }
3661
- function Field({ label, children, hint, error, required, labelAction, id: idProp, className }) {
3662
- const generated = useId();
3663
- const id = idProp ?? `field-${generated}`;
3664
- const hintId = `${id}-hint`;
3665
- const errorId = `${id}-error`;
3666
- const hasError = typeof error === "string" && error.length > 0;
3667
- const describedBy = [hint ? hintId : null, hasError ? errorId : null].filter(Boolean).join(" ") || void 0;
3668
- const controlProps = {
3669
- id,
3670
- "aria-describedby": describedBy,
3671
- ...hasError ? { "aria-invalid": true, error: true } : {},
3672
- ...required ? { "aria-required": true, required: true } : {}
3673
- };
3674
- let control;
3675
- if (typeof children === "function") {
3676
- control = children(controlProps);
3677
- } else {
3678
- const child = Children.only(children);
3679
- if (!isValidElement(child)) throw new Error("Field expects a single form control as its child");
3680
- const isNative = typeof child.type === "string";
3681
- const { error: errorFlag, ...ariaOnly } = controlProps;
3682
- control = cloneElement(child, isNative ? ariaOnly : { ...ariaOnly, ...errorFlag ? { error: true } : {} });
3683
- }
3684
- return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-ds-1.5", className), children: [
3685
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-ds-2", children: [
3686
- /* @__PURE__ */ jsxs(Label, { htmlFor: id, children: [
3687
- label,
3688
- required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "ml-ds-0.5 text-[var(--ds-status-error)]", children: "*" })
3689
- ] }),
3690
- labelAction
3691
- ] }),
3692
- control,
3693
- hint && /* @__PURE__ */ jsx("p", { id: hintId, className: "text-xs text-[var(--ds-text-muted)]", children: hint }),
3694
- hasError && /* @__PURE__ */ jsx(InlineFieldError, { id: errorId, message: error, className: "mt-0" })
3695
- ] });
3696
- }
3697
4035
  var variantStyles2 = {
3698
4036
  info: "border-[var(--ds-accent-primary)] bg-[var(--ds-accent-subtle)]",
3699
4037
  success: "border-[var(--ds-status-success)] bg-[var(--ds-status-success-bg)]",
@@ -4369,7 +4707,6 @@ function MentionInput({
4369
4707
  const value = controlledValue ?? internalValue;
4370
4708
  const [query, setQuery] = useState("");
4371
4709
  const [showPopup, setShowPopup] = useState(false);
4372
- useOverlayLayer(showPopup);
4373
4710
  const [activeIndex, setActiveIndex] = useState(0);
4374
4711
  const [triggerStart, setTriggerStart] = useState(-1);
4375
4712
  const [popupPos, setPopupPos] = useState({ top: 0, left: 0 });
@@ -4379,6 +4716,7 @@ function MentionInput({
4379
4716
  const filtered = suggestions.filter(
4380
4717
  (s) => s.label.toLowerCase().includes(query.toLowerCase()) || s.description?.toLowerCase().includes(query.toLowerCase())
4381
4718
  );
4719
+ useOverlayLayer(showPopup && filtered.length > 0);
4382
4720
  const updateValue = useCallback(
4383
4721
  (v) => {
4384
4722
  if (controlledValue === void 0) setInternalValue(v);
@@ -4457,6 +4795,7 @@ function MentionInput({
4457
4795
  e.preventDefault();
4458
4796
  insertMention(filtered[activeIndex]);
4459
4797
  } else if (e.key === "Escape") {
4798
+ e.preventDefault();
4460
4799
  setShowPopup(false);
4461
4800
  }
4462
4801
  };
@@ -4560,6 +4899,8 @@ function MentionInput({
4560
4899
  ] });
4561
4900
  }
4562
4901
  var CardCtx = createContext(false);
4902
+ var CardActionCtx = createContext(null);
4903
+ var CARD_ACTION_ATTR = "data-card-action";
4563
4904
  var CardSurfaceCtx = createContext(false);
4564
4905
  var variantStyles3 = {
4565
4906
  default: `bg-[var(--ds-bg-surface)] border-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)] ${GLASS_SURFACE}`,
@@ -4679,21 +5020,57 @@ var Card = forwardRef(function Card2({
4679
5020
  const isSelectable = selectable || !!selectType;
4680
5021
  const isToggle = selectType === "toggle";
4681
5022
  const hasSelectType = !!selectType;
5023
+ const isAction = !isSelectable && variant === "interactive" && !!onClick;
5024
+ const rootRef = useRef(null);
5025
+ const setRoot = useCallback((node) => {
5026
+ rootRef.current = node;
5027
+ if (typeof ref === "function") ref(node);
5028
+ else if (ref) ref.current = node;
5029
+ }, [ref]);
5030
+ const [inner, setInner] = useState({ controls: false, titleId: null });
5031
+ useLayoutEffect(() => {
5032
+ const root = rootRef.current;
5033
+ if (!root || !isAction && !checkable) return;
5034
+ const controls = Array.from(root.querySelectorAll(FOCUSABLE)).some((el) => !el.hasAttribute(CARD_ACTION_ATTR));
5035
+ const titleId = root.querySelector("[data-card-title]")?.id || null;
5036
+ setInner((cur) => cur.controls === controls && cur.titleId === titleId ? cur : { controls, titleId });
5037
+ }, [isAction, checkable, loading, children]);
5038
+ const holdsControls = isAction && inner.controls;
5039
+ const action = useMemo(() => holdsControls ? { activate: () => rootRef.current?.click(), disabled } : null, [holdsControls, disabled]);
4682
5040
  const handleClick = isSelectable && !disabled ? (e) => {
4683
5041
  onSelectedChange?.(!selected);
4684
5042
  onClick?.(e);
4685
- } : onClick;
5043
+ } : isAction && disabled ? void 0 : onClick;
5044
+ const handleCardClick = (e) => {
5045
+ if (disabled || !(e.target instanceof Element) || !e.currentTarget.contains(e.target)) return;
5046
+ const control = e.target.closest(FOCUSABLE);
5047
+ if (control && control !== e.currentTarget && e.currentTarget.contains(control)) return;
5048
+ onClick?.(e);
5049
+ };
4686
5050
  const handleKeyDown = isSelectable && !disabled ? (e) => {
4687
5051
  if (e.key === "Enter" || e.key === " ") {
4688
5052
  e.preventDefault();
4689
5053
  onSelectedChange?.(!selected);
4690
5054
  }
5055
+ } : isAction && !disabled ? (e) => {
5056
+ if (e.target !== e.currentTarget) return;
5057
+ if (e.key === "Enter") {
5058
+ e.preventDefault();
5059
+ e.currentTarget.click();
5060
+ } else if (e.key === " ") e.preventDefault();
5061
+ } : void 0;
5062
+ const handleKeyUp = isAction && !disabled ? (e) => {
5063
+ if (e.target === e.currentTarget && e.key === " ") {
5064
+ e.preventDefault();
5065
+ e.currentTarget.click();
5066
+ }
4691
5067
  } : void 0;
4692
5068
  const handleCheckableClick = (e) => {
4693
5069
  e.stopPropagation();
4694
5070
  onSelectedChange?.(!selected);
4695
5071
  };
4696
- const role = selectType === "radio" ? "radio" : selectType === "checkbox" ? "checkbox" : selectType === "toggle" ? "switch" : isSelectable ? "button" : void 0;
5072
+ const isButton = isSelectable || isAction && !holdsControls;
5073
+ const role = selectType === "radio" ? "radio" : selectType === "checkbox" ? "checkbox" : selectType === "toggle" ? "switch" : isButton ? "button" : void 0;
4697
5074
  const ariaAttr = selectType === "radio" || selectType === "checkbox" || selectType === "toggle" ? { "aria-checked": !!selected } : isSelectable ? { "aria-pressed": !!selected } : {};
4698
5075
  const showRing = isSelectable && !isToggle && !hasSelectType && selected;
4699
5076
  const indicator = selectType === "radio" ? /* @__PURE__ */ jsx(RadioIndicator, { checked: !!selected, disabled }) : selectType === "checkbox" ? /* @__PURE__ */ jsx(CheckboxIndicator, { checked: !!selected, indeterminate, disabled }) : selectType === "toggle" ? /* @__PURE__ */ jsx(ToggleIndicator, { checked: !!selected, disabled }) : null;
@@ -4701,11 +5078,11 @@ var Card = forwardRef(function Card2({
4701
5078
  return /* @__PURE__ */ jsxs(
4702
5079
  "div",
4703
5080
  {
4704
- ref,
5081
+ ref: setRoot,
4705
5082
  "data-card": "",
4706
5083
  role,
4707
- tabIndex: isSelectable ? 0 : void 0,
4708
- "aria-disabled": isSelectable && disabled ? true : void 0,
5084
+ tabIndex: isSelectable || isButton && !disabled ? 0 : void 0,
5085
+ "aria-disabled": isButton && disabled ? true : void 0,
4709
5086
  ...name && selectType === "radio" ? { "data-radio-group": name } : {},
4710
5087
  style: variant === "interactive" ? { ...INTERACTIVE_SCALE_CAP, ...style } : style,
4711
5088
  className: cn(
@@ -4713,14 +5090,15 @@ var Card = forwardRef(function Card2({
4713
5090
  variantStyles3[variant],
4714
5091
  isSelectable && !disabled && "cursor-pointer select-none transition-all duration-[var(--ds-transition-speed)] ease-[var(--ds-transition-ease)]",
4715
5092
  isSelectable && !disabled && !selected && "hover:border-[var(--ds-accent-primary)]",
4716
- isSelectable && "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
5093
+ isButton && "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
4717
5094
  (showRing || hasSelectType && selected) && ACTIVE_RING,
4718
5095
  hasSelectType && selected && "bg-[var(--ds-accent-subtle)]",
4719
5096
  disabled && "opacity-disabled cursor-not-allowed",
4720
5097
  className
4721
5098
  ),
4722
- onClick: handleClick,
4723
- onKeyDown: handleKeyDown,
5099
+ onClick: holdsControls ? handleCardClick : handleClick,
5100
+ onKeyDown: holdsControls ? void 0 : handleKeyDown,
5101
+ onKeyUp: holdsControls ? void 0 : handleKeyUp,
4724
5102
  ...ariaAttr,
4725
5103
  ...props,
4726
5104
  children: [
@@ -4733,6 +5111,7 @@ var Card = forwardRef(function Card2({
4733
5111
  onClick: handleCheckableClick,
4734
5112
  role: "checkbox",
4735
5113
  "aria-checked": !!selected,
5114
+ ...inner.titleId ? { "aria-labelledby": inner.titleId } : { "aria-label": "Select" },
4736
5115
  tabIndex: 0,
4737
5116
  onKeyDown: (e) => {
4738
5117
  if (e.key === "Enter" || e.key === " ") {
@@ -4744,7 +5123,7 @@ var Card = forwardRef(function Card2({
4744
5123
  children: /* @__PURE__ */ jsx(CheckboxIndicator, { checked: !!selected })
4745
5124
  }
4746
5125
  ),
4747
- /* @__PURE__ */ jsx(CardSurfaceCtx, { value: true, children: /* @__PURE__ */ jsx(CardCtx, { value: checkable, children: loading ? (
5126
+ /* @__PURE__ */ jsx(CardSurfaceCtx, { value: true, children: /* @__PURE__ */ jsx(CardActionCtx, { value: action, children: /* @__PURE__ */ jsx(CardCtx, { value: checkable, children: loading ? (
4748
5127
  // Round 15 (the owner's 52.1): a placeholder stands where the content will stand — inside the card's
4749
5128
  // content inset (CardContent's padding), a title-height line then two body lines — never flush against the border.
4750
5129
  /* @__PURE__ */ jsx("div", { "data-card-loading": "", "aria-busy": "true", className: cn("pr-ds-5 py-ds-4", checkable ? "pl-ds-10" : "pl-ds-5"), children: /* @__PURE__ */ jsxs("div", { className: "space-y-ds-3", children: [
@@ -4752,7 +5131,7 @@ var Card = forwardRef(function Card2({
4752
5131
  /* @__PURE__ */ jsx(Skeleton, { variant: "custom", className: "h-3 w-full" }),
4753
5132
  /* @__PURE__ */ jsx(Skeleton, { variant: "custom", className: "h-3 w-2/3" })
4754
5133
  ] }) })
4755
- ) : children }) })
5134
+ ) : children }) }) })
4756
5135
  ]
4757
5136
  }
4758
5137
  );
@@ -4770,6 +5149,7 @@ var CardHeader = forwardRef(function CardHeader2({ className, children, actions,
4770
5149
  "button",
4771
5150
  {
4772
5151
  type: "button",
5152
+ "aria-label": "More actions",
4773
5153
  className: cn(
4774
5154
  "inline-flex items-center justify-center h-8 w-8 rounded-[var(--ds-radius-md)]",
4775
5155
  "text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] hover:bg-[var(--ds-bg-muted)]",
@@ -4806,20 +5186,42 @@ var cardLineClampClasses = {
4806
5186
  5: "line-clamp-5",
4807
5187
  6: "line-clamp-6"
4808
5188
  };
4809
- var CardTitle = forwardRef(function CardTitle2({ className, children, lines = 2, ...props }, forwardedRef) {
5189
+ var CardTitle = forwardRef(function CardTitle2({ className, children, lines = 2, id, ...props }, forwardedRef) {
5190
+ const action = useContext(CardActionCtx);
5191
+ const generatedId = useId();
4810
5192
  const { ref: overflowRef, isTruncated } = useTextOverflow({ lines, disabled: lines === 0 });
4811
5193
  const lineClampClass = lines > 0 ? cardLineClampClasses[lines] || `line-clamp-[${lines}]` : "";
4812
5194
  const heading = /* @__PURE__ */ jsx(
4813
5195
  "h3",
4814
5196
  {
4815
5197
  ref: (node) => {
4816
- overflowRef(lines > 0 ? node : null);
5198
+ if (!action) overflowRef(lines > 0 ? node : null);
4817
5199
  if (typeof forwardedRef === "function") forwardedRef(node);
4818
5200
  else if (forwardedRef) forwardedRef.current = node;
4819
5201
  },
4820
- className: cn("font-[family-name:var(--ds-font-display)] text-lg font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)]", lines > 0 && lineClampClass, className),
5202
+ id: id ?? generatedId,
5203
+ "data-card-title": "",
5204
+ className: cn("font-[family-name:var(--ds-font-display)] text-lg font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)]", !action && lines > 0 && lineClampClass, className),
4821
5205
  ...props,
4822
- children
5206
+ children: action ? (
5207
+ // The card's action when the card holds controls of its own (Sprint 25.7): a real button, which a click
5208
+ // elsewhere on the card also runs. Its ::after spans the card to draw the card's focus ring, taking no clicks.
5209
+ // The clamp moves inside, since a button lays out its own lines.
5210
+ /* @__PURE__ */ jsx(
5211
+ "button",
5212
+ {
5213
+ type: "button",
5214
+ ...{ [CARD_ACTION_ATTR]: "" },
5215
+ disabled: action.disabled,
5216
+ onClick: action.activate,
5217
+ className: cn(
5218
+ "block w-full text-left cursor-pointer [text-overflow:inherit] focus-visible:outline-none disabled:cursor-not-allowed",
5219
+ "after:absolute after:inset-0 after:rounded-[var(--ds-radius-md)] after:pointer-events-none focus-visible:after:shadow-[var(--ds-shadow-focus)]"
5220
+ ),
5221
+ children: /* @__PURE__ */ jsx("span", { ref: (node) => overflowRef(lines > 0 ? node : null), className: cn("block [text-overflow:inherit]", lines > 0 && lineClampClass), children })
5222
+ }
5223
+ )
5224
+ ) : children
4823
5225
  }
4824
5226
  );
4825
5227
  if (!isTruncated || lines === 0) return heading;
@@ -5278,6 +5680,7 @@ function Table({
5278
5680
  return /* @__PURE__ */ jsxs(
5279
5681
  "div",
5280
5682
  {
5683
+ "data-row": "",
5281
5684
  className: cn(
5282
5685
  "border-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
5283
5686
  "rounded-[var(--ds-radius-md)] bg-[var(--ds-bg-surface)]",
@@ -5830,11 +6233,34 @@ function OverflowPopover({
5830
6233
  offset: 4,
5831
6234
  flip: true
5832
6235
  });
6236
+ const { isTop, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: floatingRef, close: () => setOpen(false), tabExits: "edges" });
6237
+ useEffect(() => {
6238
+ if (!open) return;
6239
+ const raf = requestAnimationFrame(() => focusInitial(floatingRef.current));
6240
+ const onKey = (e) => {
6241
+ if (e.key !== "Escape" || e.defaultPrevented || !isTop()) return;
6242
+ e.preventDefault();
6243
+ returnFocus();
6244
+ setOpen(false);
6245
+ };
6246
+ const onDown = (e) => {
6247
+ if (!triggerRef.current?.contains(e.target)) setOpen(false);
6248
+ };
6249
+ document.addEventListener("keydown", onKey);
6250
+ document.addEventListener("mousedown", onDown);
6251
+ return () => {
6252
+ cancelAnimationFrame(raf);
6253
+ document.removeEventListener("keydown", onKey);
6254
+ document.removeEventListener("mousedown", onDown);
6255
+ };
6256
+ }, [open, isTop, returnFocus]);
5833
6257
  return /* @__PURE__ */ jsxs(Fragment, { children: [
5834
6258
  /* @__PURE__ */ jsxs(
5835
6259
  "button",
5836
6260
  {
5837
6261
  ref: triggerRef,
6262
+ "aria-haspopup": "dialog",
6263
+ "aria-expanded": open,
5838
6264
  className: "text-2xs text-[var(--ds-accent-primary)] font-medium cursor-pointer hover:underline px-ds-1.5",
5839
6265
  onClick: (e) => {
5840
6266
  e.stopPropagation();
@@ -5852,6 +6278,8 @@ function OverflowPopover({
5852
6278
  "div",
5853
6279
  {
5854
6280
  ref: floatingRef,
6281
+ role: "dialog",
6282
+ "aria-label": date.toLocaleDateString(void 0, { weekday: "long", month: "long", day: "numeric" }),
5855
6283
  className: cn(
5856
6284
  "fixed z-[var(--ds-z-popover)] w-56 max-h-48 overflow-y-auto",
5857
6285
  "bg-[var(--ds-bg-surface-elevated)]",
@@ -6468,6 +6896,9 @@ function CompactDayCell({
6468
6896
  const today = isCurrentMonth && isToday(day);
6469
6897
  const selected = isCurrentMonth && selectedDate && isSameDay2(day, selectedDate);
6470
6898
  const hasEvents = dayEvents.length > 0;
6899
+ const shown = open && hasEvents;
6900
+ const isTop = useOverlayLayer(shown);
6901
+ useCloseOnFocusOut(shown, cellRef, floatingRef, () => setOpen(false), isTop);
6471
6902
  const eventColors = dayEvents.slice(0, 3).map((e) => categories[e.category ?? ""]?.color ?? "var(--ds-accent-primary)");
6472
6903
  const handleMouseEnter = () => {
6473
6904
  if (!hasEvents) return;
@@ -6486,7 +6917,9 @@ function CompactDayCell({
6486
6917
  useEffect(() => {
6487
6918
  if (!open) return;
6488
6919
  const onKey = (e) => {
6489
- if (e.key === "Escape") setOpen(false);
6920
+ if (e.key !== "Escape" || e.defaultPrevented || !isTop()) return;
6921
+ e.preventDefault();
6922
+ setOpen(false);
6490
6923
  };
6491
6924
  const onClick = (e) => {
6492
6925
  const t = e.target;
@@ -6499,7 +6932,7 @@ function CompactDayCell({
6499
6932
  document.removeEventListener("keydown", onKey);
6500
6933
  document.removeEventListener("mousedown", onClick);
6501
6934
  };
6502
- }, [open]);
6935
+ }, [open, isTop]);
6503
6936
  return /* @__PURE__ */ jsxs(Fragment, { children: [
6504
6937
  /* @__PURE__ */ jsxs(
6505
6938
  "button",
@@ -6663,10 +7096,18 @@ function MonthDropdown({
6663
7096
  setPrevBrowseSyncKey(browseSyncKey);
6664
7097
  if (open) setBrowseYear(currentDate.getFullYear());
6665
7098
  }
7099
+ const { isTop, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: floatingRef, close: () => setOpen(false), tabExits: "edges" });
6666
7100
  useEffect(() => {
6667
7101
  if (!open) return;
7102
+ const raf = requestAnimationFrame(() => {
7103
+ const panel = floatingRef.current;
7104
+ (panel?.querySelector("[data-current]") ?? panel?.querySelector("button"))?.focus();
7105
+ });
6668
7106
  const onKey = (e) => {
6669
- if (e.key === "Escape") setOpen(false);
7107
+ if (e.key !== "Escape" || e.defaultPrevented || !isTop()) return;
7108
+ e.preventDefault();
7109
+ returnFocus();
7110
+ setOpen(false);
6670
7111
  };
6671
7112
  const onClick = (e) => {
6672
7113
  const t = e.target;
@@ -6676,10 +7117,11 @@ function MonthDropdown({
6676
7117
  document.addEventListener("keydown", onKey);
6677
7118
  document.addEventListener("mousedown", onClick);
6678
7119
  return () => {
7120
+ cancelAnimationFrame(raf);
6679
7121
  document.removeEventListener("keydown", onKey);
6680
7122
  document.removeEventListener("mousedown", onClick);
6681
7123
  };
6682
- }, [open]);
7124
+ }, [open, isTop, returnFocus]);
6683
7125
  const currentMonth = currentDate.getMonth();
6684
7126
  const currentYear = currentDate.getFullYear();
6685
7127
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -6749,7 +7191,9 @@ function MonthDropdown({
6749
7191
  "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
6750
7192
  isActive ? "bg-[var(--ds-accent-primary)] text-[var(--ds-text-on-primary)]" : "text-[var(--ds-text-secondary)] hover:bg-[var(--ds-bg-emphasis)] hover:text-[var(--ds-text-primary)]"
6751
7193
  ),
7194
+ "data-current": isActive || void 0,
6752
7195
  onClick: () => {
7196
+ returnFocus();
6753
7197
  onSelect(new Date(browseYear, i, 1));
6754
7198
  setOpen(false);
6755
7199
  },
@@ -6786,10 +7230,18 @@ function YearDropdown({
6786
7230
  setPrevRangeSyncKey(rangeSyncKey);
6787
7231
  if (open) setRangeStart(currentYear - 4);
6788
7232
  }
7233
+ const { isTop, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: floatingRef, close: () => setOpen(false), tabExits: "edges" });
6789
7234
  useEffect(() => {
6790
7235
  if (!open) return;
7236
+ const raf = requestAnimationFrame(() => {
7237
+ const panel = floatingRef.current;
7238
+ (panel?.querySelector("[data-current]") ?? panel?.querySelector("button"))?.focus();
7239
+ });
6791
7240
  const onKey = (e) => {
6792
- if (e.key === "Escape") setOpen(false);
7241
+ if (e.key !== "Escape" || e.defaultPrevented || !isTop()) return;
7242
+ e.preventDefault();
7243
+ returnFocus();
7244
+ setOpen(false);
6793
7245
  };
6794
7246
  const onClick = (e) => {
6795
7247
  const t = e.target;
@@ -6799,10 +7251,11 @@ function YearDropdown({
6799
7251
  document.addEventListener("keydown", onKey);
6800
7252
  document.addEventListener("mousedown", onClick);
6801
7253
  return () => {
7254
+ cancelAnimationFrame(raf);
6802
7255
  document.removeEventListener("keydown", onKey);
6803
7256
  document.removeEventListener("mousedown", onClick);
6804
7257
  };
6805
- }, [open]);
7258
+ }, [open, isTop, returnFocus]);
6806
7259
  const years = Array.from({ length: 9 }, (_, i) => rangeStart + i);
6807
7260
  return /* @__PURE__ */ jsxs(Fragment, { children: [
6808
7261
  /* @__PURE__ */ jsxs(
@@ -6871,7 +7324,9 @@ function YearDropdown({
6871
7324
  "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
6872
7325
  y === currentYear ? "bg-[var(--ds-accent-primary)] text-[var(--ds-text-on-primary)]" : "text-[var(--ds-text-secondary)] hover:bg-[var(--ds-bg-emphasis)] hover:text-[var(--ds-text-primary)]"
6873
7326
  ),
7327
+ "data-current": y === currentYear || void 0,
6874
7328
  onClick: () => {
7329
+ returnFocus();
6875
7330
  onSelect(new Date(y, currentDate.getMonth(), 1));
6876
7331
  setOpen(false);
6877
7332
  },
@@ -6947,7 +7402,7 @@ function Calendar({
6947
7402
  if (currentView === "month") {
6948
7403
  setCurrentDate(addMonths(currentDate, direction));
6949
7404
  } else if (currentView === "week") {
6950
- setCurrentDate(addDays(currentDate, direction * 7));
7405
+ setCurrentDate(addDays2(currentDate, direction * 7));
6951
7406
  } else {
6952
7407
  setCurrentDate(new Date(currentDate.getFullYear() + direction, currentDate.getMonth(), 1));
6953
7408
  }
@@ -9600,8 +10055,11 @@ var Breadcrumbs = forwardRef(function Breadcrumbs2({
9600
10055
  container
9601
10056
  }, ref) {
9602
10057
  const [dropdownOpen, setDropdownOpen] = useState(false);
10058
+ const [activeIdx, setActiveIdx] = useState(0);
9603
10059
  const ellipsisBtnRef = useRef(null);
9604
10060
  const dropdownRef = useRef(null);
10061
+ const closeDropdown = useCallback(() => setDropdownOpen(false), []);
10062
+ const { isTop, returnFocus } = usePopupFocus(dropdownOpen, { trigger: ellipsisBtnRef, panel: dropdownRef, close: closeDropdown, tabExits: "any" });
9605
10063
  const pos = usePosition(ellipsisBtnRef, dropdownRef, dropdownOpen, { side: "bottom", align: "start", offset: 4, flip: true });
9606
10064
  const effectiveMaxItems = maxItems ? Math.max(maxItems, 2) : void 0;
9607
10065
  const shouldCollapse = effectiveMaxItems && items.length > effectiveMaxItems;
@@ -9614,9 +10072,31 @@ var Breadcrumbs = forwardRef(function Breadcrumbs2({
9614
10072
  if (ellipsisBtnRef.current?.contains(target) || dropdownRef.current?.contains(target)) return;
9615
10073
  setDropdownOpen(false);
9616
10074
  };
10075
+ const onKeyDown = (e) => {
10076
+ if (e.key !== "Escape" || e.defaultPrevented || !isTop()) return;
10077
+ e.preventDefault();
10078
+ returnFocus();
10079
+ setDropdownOpen(false);
10080
+ };
9617
10081
  document.addEventListener("mousedown", handler);
9618
- return () => document.removeEventListener("mousedown", handler);
9619
- }, [dropdownOpen]);
10082
+ document.addEventListener("keydown", onKeyDown);
10083
+ return () => {
10084
+ document.removeEventListener("mousedown", handler);
10085
+ document.removeEventListener("keydown", onKeyDown);
10086
+ };
10087
+ }, [dropdownOpen, isTop, returnFocus]);
10088
+ useEffect(() => {
10089
+ if (!dropdownOpen) return;
10090
+ const id = requestAnimationFrame(() => dropdownRef.current?.querySelector(`[data-index="${activeIdx}"]`)?.focus());
10091
+ return () => cancelAnimationFrame(id);
10092
+ }, [dropdownOpen, activeIdx]);
10093
+ function onMenuKeyDown(e) {
10094
+ const last = collapsedItems.length - 1;
10095
+ 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;
10096
+ if (next === null) return;
10097
+ e.preventDefault();
10098
+ setActiveIdx(next);
10099
+ }
9620
10100
  const textClass = size === "sm" ? "text-xs" : "text-sm";
9621
10101
  const gapClass = size === "sm" ? "gap-ds-1" : "gap-ds-1.5";
9622
10102
  const iconSize = size === "sm" ? 12 : 14;
@@ -9681,7 +10161,10 @@ var Breadcrumbs = forwardRef(function Breadcrumbs2({
9681
10161
  "button",
9682
10162
  {
9683
10163
  ref: ellipsisBtnRef,
9684
- onClick: () => setDropdownOpen((v) => !v),
10164
+ onClick: () => {
10165
+ if (!dropdownOpen) setActiveIdx(0);
10166
+ setDropdownOpen(!dropdownOpen);
10167
+ },
9685
10168
  "aria-label": "Show hidden breadcrumbs",
9686
10169
  "aria-expanded": dropdownOpen,
9687
10170
  "aria-haspopup": "menu",
@@ -9712,6 +10195,8 @@ var Breadcrumbs = forwardRef(function Breadcrumbs2({
9712
10195
  {
9713
10196
  ref: dropdownRef,
9714
10197
  role: "menu",
10198
+ "aria-label": "Hidden breadcrumbs",
10199
+ onKeyDown: onMenuKeyDown,
9715
10200
  className: cn(
9716
10201
  "fixed z-[var(--ds-z-popover)] min-w-[160px] max-h-60 overflow-auto",
9717
10202
  "bg-[var(--ds-bg-surface-elevated)]",
@@ -9725,12 +10210,15 @@ var Breadcrumbs = forwardRef(function Breadcrumbs2({
9725
10210
  children: collapsedItems.map((ci, idx) => /* @__PURE__ */ jsxs(
9726
10211
  "button",
9727
10212
  {
10213
+ "data-index": idx,
9728
10214
  role: "menuitem",
10215
+ tabIndex: idx === activeIdx ? 0 : -1,
9729
10216
  className: cn(
9730
10217
  "flex items-center w-full text-left text-sm cursor-pointer",
9731
10218
  "px-ds-3 py-ds-1.5 transition-colors",
9732
10219
  "text-[var(--ds-text-secondary)]",
9733
10220
  "hover:bg-[var(--ds-bg-muted)] hover:text-[var(--ds-text-primary)]",
10221
+ "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)]",
9734
10222
  gapClass
9735
10223
  ),
9736
10224
  onClick: () => {
@@ -9788,28 +10276,31 @@ function Popover({ open, onOpenChange, trigger, children, side = "bottom", align
9788
10276
  setTriggerWidth(node?.offsetWidth);
9789
10277
  }, []);
9790
10278
  const pos = usePosition(triggerRef, floatingRef, open, { side, align, offset: 8, flip: true });
9791
- const isTopLayer = useOverlayLayer(open);
9792
- useCloseOnFocusOut(open, triggerRef, floatingRef, () => onOpenChange(false), isTopLayer);
10279
+ const onOpenChangeRef = useLatest(onOpenChange);
10280
+ const { props: pressMarks, marked } = usePressMarks();
10281
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: floatingRef, close: () => onOpenChangeRef.current(false), tabExits: "edges", marked });
9793
10282
  useEffect(() => {
9794
10283
  if (!open) return;
9795
10284
  const onKeyDown = (e) => {
9796
- if (e.key === "Escape" && isTopLayer()) {
9797
- onOpenChange(false);
9798
- triggerRef.current?.focus();
9799
- }
10285
+ if (e.key !== "Escape" || e.defaultPrevented || !isTopLayer()) return;
10286
+ const active = document.activeElement;
10287
+ if (active && active !== document.body && !triggerRef.current?.contains(active) && !floatingRef.current?.contains(active)) return;
10288
+ e.preventDefault();
10289
+ returnFocus();
10290
+ onOpenChangeRef.current(false);
9800
10291
  };
9801
- const onClick = (e) => {
10292
+ const onPress = (e) => {
9802
10293
  const target = e.target;
9803
- if (triggerRef.current?.contains(target) || floatingRef.current?.contains(target)) return;
9804
- onOpenChange(false);
10294
+ if (marked(e) || triggerRef.current?.contains(target) || floatingRef.current?.contains(target)) return;
10295
+ onOpenChangeRef.current(false);
9805
10296
  };
9806
10297
  document.addEventListener("keydown", onKeyDown);
9807
- document.addEventListener("mousedown", onClick);
10298
+ document.addEventListener("mousedown", onPress);
9808
10299
  return () => {
9809
10300
  document.removeEventListener("keydown", onKeyDown);
9810
- document.removeEventListener("mousedown", onClick);
10301
+ document.removeEventListener("mousedown", onPress);
9811
10302
  };
9812
- }, [open, onOpenChange, isTopLayer]);
10303
+ }, [open, onOpenChangeRef, isTopLayer, returnFocus, marked]);
9813
10304
  useEffect(() => {
9814
10305
  if (open) focusInitial(floatingRef.current);
9815
10306
  }, [open]);
@@ -9831,6 +10322,8 @@ function Popover({ open, onOpenChange, trigger, children, side = "bottom", align
9831
10322
  {
9832
10323
  ref: floatingRef,
9833
10324
  "data-gs": "popover",
10325
+ ...pressMarks,
10326
+ onClick: (e) => e.stopPropagation(),
9834
10327
  className: cn(
9835
10328
  "fixed z-[var(--ds-z-popover)]",
9836
10329
  "bg-[var(--ds-bg-surface-elevated)]",
@@ -10045,10 +10538,87 @@ function EllipsisJumper({ totalPages, onPageChange }) {
10045
10538
  }
10046
10539
  );
10047
10540
  }
10541
+ var HeaderTabRowContext = createContext(null);
10542
+ function useHeaderTabRow() {
10543
+ return useContext(HeaderTabRowContext);
10544
+ }
10545
+ function useNarrowRow(row) {
10546
+ const [narrow, setNarrow] = useState(false);
10547
+ useLayoutEffect(() => {
10548
+ if (!row) return;
10549
+ const limit = readBreakpointPx("sm");
10550
+ const measure = () => setNarrow(row.clientWidth < limit);
10551
+ measure();
10552
+ if (typeof ResizeObserver === "undefined") return;
10553
+ const ro = new ResizeObserver(measure);
10554
+ ro.observe(row);
10555
+ return () => ro.disconnect();
10556
+ }, [row]);
10557
+ return narrow;
10558
+ }
10048
10559
  function Stepper({ steps, currentStep, onStepChange, orientation = "horizontal", className }) {
10049
10560
  const isHorizontal = orientation === "horizontal";
10561
+ const tabRow = useHeaderTabRow();
10562
+ const narrow = useNarrowRow(tabRow?.row ?? null);
10563
+ const inHeader = isHorizontal && tabRow != null;
10564
+ if (inHeader && narrow) {
10565
+ const current = steps[currentStep];
10566
+ const where = `Step ${Math.min(currentStep + 1, steps.length)} of ${steps.length}`;
10567
+ return /* @__PURE__ */ jsxs("nav", { "data-gs": "stepper", "aria-label": "Progress", className: cn("mx-ds-1", className), children: [
10568
+ /* @__PURE__ */ jsxs("p", { className: cn("mb-ds-2 text-sm font-medium text-[var(--ds-text-secondary)]", tabRow.hasCaption && "sr-only"), children: [
10569
+ where,
10570
+ current ? /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
10571
+ ": ",
10572
+ current.label
10573
+ ] }) : null
10574
+ ] }),
10575
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "flex gap-ds-1", children: steps.map((step, i) => /* @__PURE__ */ jsx(
10576
+ "div",
10577
+ {
10578
+ className: cn(
10579
+ "h-1.5 flex-1 rounded-[var(--ds-radius-full)] transition-colors",
10580
+ step.status === "error" ? "bg-[var(--ds-status-error)]" : i <= currentStep ? "bg-[var(--ds-accent-primary)]" : "bg-[var(--ds-border-subtle)]"
10581
+ )
10582
+ },
10583
+ i
10584
+ )) })
10585
+ ] });
10586
+ }
10587
+ if (inHeader) {
10588
+ return /* @__PURE__ */ jsx("nav", { "data-gs": "stepper", "aria-label": "Progress", className: cn("flex items-start mx-ds-1", className), children: steps.map((step, i) => {
10589
+ const status = step.status ?? (i < currentStep ? "complete" : i === currentStep ? "current" : "upcoming");
10590
+ const isLast = i === steps.length - 1;
10591
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col items-start min-w-0", isLast ? "flex-none" : "flex-1"), children: [
10592
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center w-full", children: [
10593
+ /* @__PURE__ */ jsx(StepCircle, { status, index: i, onClick: onStepChange ? () => onStepChange(i) : void 0, clickable: !!onStepChange }),
10594
+ !isLast && /* @__PURE__ */ jsx(
10595
+ "div",
10596
+ {
10597
+ className: cn(
10598
+ "flex-1 h-0.5 mx-ds-2 transition-colors",
10599
+ i < currentStep ? "bg-[var(--ds-accent-primary)]" : "bg-[var(--ds-border-subtle)]"
10600
+ )
10601
+ }
10602
+ )
10603
+ ] }),
10604
+ /* @__PURE__ */ jsxs("div", { className: "mt-ds-2 pr-ds-2", children: [
10605
+ /* @__PURE__ */ jsx(
10606
+ "p",
10607
+ {
10608
+ className: cn(
10609
+ "text-xs font-medium",
10610
+ status === "current" ? "text-[var(--ds-accent-primary)]" : "text-[var(--ds-text-primary)]"
10611
+ ),
10612
+ children: step.label
10613
+ }
10614
+ ),
10615
+ step.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--ds-text-muted)] mt-ds-0.5", children: step.description })
10616
+ ] })
10617
+ ] }, i);
10618
+ }) });
10619
+ }
10050
10620
  if (!isHorizontal) {
10051
- return /* @__PURE__ */ jsx("nav", { "aria-label": "Progress", className: cn("flex flex-col", className), children: steps.map((step, i) => {
10621
+ return /* @__PURE__ */ jsx("nav", { "data-gs": "stepper", "aria-label": "Progress", className: cn("flex flex-col", className), children: steps.map((step, i) => {
10052
10622
  const status = step.status ?? (i < currentStep ? "complete" : i === currentStep ? "current" : "upcoming");
10053
10623
  const isLast = i === steps.length - 1;
10054
10624
  return /* @__PURE__ */ jsxs("div", { className: "flex gap-ds-3", children: [
@@ -10083,6 +10653,7 @@ function Stepper({ steps, currentStep, onStepChange, orientation = "horizontal",
10083
10653
  return /* @__PURE__ */ jsx(
10084
10654
  "nav",
10085
10655
  {
10656
+ "data-gs": "stepper",
10086
10657
  "aria-label": "Progress",
10087
10658
  className: cn("flex items-start", className),
10088
10659
  children: steps.map((step, i) => {
@@ -10503,6 +11074,7 @@ function computeCompactFlyoutPos(anchorEl, itemsLength) {
10503
11074
  return { top: Math.max(8, top), left: rect.right - (FLYOUT_BRIDGE_PX - 4) };
10504
11075
  }
10505
11076
  function CompactFlyout({
11077
+ id,
10506
11078
  parentLabel,
10507
11079
  items,
10508
11080
  anchorEl,
@@ -10510,17 +11082,40 @@ function CompactFlyout({
10510
11082
  onItemClick,
10511
11083
  onClose,
10512
11084
  onMouseEnter,
10513
- onMouseLeave
11085
+ onMouseLeave,
11086
+ focusIn = false
10514
11087
  }) {
10515
11088
  const pos = useMemo(() => computeCompactFlyoutPos(anchorEl, items.length), [anchorEl, items.length]);
10516
- const isTopLayer = useOverlayLayer(true);
11089
+ const anchorRef = useRef(anchorEl);
11090
+ const panelRef = useRef(null);
11091
+ const headingId = useId();
11092
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(true, { trigger: anchorRef, panel: panelRef, close: onClose, tabExits: "edges" });
11093
+ useEffect(() => {
11094
+ if (!focusIn) return;
11095
+ const raf = requestAnimationFrame(() => panelRef.current?.querySelector("a[href]")?.focus());
11096
+ return () => cancelAnimationFrame(raf);
11097
+ }, [focusIn]);
10517
11098
  useEffect(() => {
10518
11099
  function handler(e) {
10519
- if (e.key === "Escape" && isTopLayer()) onClose();
11100
+ if (e.defaultPrevented || !isTopLayer()) return;
11101
+ const inside = !!panelRef.current?.contains(document.activeElement);
11102
+ if (e.key === "Escape" || e.key === "ArrowLeft" && inside) {
11103
+ e.preventDefault();
11104
+ if (inside) returnFocus();
11105
+ onClose();
11106
+ } else if (inside && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
11107
+ const links = Array.from(panelRef.current?.querySelectorAll("a[href]") ?? []);
11108
+ const at = links.indexOf(document.activeElement);
11109
+ const next = links[(at + (e.key === "ArrowDown" ? 1 : -1) + links.length) % links.length];
11110
+ if (next) {
11111
+ e.preventDefault();
11112
+ next.focus();
11113
+ }
11114
+ }
10520
11115
  }
10521
11116
  document.addEventListener("keydown", handler);
10522
11117
  return () => document.removeEventListener("keydown", handler);
10523
- }, [onClose, isTopLayer]);
11118
+ }, [onClose, isTopLayer, returnFocus]);
10524
11119
  const content = /* @__PURE__ */ jsx(
10525
11120
  "div",
10526
11121
  {
@@ -10531,7 +11126,8 @@ function CompactFlyout({
10531
11126
  children: /* @__PURE__ */ jsxs(
10532
11127
  "div",
10533
11128
  {
10534
- role: "menu",
11129
+ ref: panelRef,
11130
+ id,
10535
11131
  className: cn(
10536
11132
  "min-w-[160px] py-ds-1",
10537
11133
  "bg-[var(--ds-sidebar-bg)] rounded-[var(--ds-radius-md)]",
@@ -10539,12 +11135,11 @@ function CompactFlyout({
10539
11135
  "border-[length:var(--ds-border-width)] border-[var(--ds-border-default)]"
10540
11136
  ),
10541
11137
  children: [
10542
- /* @__PURE__ */ jsx("p", { className: "px-ds-2 pt-ds-1 pb-ds-1.5 text-2xs font-semibold uppercase tracking-wider text-[var(--ds-sidebar-text-muted)]", children: parentLabel }),
10543
- /* @__PURE__ */ jsx("div", { className: "flex flex-col px-ds-1", style: { gap: "var(--ds-space-0\\.5)" }, children: items.map((child, i) => /* @__PURE__ */ jsxs(
11138
+ /* @__PURE__ */ jsx("p", { id: headingId, className: "px-ds-2 pt-ds-1 pb-ds-1.5 text-2xs font-semibold uppercase tracking-wider text-[var(--ds-sidebar-text-muted)]", children: parentLabel }),
11139
+ /* @__PURE__ */ jsx("ul", { "aria-labelledby": headingId, className: "flex flex-col px-ds-1", style: { gap: "var(--ds-space-0\\.5)" }, children: items.map((child, i) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
10544
11140
  "a",
10545
11141
  {
10546
11142
  href: child.href ?? "#",
10547
- role: "menuitem",
10548
11143
  onClick: (e) => {
10549
11144
  e.preventDefault();
10550
11145
  onItemClick?.(child, i);
@@ -10559,9 +11154,8 @@ function CompactFlyout({
10559
11154
  /* @__PURE__ */ jsx(TruncLabel3, { children: child.label }),
10560
11155
  child.badge && /* @__PURE__ */ jsx("span", { className: "shrink-0", children: child.badge })
10561
11156
  ]
10562
- },
10563
- child.label
10564
- )) })
11157
+ }
11158
+ ) }, child.label)) })
10565
11159
  ]
10566
11160
  }
10567
11161
  )
@@ -10908,6 +11502,9 @@ function CompactNavItem({
10908
11502
  onToggleExpand
10909
11503
  }) {
10910
11504
  const [triggerEl, setTriggerEl] = useState(null);
11505
+ const flyoutId = useId();
11506
+ const [keyboardOpen, setKeyboardOpen] = useState(false);
11507
+ if (!isOpen && keyboardOpen) setKeyboardOpen(false);
10911
11508
  const handleMouseEnter = useCallback(() => {
10912
11509
  onCancelClose();
10913
11510
  if (hasChildren && !isOpen) onOpen(flyoutKey);
@@ -10930,7 +11527,15 @@ function CompactNavItem({
10930
11527
  onMouseEnter: !mobile && hasChildren ? handleMouseEnter : void 0,
10931
11528
  onMouseLeave: !mobile && hasChildren && isOpen ? handleMouseLeave : void 0,
10932
11529
  onClick: handleClick,
11530
+ onKeyDown: !mobile && hasChildren ? (e) => {
11531
+ if (e.key !== "ArrowRight") return;
11532
+ e.preventDefault();
11533
+ onCancelClose();
11534
+ setKeyboardOpen(true);
11535
+ onOpen(flyoutKey);
11536
+ } : void 0,
10933
11537
  "aria-expanded": hasChildren ? mobile ? expanded : isOpen : void 0,
11538
+ "aria-controls": !mobile && hasChildren && isOpen ? flyoutId : void 0,
10934
11539
  className: cn(
10935
11540
  "relative flex flex-col items-center justify-center gap-[3px] w-full py-ds-3 px-ds-1 rounded-[var(--ds-radius-md)] transition-colors cursor-pointer",
10936
11541
  // Compact tiles have no edge to stroke: they read the fill and text
@@ -10965,6 +11570,7 @@ function CompactNavItem({
10965
11570
  !mobile && isOpen && hasChildren && item.children && triggerEl && /* @__PURE__ */ jsx(
10966
11571
  CompactFlyout,
10967
11572
  {
11573
+ id: flyoutId,
10968
11574
  parentLabel: item.label,
10969
11575
  items: item.children,
10970
11576
  anchorEl: triggerEl,
@@ -10972,7 +11578,8 @@ function CompactNavItem({
10972
11578
  onItemClick,
10973
11579
  onClose,
10974
11580
  onMouseEnter: onCancelClose,
10975
- onMouseLeave: handleMouseLeave
11581
+ onMouseLeave: handleMouseLeave,
11582
+ focusIn: keyboardOpen
10976
11583
  }
10977
11584
  ),
10978
11585
  mobile && expanded && hasChildren && item.children && /* @__PURE__ */ jsx("div", { className: "w-full flex flex-col gap-ds-1 pb-ds-1", children: item.children.map((child, ci) => /* @__PURE__ */ jsxs(
@@ -11439,6 +12046,35 @@ function BackButton({ onLight, onClick }) {
11439
12046
  }
11440
12047
  );
11441
12048
  }
12049
+ function BackLink({ onClick, onLight }) {
12050
+ return /* @__PURE__ */ jsxs(
12051
+ "button",
12052
+ {
12053
+ type: "button",
12054
+ onClick,
12055
+ className: cn(
12056
+ "inline-flex items-center gap-ds-1 mb-ds-3 text-sm underline underline-offset-4 cursor-pointer",
12057
+ "rounded-[var(--ds-radius-sm)] focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
12058
+ onLight ? "text-[var(--ds-text-on-primary)]" : "text-[var(--ds-text-primary)] hover:text-[var(--ds-accent-primary)]"
12059
+ ),
12060
+ children: [
12061
+ /* @__PURE__ */ jsx(Icon, { name: "chevron-left", size: 14 }),
12062
+ "Back"
12063
+ ]
12064
+ }
12065
+ );
12066
+ }
12067
+ function Caption({ children, onLight, onTint }) {
12068
+ return /* @__PURE__ */ jsx("p", { className: cn(
12069
+ "mb-ds-1 text-sm font-medium",
12070
+ onLight ? "text-[var(--ds-text-on-primary)]/80" : onTint ? "text-[var(--ds-text-muted-on-tint)]" : "text-[var(--ds-text-secondary)]"
12071
+ ), children });
12072
+ }
12073
+ function TabRow({ children, hasCaption, className, onBand }) {
12074
+ const [row, setRow] = useState(null);
12075
+ const value = useMemo(() => ({ row, hasCaption }), [row, hasCaption]);
12076
+ return /* @__PURE__ */ jsx("div", { ref: setRow, className: cn(className, onBand && "mx-0 rounded-[var(--ds-radius-md)] bg-[var(--ds-bg-surface)] px-ds-3 py-ds-2"), children: /* @__PURE__ */ jsx(HeaderTabRowContext.Provider, { value, children }) });
12077
+ }
11442
12078
  var FLOATING_HEADER = cn(
11443
12079
  "rounded-[var(--ds-radius-md)]",
11444
12080
  "border-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
@@ -11456,9 +12092,10 @@ function computeFloatingPadding(navbarFloating, sidebarFloating, overlapActive,
11456
12092
  var PageHeader = forwardRef(function PageHeader2({
11457
12093
  title,
11458
12094
  description,
12095
+ caption,
11459
12096
  badge,
11460
12097
  breadcrumbs,
11461
- onBack,
12098
+ onBack: onBackProp,
11462
12099
  actions,
11463
12100
  avatar,
11464
12101
  filters,
@@ -11517,12 +12154,16 @@ var PageHeader = forwardRef(function PageHeader2({
11517
12154
  const containedBreadcrumbs = containedBreadcrumbsProp ?? (container?.breadcrumbsInsideHeader && breadcrumbs?.length ? /* @__PURE__ */ jsx(Breadcrumbs, { variant: "contained", itemVariant: r.breadcrumbStyle, bleed: true, contentMaxWidth: gutterWidth, separator: bcSep, items: [...breadcrumbs, { label: title }] }) : void 0);
11518
12155
  const noBottomMargin = noBottomMarginProp ?? container?.noBottomMargin;
11519
12156
  const overlapActive = container?.overlap ?? false;
12157
+ const focusBack = shell?.focus ? onBackProp : void 0;
12158
+ const onBack = focusBack ? void 0 : onBackProp;
11520
12159
  if (behavior === "snapping") {
11521
12160
  return /* @__PURE__ */ jsx(
11522
12161
  SnappingHeader,
11523
12162
  {
11524
12163
  title,
11525
12164
  description,
12165
+ caption,
12166
+ focusBack,
11526
12167
  badge,
11527
12168
  breadcrumbs: inlineBreadcrumbs,
11528
12169
  onBack,
@@ -11554,13 +12195,15 @@ var PageHeader = forwardRef(function PageHeader2({
11554
12195
  }
11555
12196
  let header;
11556
12197
  if (variant === "minimal") {
11557
- header = /* @__PURE__ */ jsx(MinimalHeader, { title, badge, breadcrumbs: inlineBreadcrumbs, actions, breadcrumbPill: bcPill, breadcrumbSeparator: bcSep, as: Heading, className, headerRef: floating ? void 0 : ref, isFloating: floating, contentMaxWidth: gutterWidth });
12198
+ header = /* @__PURE__ */ jsx(MinimalHeader, { title, caption, focusBack, badge, breadcrumbs: inlineBreadcrumbs, actions, tabs, breadcrumbPill: bcPill, breadcrumbSeparator: bcSep, as: Heading, className, headerRef: floating ? void 0 : ref, isFloating: floating, contentMaxWidth: gutterWidth });
11558
12199
  } else if (variant === "tabs-integrated") {
11559
12200
  header = /* @__PURE__ */ jsx(
11560
12201
  TabsIntegratedHeader,
11561
12202
  {
11562
12203
  title,
11563
12204
  description,
12205
+ caption,
12206
+ focusBack,
11564
12207
  badge,
11565
12208
  breadcrumbs: inlineBreadcrumbs,
11566
12209
  onBack,
@@ -11583,6 +12226,9 @@ var PageHeader = forwardRef(function PageHeader2({
11583
12226
  gutter: !insidePaddedColumn,
11584
12227
  title,
11585
12228
  description,
12229
+ caption,
12230
+ focusBack,
12231
+ tabs,
11586
12232
  badge,
11587
12233
  breadcrumbs: inlineBreadcrumbs,
11588
12234
  onBack,
@@ -11608,6 +12254,9 @@ var PageHeader = forwardRef(function PageHeader2({
11608
12254
  {
11609
12255
  title,
11610
12256
  description,
12257
+ caption,
12258
+ focusBack,
12259
+ tabs,
11611
12260
  badge,
11612
12261
  breadcrumbs: inlineBreadcrumbs,
11613
12262
  onBack,
@@ -11648,6 +12297,9 @@ function floatingContentProps(isFloating, contentMaxWidth, extraClassName) {
11648
12297
  function DefaultHeader({
11649
12298
  title,
11650
12299
  description,
12300
+ caption,
12301
+ focusBack,
12302
+ tabs,
11651
12303
  badge,
11652
12304
  breadcrumbs,
11653
12305
  onBack,
@@ -11670,6 +12322,7 @@ function DefaultHeader({
11670
12322
  return /* @__PURE__ */ jsxs(
11671
12323
  "header",
11672
12324
  {
12325
+ "data-gs": "page-header",
11673
12326
  ref: headerRef,
11674
12327
  className: cn(
11675
12328
  !hasBanner && (hasBreadcrumbs ? "pt-ds-3 pb-ds-6" : "py-ds-6"),
@@ -11688,6 +12341,7 @@ function DefaultHeader({
11688
12341
  children: [
11689
12342
  hasBanner && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-[var(--ds-bg-canvas)]/85" }),
11690
12343
  /* @__PURE__ */ jsxs("div", { className: inner.className, style: inner.style, children: [
12344
+ focusBack && /* @__PURE__ */ jsx(BackLink, { onClick: focusBack }),
11691
12345
  hasBreadcrumbs && /* @__PURE__ */ jsx(Breadcrumbs3, { items: breadcrumbs, pill: breadcrumbPill, separator: breadcrumbSeparator }),
11692
12346
  /* @__PURE__ */ jsxs("div", { className: cn(
11693
12347
  "flex flex-col @sm:flex-row @sm:items-center justify-between gap-ds-4",
@@ -11697,6 +12351,7 @@ function DefaultHeader({
11697
12351
  onBack && /* @__PURE__ */ jsx(BackButton, { onClick: onBack }),
11698
12352
  avatar && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: avatar }),
11699
12353
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
12354
+ caption && /* @__PURE__ */ jsx(Caption, { children: caption }),
11700
12355
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2", children: [
11701
12356
  /* @__PURE__ */ jsx(TruncHeading, { as: Heading, className: "font-[family-name:var(--ds-font-display)] text-3xl font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)]", children: title }),
11702
12357
  badge
@@ -11706,7 +12361,8 @@ function DefaultHeader({
11706
12361
  ] }),
11707
12362
  actions && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-ds-2 shrink-0", children: actions })
11708
12363
  ] }),
11709
- filters && /* @__PURE__ */ jsx(ControlSizeContext.Provider, { value: "sm", children: /* @__PURE__ */ jsx("div", { className: "flex items-center flex-wrap gap-ds-3 mt-ds-4", children: filters }) })
12364
+ filters && /* @__PURE__ */ jsx(ControlSizeContext.Provider, { value: "sm", children: /* @__PURE__ */ jsx("div", { className: "flex items-center flex-wrap gap-ds-3 mt-ds-4", children: filters }) }),
12365
+ tabs && /* @__PURE__ */ jsx(TabRow, { hasCaption: !!caption, className: "mt-ds-4 -mx-ds-1", children: tabs })
11710
12366
  ] })
11711
12367
  ]
11712
12368
  }
@@ -11714,9 +12370,12 @@ function DefaultHeader({
11714
12370
  }
11715
12371
  function MinimalHeader({
11716
12372
  title,
12373
+ caption,
12374
+ focusBack,
11717
12375
  badge,
11718
12376
  breadcrumbs,
11719
12377
  actions,
12378
+ tabs,
11720
12379
  breadcrumbPill,
11721
12380
  breadcrumbSeparator,
11722
12381
  as: Heading = "h1",
@@ -11727,20 +12386,26 @@ function MinimalHeader({
11727
12386
  }) {
11728
12387
  const hasBreadcrumbs = !!(breadcrumbs && breadcrumbs.length > 0);
11729
12388
  const inner = floatingContentProps(isFloating, contentMaxWidth);
11730
- return /* @__PURE__ */ jsx("header", { ref: headerRef, className: cn("py-ds-2", hasBreadcrumbs && "pt-ds-1", isFloating && FLOATING_HEADER, className), children: /* @__PURE__ */ jsxs("div", { className: inner.className, style: inner.style, children: [
12389
+ return /* @__PURE__ */ jsx("header", { "data-gs": "page-header", ref: headerRef, className: cn("py-ds-2", hasBreadcrumbs && "pt-ds-1", tabs && "pb-ds-4", isFloating && FLOATING_HEADER, className), children: /* @__PURE__ */ jsxs("div", { className: inner.className, style: inner.style, children: [
12390
+ focusBack && /* @__PURE__ */ jsx(BackLink, { onClick: focusBack }),
11731
12391
  hasBreadcrumbs && /* @__PURE__ */ jsx(Breadcrumbs3, { items: breadcrumbs, pill: breadcrumbPill, separator: breadcrumbSeparator }),
12392
+ caption && /* @__PURE__ */ jsx(Caption, { children: caption }),
11732
12393
  /* @__PURE__ */ jsxs("div", { className: cn("flex items-center justify-between gap-ds-4", hasBreadcrumbs && "mt-ds-0.5"), children: [
11733
12394
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2 min-w-0", children: [
11734
12395
  /* @__PURE__ */ jsx(TruncHeading, { as: Heading, lines: 1, className: "font-[family-name:var(--ds-font-display)] text-xl font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)]", children: title }),
11735
12396
  badge
11736
12397
  ] }),
11737
12398
  actions && /* @__PURE__ */ jsx(ControlSizeContext.Provider, { value: "sm", children: /* @__PURE__ */ jsx("div", { className: "flex items-center gap-ds-2 shrink-0", children: actions }) })
11738
- ] })
12399
+ ] }),
12400
+ tabs && /* @__PURE__ */ jsx(TabRow, { hasCaption: !!caption, className: "mt-ds-3 -mx-ds-1", children: tabs })
11739
12401
  ] }) });
11740
12402
  }
11741
12403
  function ColourfulHeader({
11742
12404
  title,
11743
12405
  description,
12406
+ caption,
12407
+ focusBack,
12408
+ tabs,
11744
12409
  badge,
11745
12410
  breadcrumbs,
11746
12411
  onBack,
@@ -11765,6 +12430,7 @@ function ColourfulHeader({
11765
12430
  return /* @__PURE__ */ jsxs(
11766
12431
  "header",
11767
12432
  {
12433
+ "data-gs": "page-header",
11768
12434
  ref: headerRef,
11769
12435
  className: cn(
11770
12436
  "relative overflow-hidden w-full",
@@ -11808,6 +12474,7 @@ function ColourfulHeader({
11808
12474
  ...overlapActive ? { paddingBottom: `calc(var(--ds-space-6) + var(--ds-overlap, 0px))` } : {}
11809
12475
  },
11810
12476
  children: [
12477
+ focusBack && /* @__PURE__ */ jsx(BackLink, { onClick: focusBack, onLight: isBold }),
11811
12478
  hasBreadcrumbs && /* @__PURE__ */ jsx(Breadcrumbs3, { items: breadcrumbs, onLight: isBold, onTint: !isBold, pill: breadcrumbPill, separator: breadcrumbSeparator }),
11812
12479
  /* @__PURE__ */ jsxs("div", { className: cn(
11813
12480
  "flex flex-col @sm:flex-row @sm:items-center justify-between gap-ds-4",
@@ -11817,6 +12484,7 @@ function ColourfulHeader({
11817
12484
  onBack && /* @__PURE__ */ jsx(BackButton, { onClick: onBack, onLight: isBold }),
11818
12485
  avatar && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: avatar }),
11819
12486
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
12487
+ caption && /* @__PURE__ */ jsx(Caption, { onLight: isBold, onTint: !isBold, children: caption }),
11820
12488
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2", children: [
11821
12489
  /* @__PURE__ */ jsx(TruncHeading, { as: Heading, className: cn(
11822
12490
  "font-[family-name:var(--ds-font-display)] text-3xl font-[var(--ds-weight-heading)]",
@@ -11832,7 +12500,8 @@ function ColourfulHeader({
11832
12500
  ] }),
11833
12501
  actions && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-ds-2 shrink-0", children: actions })
11834
12502
  ] }),
11835
- filters && /* @__PURE__ */ jsx(ControlSizeContext.Provider, { value: "sm", children: /* @__PURE__ */ jsx("div", { className: "flex items-center flex-wrap gap-ds-3 mt-ds-4", children: filters }) })
12503
+ filters && /* @__PURE__ */ jsx(ControlSizeContext.Provider, { value: "sm", children: /* @__PURE__ */ jsx("div", { className: "flex items-center flex-wrap gap-ds-3 mt-ds-4", children: filters }) }),
12504
+ tabs && /* @__PURE__ */ jsx(TabRow, { hasCaption: !!caption, className: "mt-ds-4 -mx-ds-1", onBand: isBold, children: tabs })
11836
12505
  ]
11837
12506
  }
11838
12507
  )
@@ -11843,6 +12512,8 @@ function ColourfulHeader({
11843
12512
  function SnappingHeader({
11844
12513
  title,
11845
12514
  description,
12515
+ caption,
12516
+ focusBack,
11846
12517
  badge,
11847
12518
  breadcrumbs,
11848
12519
  onBack,
@@ -11966,6 +12637,7 @@ function SnappingHeader({
11966
12637
  const headerEl = /* @__PURE__ */ jsxs(
11967
12638
  "header",
11968
12639
  {
12640
+ "data-gs": "page-header",
11969
12641
  ref: !isFloating ? mergedRef : void 0,
11970
12642
  className: cn(
11971
12643
  "transition-all duration-[var(--ds-transition-speed)] ease-[var(--ds-transition-ease)]",
@@ -12002,8 +12674,10 @@ function SnappingHeader({
12002
12674
  onTint && !backgroundImage && /* @__PURE__ */ jsx(TintWash, {}),
12003
12675
  (() => {
12004
12676
  const hasBreadcrumbs = !!(breadcrumbs && breadcrumbs.length > 0);
12005
- const innerContent = isMinimal && !collapsed ? /* @__PURE__ */ jsxs("div", { className: cn("py-ds-2", hasBreadcrumbs && !hideExpandedBreadcrumbs && "pt-ds-1"), children: [
12677
+ const innerContent = isMinimal && !collapsed ? /* @__PURE__ */ jsxs("div", { className: cn("py-ds-2", hasBreadcrumbs && !hideExpandedBreadcrumbs && "pt-ds-1", tabs && "pb-ds-4"), children: [
12678
+ focusBack && /* @__PURE__ */ jsx(BackLink, { onClick: focusBack }),
12006
12679
  hasBreadcrumbs && !hideExpandedBreadcrumbs && /* @__PURE__ */ jsx(Breadcrumbs3, { items: breadcrumbs, pill: breadcrumbPill, separator: breadcrumbSeparator }),
12680
+ caption && /* @__PURE__ */ jsx(Caption, { children: caption }),
12007
12681
  /* @__PURE__ */ jsxs("div", { className: cn("flex items-center justify-between gap-ds-4", hasBreadcrumbs && !hideExpandedBreadcrumbs && "mt-ds-0.5"), children: [
12008
12682
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2 min-w-0", children: [
12009
12683
  /* @__PURE__ */ jsx(TruncHeading, { as: Heading, className: "font-[family-name:var(--ds-font-display)] text-xl font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)]", children: title }),
@@ -12013,7 +12687,7 @@ function SnappingHeader({
12013
12687
  ] }),
12014
12688
  filters && /* @__PURE__ */ jsx(ControlSizeContext.Provider, { value: "sm", children: /* @__PURE__ */ jsx("div", { className: "flex items-center flex-wrap gap-ds-3 mt-ds-3", children: filters }) }),
12015
12689
  expandedContent && /* @__PURE__ */ jsx("div", { className: "mt-ds-3", children: expandedContent }),
12016
- tabs && /* @__PURE__ */ jsx("div", { className: "mt-ds-3 -mx-ds-1", children: tabs })
12690
+ tabs && /* @__PURE__ */ jsx(TabRow, { hasCaption: !!caption, className: "mt-ds-3 -mx-ds-1", children: tabs })
12017
12691
  ] }) : /* @__PURE__ */ jsxs(
12018
12692
  "div",
12019
12693
  {
@@ -12027,6 +12701,7 @@ function SnappingHeader({
12027
12701
  ),
12028
12702
  style: !collapsed && isColourful && overlapActive ? { paddingBottom: `calc(var(--ds-space-6) + var(--ds-overlap, 0px))` } : void 0,
12029
12703
  children: [
12704
+ !collapsed && focusBack && /* @__PURE__ */ jsx(BackLink, { onClick: focusBack, onLight }),
12030
12705
  !collapsed && hasBreadcrumbs && !hideExpandedBreadcrumbs && /* @__PURE__ */ jsx(Breadcrumbs3, { items: breadcrumbs, onLight, onTint, pill: breadcrumbPill, separator: breadcrumbSeparator }),
12031
12706
  /* @__PURE__ */ jsxs("div", { className: cn(
12032
12707
  "flex justify-between gap-ds-4",
@@ -12038,6 +12713,7 @@ function SnappingHeader({
12038
12713
  onBack && !collapsed && /* @__PURE__ */ jsx(BackButton, { onClick: onBack, onLight }),
12039
12714
  avatar && !collapsed && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: avatar }),
12040
12715
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
12716
+ !collapsed && caption && /* @__PURE__ */ jsx(Caption, { onLight, onTint, children: caption }),
12041
12717
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2", children: [
12042
12718
  collapsed && hasBreadcrumbs && (() => {
12043
12719
  const lastCrumb = breadcrumbs[breadcrumbs.length - 1];
@@ -12080,7 +12756,7 @@ function SnappingHeader({
12080
12756
  ] }),
12081
12757
  !collapsed && filters && /* @__PURE__ */ jsx(ControlSizeContext.Provider, { value: "sm", children: /* @__PURE__ */ jsx("div", { className: "flex items-center flex-wrap gap-ds-3 mt-ds-4", children: filters }) }),
12082
12758
  !collapsed && expandedContent && /* @__PURE__ */ jsx("div", { className: "mt-ds-4", children: expandedContent }),
12083
- !collapsed && tabs && /* @__PURE__ */ jsx("div", { className: "mt-ds-4 -mx-ds-1", children: tabs })
12759
+ !collapsed && tabs && /* @__PURE__ */ jsx(TabRow, { hasCaption: !!caption, className: "mt-ds-4 -mx-ds-1", onBand: isBoldColourful, children: tabs })
12084
12760
  ]
12085
12761
  }
12086
12762
  );
@@ -12128,6 +12804,8 @@ function SnappingHeader({
12128
12804
  function TabsIntegratedHeader({
12129
12805
  title,
12130
12806
  description,
12807
+ caption,
12808
+ focusBack,
12131
12809
  badge,
12132
12810
  breadcrumbs,
12133
12811
  onBack,
@@ -12144,7 +12822,8 @@ function TabsIntegratedHeader({
12144
12822
  }) {
12145
12823
  const hasBreadcrumbs = !!(breadcrumbs && breadcrumbs.length > 0);
12146
12824
  const inner = floatingContentProps(isFloating, contentMaxWidth);
12147
- return /* @__PURE__ */ jsx("header", { ref: headerRef, className: cn(hasBreadcrumbs ? "pt-ds-3" : "pt-ds-6", "pb-ds-4", isFloating && cn(FLOATING_HEADER, "overflow-hidden"), className), children: /* @__PURE__ */ jsxs("div", { className: inner.className, style: inner.style, children: [
12825
+ return /* @__PURE__ */ jsx("header", { "data-gs": "page-header", ref: headerRef, className: cn(hasBreadcrumbs ? "pt-ds-3" : "pt-ds-6", "pb-ds-4", isFloating && cn(FLOATING_HEADER, "overflow-hidden"), className), children: /* @__PURE__ */ jsxs("div", { className: inner.className, style: inner.style, children: [
12826
+ focusBack && /* @__PURE__ */ jsx(BackLink, { onClick: focusBack }),
12148
12827
  hasBreadcrumbs && /* @__PURE__ */ jsx(Breadcrumbs3, { items: breadcrumbs, pill: breadcrumbPill, separator: breadcrumbSeparator }),
12149
12828
  /* @__PURE__ */ jsxs("div", { className: cn(
12150
12829
  "flex flex-col @sm:flex-row @sm:items-center justify-between gap-ds-4",
@@ -12154,6 +12833,7 @@ function TabsIntegratedHeader({
12154
12833
  onBack && /* @__PURE__ */ jsx(BackButton, { onClick: onBack }),
12155
12834
  avatar && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: avatar }),
12156
12835
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
12836
+ caption && /* @__PURE__ */ jsx(Caption, { children: caption }),
12157
12837
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2", children: [
12158
12838
  /* @__PURE__ */ jsx(TruncHeading, { as: Heading, className: "font-[family-name:var(--ds-font-display)] text-3xl font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)]", children: title }),
12159
12839
  badge
@@ -12163,7 +12843,7 @@ function TabsIntegratedHeader({
12163
12843
  ] }),
12164
12844
  actions && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-ds-2 shrink-0", children: actions })
12165
12845
  ] }),
12166
- tabs && /* @__PURE__ */ jsx("div", { className: "mt-ds-4 -mx-ds-1", children: tabs })
12846
+ tabs && /* @__PURE__ */ jsx(TabRow, { hasCaption: !!caption, className: "mt-ds-4 -mx-ds-1", children: tabs })
12167
12847
  ] }) });
12168
12848
  }
12169
12849
  var DEFAULT_BASE = 48;
@@ -12326,12 +13006,12 @@ function NavMenu({
12326
13006
  className
12327
13007
  }) {
12328
13008
  const [open, setOpen] = useState(false);
12329
- const isTopLayer = useOverlayLayer(open);
12330
13009
  const triggerRef = useRef(null);
12331
13010
  const panelRef = useRef(null);
13011
+ const panelId = useId();
12332
13012
  const [pos, setPos] = useState({ top: 0, left: 0, minWidth: 0 });
12333
13013
  const close = useCallback(() => setOpen(false), []);
12334
- useCloseOnFocusOut(open, triggerRef, panelRef, close, isTopLayer);
13014
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: panelRef, close, tabExits: "edges" });
12335
13015
  useEffect(() => {
12336
13016
  if (!open) return;
12337
13017
  const id = requestAnimationFrame(() => focusInitial(panelRef.current));
@@ -12354,10 +13034,10 @@ function NavMenu({
12354
13034
  }
12355
13035
  }
12356
13036
  function handleKey(e) {
12357
- if (e.key === "Escape" && isTopLayer()) {
12358
- close();
12359
- triggerRef.current?.focus();
12360
- }
13037
+ if (e.key !== "Escape" || e.defaultPrevented || !isTopLayer()) return;
13038
+ e.preventDefault();
13039
+ returnFocus();
13040
+ close();
12361
13041
  }
12362
13042
  document.addEventListener("mousedown", handleClick);
12363
13043
  document.addEventListener("keydown", handleKey);
@@ -12365,7 +13045,7 @@ function NavMenu({
12365
13045
  document.removeEventListener("mousedown", handleClick);
12366
13046
  document.removeEventListener("keydown", handleKey);
12367
13047
  };
12368
- }, [open, close, isTopLayer]);
13048
+ }, [open, close, isTopLayer, returnFocus]);
12369
13049
  const hasActiveChild = active || items.some(
12370
13050
  (it) => it.active || it.children?.some((c) => c.active)
12371
13051
  );
@@ -12399,7 +13079,7 @@ function NavMenu({
12399
13079
  ref: triggerRef,
12400
13080
  onClick: () => setOpen(!open),
12401
13081
  "aria-expanded": open,
12402
- "aria-haspopup": "true",
13082
+ "aria-controls": open ? panelId : void 0,
12403
13083
  className: triggerClasses,
12404
13084
  children: [
12405
13085
  label,
@@ -12419,7 +13099,7 @@ function NavMenu({
12419
13099
  "div",
12420
13100
  {
12421
13101
  ref: panelRef,
12422
- role: "menu",
13102
+ id: panelId,
12423
13103
  "data-gs": "menu",
12424
13104
  className: cn(
12425
13105
  "fixed z-[var(--ds-z-popover)] py-ds-1.5",
@@ -12428,7 +13108,7 @@ function NavMenu({
12428
13108
  "border-[length:var(--ds-border-width)] border-[var(--ds-border-default)]"
12429
13109
  ),
12430
13110
  style: { top: pos.top, left: pos.left, minWidth: pos.minWidth },
12431
- children: items.map((item, i) => {
13111
+ children: /* @__PURE__ */ jsx("ul", { children: items.map((item, i) => {
12432
13112
  if (item.children?.length) {
12433
13113
  return /* @__PURE__ */ jsx(
12434
13114
  NavMenuGroup,
@@ -12454,7 +13134,7 @@ function NavMenu({
12454
13134
  },
12455
13135
  item.label
12456
13136
  );
12457
- })
13137
+ }) })
12458
13138
  }
12459
13139
  ),
12460
13140
  document.body
@@ -12465,11 +13145,10 @@ function NavMenuItemRow({
12465
13145
  item,
12466
13146
  onClick
12467
13147
  }) {
12468
- return /* @__PURE__ */ jsxs(
13148
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
12469
13149
  "a",
12470
13150
  {
12471
13151
  href: item.href ?? "#",
12472
- role: "menuitem",
12473
13152
  onClick: (e) => {
12474
13153
  e.preventDefault();
12475
13154
  onClick();
@@ -12495,14 +13174,15 @@ function NavMenuItemRow({
12495
13174
  ] })
12496
13175
  ]
12497
13176
  }
12498
- );
13177
+ ) });
12499
13178
  }
12500
13179
  function NavMenuGroup({
12501
13180
  group,
12502
13181
  isFirst,
12503
13182
  onItemClick
12504
13183
  }) {
12505
- return /* @__PURE__ */ jsxs("div", { children: [
13184
+ const headingId = useId();
13185
+ return /* @__PURE__ */ jsxs("li", { children: [
12506
13186
  !isFirst && /* @__PURE__ */ jsx(
12507
13187
  "div",
12508
13188
  {
@@ -12510,15 +13190,15 @@ function NavMenuGroup({
12510
13190
  style: { height: "var(--ds-divider-width, 1px)", marginTop: "var(--ds-space-1)", marginBottom: "var(--ds-space-1)" }
12511
13191
  }
12512
13192
  ),
12513
- /* @__PURE__ */ jsx("p", { className: "px-ds-3 pt-ds-1.5 pb-ds-1 text-2xs font-semibold uppercase tracking-wider text-[var(--ds-text-muted)]", children: group.label }),
12514
- group.children.map((child) => /* @__PURE__ */ jsx(
13193
+ /* @__PURE__ */ jsx("p", { id: headingId, className: "px-ds-3 pt-ds-1.5 pb-ds-1 text-2xs font-semibold uppercase tracking-wider text-[var(--ds-text-muted)]", children: group.label }),
13194
+ /* @__PURE__ */ jsx("ul", { "aria-labelledby": headingId, children: group.children.map((child) => /* @__PURE__ */ jsx(
12515
13195
  NavMenuItemRow,
12516
13196
  {
12517
13197
  item: child,
12518
13198
  onClick: () => onItemClick(child)
12519
13199
  },
12520
13200
  child.label
12521
- ))
13201
+ )) })
12522
13202
  ] });
12523
13203
  }
12524
13204
  function resolveNavItemType(type) {
@@ -12582,64 +13262,234 @@ function getTypeClasses(type, active) {
12582
13262
  );
12583
13263
  }
12584
13264
  }
12585
- function HamburgerButton({ onClick, open, className: cls }) {
12586
- return /* @__PURE__ */ jsx(
12587
- "button",
12588
- {
12589
- onClick,
12590
- "aria-label": open ? "Close navigation menu" : "Open navigation menu",
12591
- "aria-expanded": open ?? void 0,
12592
- className: cn(
12593
- "flex items-center justify-center shrink-0",
12594
- "w-10 h-10 rounded-[var(--ds-radius-md)]",
12595
- "text-[var(--ds-text-secondary)] hover:text-[var(--ds-text-primary)]",
12596
- "hover:bg-[var(--ds-bg-muted)] transition-colors",
12597
- "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
12598
- "cursor-pointer",
12599
- cls
12600
- ),
12601
- children: /* @__PURE__ */ jsx(Icon, { name: open ? "close" : "menu", size: 20 })
12602
- }
12603
- );
12604
- }
12605
- function flattenLinks(node) {
12606
- const out = [];
12607
- Children.forEach(node, (child) => {
12608
- if (!isValidElement(child)) return;
12609
- if (child.type === Fragment$1) out.push(...flattenLinks(child.props.children));
12610
- else out.push(child);
12611
- });
12612
- return out;
12613
- }
12614
- var Navbar = forwardRef(function Navbar2({
12615
- logo,
12616
- navLinks,
12617
- search,
12618
- actions,
12619
- userMenu,
12620
- variant = "default",
12621
- type: typeProp = "tint",
12622
- align = "left",
12623
- size,
12624
- position = "flush",
12625
- bordered,
12626
- sticky = true,
12627
- className,
12628
- onMenuClick
12629
- }, ref) {
12630
- const appShell = useAppShell();
12631
- useLayoutEffect(() => appShell?.registerChrome("navbar", position), [appShell?.registerChrome, position]);
12632
- const isSmallPhone = appShell?.isMobile && !appShell.isTablet;
12633
- const headerRef = useRef(null);
12634
- const navRef = useRef(null);
12635
- const widthsRef = useRef([]);
12636
- const moreWidthRef = useRef(88);
12637
- const [visibleCount, setVisibleCount] = useState(null);
12638
- const links = useMemo(() => flattenLinks(navLinks), [navLinks]);
13265
+ var DEFAULT_BUTTON_ATTR = "data-dialog-default-button";
13266
+ var sizeClasses = {
13267
+ // round 16 (the owner's item 5): the small dialog is 448 px, not 384 — a delete confirmation with a typed field was cramped
13268
+ sm: "max-w-md",
13269
+ md: "max-w-lg",
13270
+ lg: "max-w-2xl",
13271
+ xl: "max-w-4xl",
13272
+ full: "max-w-[calc(100vw-2rem)]"
13273
+ };
13274
+ function Dialog({ open, onClose: onCloseProp, title, description, children, footer, variant = "default", size = "md", alert = false, container, onSubmit, dirty, onDiscard }) {
13275
+ const guard = useDirtyGuard(dirty, onCloseProp, onDiscard);
13276
+ const onClose = guard.requestClose;
13277
+ const titleId = useId();
13278
+ const descriptionId = useId();
13279
+ const isMobile = useBreakpoint("sm");
13280
+ const overlayRef = useRef(null);
13281
+ const dialogRef = useRef(null);
13282
+ const opener = useRef(null);
13283
+ const defaultButtonRef = useRef(null);
13284
+ const onCloseRef = useLatest(onClose);
13285
+ const onSubmitRef = useLatest(onSubmit);
13286
+ const isTopLayer = useOverlayLayer(open);
13287
+ useFocusTrap(dialogRef, open, isTopLayer);
13288
+ const before = useFocusBeforeOpen(open);
13289
+ useEffect(() => {
13290
+ if (!open) return;
13291
+ const active = before.current;
13292
+ opener.current = active instanceof HTMLElement && active !== document.body && active.isConnected ? rememberFocus(active) : null;
13293
+ }, [open, before]);
13294
+ useEffect(() => {
13295
+ if (!open) return;
13296
+ requestAnimationFrame(() => {
13297
+ focusInitial(dialogRef.current, { destructive: variant === "destructive" });
13298
+ });
13299
+ }, [open, variant]);
13300
+ useEffect(() => {
13301
+ if (!open) return;
13302
+ const handler = (e) => {
13303
+ if (e.key !== "Escape" || e.defaultPrevented || !isTopLayer()) return;
13304
+ e.preventDefault();
13305
+ onCloseRef.current();
13306
+ };
13307
+ document.addEventListener("keydown", handler);
13308
+ return () => {
13309
+ document.removeEventListener("keydown", handler);
13310
+ };
13311
+ }, [open, onCloseRef, isTopLayer]);
13312
+ useEffect(() => {
13313
+ if (!open) return;
13314
+ lockBodyScroll();
13315
+ return () => {
13316
+ unlockBodyScroll();
13317
+ const r = opener.current;
13318
+ opener.current = null;
13319
+ const active = document.activeElement;
13320
+ if (!active || active === document.body || !active.isConnected) r?.restore();
13321
+ };
13322
+ }, [open]);
13323
+ if (!open) return null;
13324
+ return createPortal(
13325
+ /* @__PURE__ */ jsxs(
13326
+ "div",
13327
+ {
13328
+ ref: overlayRef,
13329
+ className: "fixed inset-0 z-[var(--ds-z-modal)] flex items-center justify-center p-ds-4",
13330
+ children: [
13331
+ /* @__PURE__ */ jsx(
13332
+ "div",
13333
+ {
13334
+ className: "fixed inset-0 bg-black/50 [backdrop-filter:blur(calc(var(--ds-glass-blur)*0.3))] [-webkit-backdrop-filter:blur(calc(var(--ds-glass-blur)*0.3))]",
13335
+ onClick: alert ? void 0 : onClose,
13336
+ "aria-hidden": "true"
13337
+ }
13338
+ ),
13339
+ /* @__PURE__ */ jsxs(
13340
+ "form",
13341
+ {
13342
+ onSubmit: (e) => {
13343
+ e.preventDefault();
13344
+ if (variant === "destructive") return;
13345
+ const submitter = e.nativeEvent.submitter;
13346
+ const implicit = !submitter || submitter === defaultButtonRef.current;
13347
+ if (!implicit && submitter.getAttribute("type") !== "submit") return;
13348
+ if (implicit) {
13349
+ const explicit = e.currentTarget.querySelector(`button[type="submit"]:not([${DEFAULT_BUTTON_ATTR}])`);
13350
+ if (explicit?.disabled) return;
13351
+ }
13352
+ onSubmitRef.current?.();
13353
+ },
13354
+ className: "contents",
13355
+ children: [
13356
+ /* @__PURE__ */ jsx(
13357
+ "button",
13358
+ {
13359
+ ref: defaultButtonRef,
13360
+ type: "submit",
13361
+ tabIndex: -1,
13362
+ "aria-hidden": "true",
13363
+ className: "sr-only",
13364
+ ...{ [DEFAULT_BUTTON_ATTR]: "" }
13365
+ }
13366
+ ),
13367
+ /* @__PURE__ */ jsxs(
13368
+ "div",
13369
+ {
13370
+ ref: dialogRef,
13371
+ role: alert ? "alertdialog" : "dialog",
13372
+ "aria-modal": "true",
13373
+ "data-gs": "dialog",
13374
+ "aria-labelledby": titleId,
13375
+ "aria-describedby": description ? descriptionId : void 0,
13376
+ style: { containerType: "inline-size" },
13377
+ className: cn(
13378
+ "relative z-10 w-full",
13379
+ isMobile ? "max-w-none h-full" : sizeClasses[size],
13380
+ "bg-[var(--ds-bg-surface)] text-[var(--ds-text-primary)]",
13381
+ GLASS_OVERLAY,
13382
+ "border-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
13383
+ isMobile ? "rounded-none" : "rounded-[var(--ds-radius-xl)]",
13384
+ "overflow-hidden",
13385
+ "animate-in fade-in-0 zoom-in-95"
13386
+ ),
13387
+ children: [
13388
+ /* @__PURE__ */ jsx(
13389
+ "button",
13390
+ {
13391
+ type: "button",
13392
+ onClick: onClose,
13393
+ ...{ [OVERLAY_CLOSE_ATTR]: "" },
13394
+ className: "absolute right-4 top-4 text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] transition-colors cursor-pointer",
13395
+ "aria-label": "Close",
13396
+ children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 18 })
13397
+ }
13398
+ ),
13399
+ /* @__PURE__ */ jsx("div", { className: "p-ds-6", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-ds-3", children: [
13400
+ variant === "destructive" && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center shrink-0 w-10 h-10 rounded-[var(--ds-radius-full)] bg-[var(--ds-status-error-bg)]", children: /* @__PURE__ */ jsx(Icon, { name: "warning", size: 20, className: "text-[var(--ds-status-error)]" }) }),
13401
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
13402
+ /* @__PURE__ */ jsx(
13403
+ "h2",
13404
+ {
13405
+ id: titleId,
13406
+ className: "font-[family-name:var(--ds-font-display)] text-lg font-[var(--ds-weight-heading)] leading-none tracking-tight",
13407
+ children: title
13408
+ }
13409
+ ),
13410
+ description && /* @__PURE__ */ jsx("p", { id: descriptionId, className: "mt-ds-2 text-sm text-[var(--ds-text-muted)]", children: description }),
13411
+ children && /* @__PURE__ */ jsx("div", { className: "mt-ds-4", children: /* @__PURE__ */ jsx(OverlayCloseCtx, { value: onClose, children }) })
13412
+ ] })
13413
+ ] }) }),
13414
+ /* @__PURE__ */ jsx(DiscardConfirm, { open: guard.confirming, onKeep: guard.keep, onDiscard: guard.discard }),
13415
+ footer && /* @__PURE__ */ jsx("div", { className: cn(
13416
+ "flex justify-end gap-ds-2 p-ds-4",
13417
+ "border-t-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
13418
+ "bg-[var(--ds-bg-muted)]",
13419
+ "rounded-b-[var(--ds-radius-xl)]"
13420
+ ), children: footer })
13421
+ ]
13422
+ }
13423
+ )
13424
+ ]
13425
+ }
13426
+ )
13427
+ ]
13428
+ }
13429
+ ),
13430
+ container ?? document.body
13431
+ );
13432
+ }
13433
+ function HamburgerButton({ onClick, open, className: cls }) {
13434
+ return /* @__PURE__ */ jsx(
13435
+ "button",
13436
+ {
13437
+ onClick,
13438
+ "aria-label": open ? "Close navigation menu" : "Open navigation menu",
13439
+ "aria-expanded": open ?? void 0,
13440
+ className: cn(
13441
+ "flex items-center justify-center shrink-0",
13442
+ "w-10 h-10 rounded-[var(--ds-radius-md)]",
13443
+ "text-[var(--ds-text-secondary)] hover:text-[var(--ds-text-primary)]",
13444
+ "hover:bg-[var(--ds-bg-muted)] transition-colors",
13445
+ "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
13446
+ "cursor-pointer",
13447
+ cls
13448
+ ),
13449
+ children: /* @__PURE__ */ jsx(Icon, { name: open ? "close" : "menu", size: 20 })
13450
+ }
13451
+ );
13452
+ }
13453
+ function flattenLinks(node) {
13454
+ const out = [];
13455
+ Children.forEach(node, (child) => {
13456
+ if (!isValidElement(child)) return;
13457
+ if (child.type === Fragment$1) out.push(...flattenLinks(child.props.children));
13458
+ else out.push(child);
13459
+ });
13460
+ return out;
13461
+ }
13462
+ var Navbar = forwardRef(function Navbar2({
13463
+ logo,
13464
+ navLinks,
13465
+ search,
13466
+ actions,
13467
+ userMenu,
13468
+ variant = "default",
13469
+ type: typeProp = "tint",
13470
+ align = "left",
13471
+ size,
13472
+ position = "flush",
13473
+ bordered,
13474
+ sticky = true,
13475
+ className,
13476
+ onMenuClick
13477
+ }, ref) {
13478
+ const appShell = useAppShell();
13479
+ useLayoutEffect(() => appShell?.registerChrome("navbar", position), [appShell?.registerChrome, position]);
13480
+ const isSmallPhone = appShell?.isMobile && !appShell.isTablet;
13481
+ const headerRef = useRef(null);
13482
+ const navRef = useRef(null);
13483
+ const widthsRef = useRef([]);
13484
+ const moreWidthRef = useRef(88);
13485
+ const [visibleCount, setVisibleCount] = useState(null);
13486
+ const links = useMemo(() => flattenLinks(navLinks), [navLinks]);
12639
13487
  const mobileHidesLinks = !!appShell?.isMobile && (appShell.hasMobileMenu || appShell.hasMobileNav);
13488
+ const focus = appShell?.focus ?? null;
13489
+ const [confirmExit, setConfirmExit] = useState(false);
12640
13490
  useEffect(() => {
12641
13491
  const header = headerRef.current;
12642
- if (!header || mobileHidesLinks || links.length === 0 || typeof ResizeObserver === "undefined") return;
13492
+ if (!header || mobileHidesLinks || focus || links.length === 0 || typeof ResizeObserver === "undefined") return;
12643
13493
  const measure = () => {
12644
13494
  const nav = navRef.current;
12645
13495
  if (!nav) return;
@@ -12676,7 +13526,7 @@ var Navbar = forwardRef(function Navbar2({
12676
13526
  const ro = new ResizeObserver(measure);
12677
13527
  ro.observe(header);
12678
13528
  return () => ro.disconnect();
12679
- }, [mobileHidesLinks, links]);
13529
+ }, [mobileHidesLinks, focus, links]);
12680
13530
  const [topInset, setTopInset] = useState(null);
12681
13531
  const shellEl = appShell?.container ?? null;
12682
13532
  useLayoutEffect(() => {
@@ -12708,7 +13558,40 @@ var Navbar = forwardRef(function Navbar2({
12708
13558
  }, [visibleCount, links]);
12709
13559
  const showHamburger = appShell?.isMobile && appShell.hasMobileMenu && !(isSmallPhone && appShell.hasMobileNav) || !!onMenuClick;
12710
13560
  const handleHamburger = () => onMenuClick ? onMenuClick() : appShell?.setSidebarOpen(!appShell.sidebarOpen);
12711
- const hideNavLinks = mobileHidesLinks;
13561
+ const hideNavLinks = mobileHidesLinks || !!focus;
13562
+ const shownSearch = focus ? null : search;
13563
+ const requestExit = () => {
13564
+ if (!focus) return;
13565
+ if (focus.kind === "in-app" && appShell?.hasUnsavedChanges) {
13566
+ setConfirmExit(true);
13567
+ return;
13568
+ }
13569
+ focus.onExit?.();
13570
+ };
13571
+ const exitControl = focus ? /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: requestExit, children: focus.exitLabel ?? (focus.kind === "before-app" ? "Save and log out" : "Exit") }) : null;
13572
+ const shownUserMenu = focus?.kind === "before-app" ? null : userMenu;
13573
+ const asking = confirmExit && focus?.kind === "in-app" && !!appShell?.hasUnsavedChanges;
13574
+ if (confirmExit && !asking) setConfirmExit(false);
13575
+ const exitDialog = focus?.kind === "in-app" ? /* @__PURE__ */ jsx(
13576
+ Dialog,
13577
+ {
13578
+ open: asking,
13579
+ onClose: () => setConfirmExit(false),
13580
+ title: "You have unsaved changes",
13581
+ description: "Exit now and your changes are lost.",
13582
+ variant: "destructive",
13583
+ size: "sm",
13584
+ alert: true,
13585
+ container: shellEl ?? void 0,
13586
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [
13587
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => setConfirmExit(false), children: "Keep editing" }),
13588
+ /* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: () => {
13589
+ setConfirmExit(false);
13590
+ focus.onExit?.();
13591
+ }, children: "Discard changes" })
13592
+ ] })
13593
+ }
13594
+ ) : null;
12712
13595
  const type = resolveNavItemType(typeProp);
12713
13596
  const isMinimal = type === "minimal";
12714
13597
  const isFloating = position === "floating";
@@ -12774,9 +13657,10 @@ var Navbar = forwardRef(function Navbar2({
12774
13657
  ] }),
12775
13658
  navLinks && !hideNavLinks ? /* @__PURE__ */ jsx(NavbarTypeContext.Provider, { value: type, children: /* @__PURE__ */ jsx("nav", { ref: navRef, className: "flex items-center gap-ds-2 min-w-0 overflow-x-clip justify-center", "data-navbar-type": type, children: linkRow }) }) : /* @__PURE__ */ jsx("div", { "data-navbar-spacer": "" }),
12776
13659
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-4 justify-end", children: [
12777
- search && /* @__PURE__ */ jsx("div", { className: "flex items-center shrink-0", children: search }),
13660
+ shownSearch && /* @__PURE__ */ jsx("div", { className: "flex items-center shrink-0", children: shownSearch }),
12778
13661
  actions && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-ds-2 shrink-0", children: actions }),
12779
- userMenu && /* @__PURE__ */ jsx("div", { className: cn("flex items-center", resolvedSize === "sm" ? "min-w-0 max-w-[180px]" : "shrink-0"), children: userMenu })
13662
+ exitControl && /* @__PURE__ */ jsx("div", { className: "flex items-center shrink-0", children: exitControl }),
13663
+ shownUserMenu && /* @__PURE__ */ jsx("div", { className: cn("flex items-center", resolvedSize === "sm" ? "min-w-0 max-w-[180px]" : "shrink-0"), children: shownUserMenu })
12780
13664
  ] })
12781
13665
  ]
12782
13666
  }
@@ -12801,15 +13685,16 @@ var Navbar = forwardRef(function Navbar2({
12801
13685
  align === "right" && /* @__PURE__ */ jsx("div", { "data-navbar-spacer": "", className: "flex-1" }),
12802
13686
  navLinks && !hideNavLinks && /* @__PURE__ */ jsx(NavbarTypeContext.Provider, { value: type, children: /* @__PURE__ */ jsx("nav", { ref: navRef, className: cn("flex items-center gap-ds-2 min-w-0 overflow-x-clip", align === "left" && "flex-1"), "data-navbar-type": type, children: linkRow }) }),
12803
13687
  (!navLinks || hideNavLinks) && /* @__PURE__ */ jsx("div", { "data-navbar-spacer": "", className: "flex-1" }),
12804
- search && /* @__PURE__ */ jsx("div", { className: "flex items-center shrink-0", children: search }),
13688
+ shownSearch && /* @__PURE__ */ jsx("div", { className: "flex items-center shrink-0", children: shownSearch }),
12805
13689
  actions && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-ds-2 shrink-0", children: actions }),
12806
- userMenu && /* @__PURE__ */ jsx("div", { className: cn("flex items-center", resolvedSize === "sm" ? "min-w-0 max-w-[180px]" : "shrink-0"), children: userMenu })
13690
+ exitControl && /* @__PURE__ */ jsx("div", { className: "flex items-center shrink-0", children: exitControl }),
13691
+ shownUserMenu && /* @__PURE__ */ jsx("div", { className: cn("flex items-center", resolvedSize === "sm" ? "min-w-0 max-w-[180px]" : "shrink-0"), children: shownUserMenu })
12807
13692
  ]
12808
13693
  }
12809
13694
  );
12810
13695
  }
12811
13696
  if (isFloating) {
12812
- return /* @__PURE__ */ jsx(
13697
+ return /* @__PURE__ */ jsxs(
12813
13698
  "div",
12814
13699
  {
12815
13700
  ref,
@@ -12819,11 +13704,17 @@ var Navbar = forwardRef(function Navbar2({
12819
13704
  className
12820
13705
  ),
12821
13706
  style: { padding: "var(--ds-space-3)" },
12822
- children: headerEl
13707
+ children: [
13708
+ headerEl,
13709
+ exitDialog
13710
+ ]
12823
13711
  }
12824
13712
  );
12825
13713
  }
12826
- return headerEl;
13714
+ return exitDialog ? /* @__PURE__ */ jsxs(Fragment, { children: [
13715
+ headerEl,
13716
+ exitDialog
13717
+ ] }) : headerEl;
12827
13718
  });
12828
13719
  function MobileHeaderMenu({ items, type = "tint", onItemClick, className }) {
12829
13720
  return /* @__PURE__ */ jsx("nav", { className: cn("flex flex-col", className), style: { padding: "var(--ds-space-4)" }, children: /* @__PURE__ */ jsx("div", { className: "flex flex-col", style: { gap: type === "underline" ? 0 : "var(--ds-space-1)" }, children: items.map((item, i) => /* @__PURE__ */ jsx(
@@ -12915,169 +13806,12 @@ function MobileMenuItem({
12915
13806
  }
12916
13807
  );
12917
13808
  }
12918
- var DEFAULT_BUTTON_ATTR = "data-dialog-default-button";
12919
- var sizeClasses = {
12920
- // round 16 (the owner's item 5): the small dialog is 448 px, not 384 — a delete confirmation with a typed field was cramped
12921
- sm: "max-w-md",
12922
- md: "max-w-lg",
12923
- lg: "max-w-2xl",
12924
- xl: "max-w-4xl",
12925
- full: "max-w-[calc(100vw-2rem)]"
12926
- };
12927
- function Dialog({ open, onClose: onCloseProp, title, description, children, footer, variant = "default", size = "md", alert = false, container, onSubmit, dirty, onDiscard }) {
12928
- const guard = useDirtyGuard(dirty, onCloseProp, onDiscard);
12929
- const onClose = guard.requestClose;
12930
- const titleId = useId();
12931
- const descriptionId = useId();
12932
- const isMobile = useBreakpoint("sm");
12933
- const overlayRef = useRef(null);
12934
- const dialogRef = useRef(null);
12935
- const triggerRef = useRef(null);
12936
- const defaultButtonRef = useRef(null);
12937
- const onCloseRef = useLatest(onClose);
12938
- const onSubmitRef = useLatest(onSubmit);
12939
- const isTopLayer = useOverlayLayer(open);
12940
- useFocusTrap(dialogRef, open, isTopLayer);
12941
- useEffect(() => {
12942
- if (!open) return;
12943
- triggerRef.current = document.activeElement;
12944
- requestAnimationFrame(() => {
12945
- focusInitial(dialogRef.current, { destructive: variant === "destructive" });
12946
- });
12947
- }, [open, variant]);
12948
- useEffect(() => {
12949
- if (!open) return;
12950
- const handler = (e) => {
12951
- if (e.key === "Escape" && !alert && isTopLayer()) onCloseRef.current();
12952
- };
12953
- document.addEventListener("keydown", handler);
12954
- return () => {
12955
- document.removeEventListener("keydown", handler);
12956
- };
12957
- }, [open, alert, onCloseRef, isTopLayer]);
12958
- useEffect(() => {
12959
- if (!open) return;
12960
- lockBodyScroll();
12961
- return () => {
12962
- unlockBodyScroll();
12963
- triggerRef.current?.focus();
12964
- };
12965
- }, [open]);
12966
- if (!open) return null;
12967
- return createPortal(
12968
- /* @__PURE__ */ jsxs(
12969
- "div",
12970
- {
12971
- ref: overlayRef,
12972
- className: "fixed inset-0 z-[var(--ds-z-modal)] flex items-center justify-center p-ds-4",
12973
- children: [
12974
- /* @__PURE__ */ jsx(
12975
- "div",
12976
- {
12977
- className: "fixed inset-0 bg-black/50 [backdrop-filter:blur(calc(var(--ds-glass-blur)*0.3))] [-webkit-backdrop-filter:blur(calc(var(--ds-glass-blur)*0.3))]",
12978
- onClick: alert ? void 0 : onClose,
12979
- "aria-hidden": "true"
12980
- }
12981
- ),
12982
- /* @__PURE__ */ jsxs(
12983
- "form",
12984
- {
12985
- onSubmit: (e) => {
12986
- e.preventDefault();
12987
- if (variant === "destructive") return;
12988
- const submitter = e.nativeEvent.submitter;
12989
- const implicit = !submitter || submitter === defaultButtonRef.current;
12990
- if (!implicit && submitter.getAttribute("type") !== "submit") return;
12991
- if (implicit) {
12992
- const explicit = e.currentTarget.querySelector(`button[type="submit"]:not([${DEFAULT_BUTTON_ATTR}])`);
12993
- if (explicit?.disabled) return;
12994
- }
12995
- onSubmitRef.current?.();
12996
- },
12997
- className: "contents",
12998
- children: [
12999
- /* @__PURE__ */ jsx(
13000
- "button",
13001
- {
13002
- ref: defaultButtonRef,
13003
- type: "submit",
13004
- tabIndex: -1,
13005
- "aria-hidden": "true",
13006
- className: "sr-only",
13007
- ...{ [DEFAULT_BUTTON_ATTR]: "" }
13008
- }
13009
- ),
13010
- /* @__PURE__ */ jsxs(
13011
- "div",
13012
- {
13013
- ref: dialogRef,
13014
- role: alert ? "alertdialog" : "dialog",
13015
- "aria-modal": "true",
13016
- "data-gs": "dialog",
13017
- "aria-labelledby": titleId,
13018
- "aria-describedby": description ? descriptionId : void 0,
13019
- style: { containerType: "inline-size" },
13020
- className: cn(
13021
- "relative z-10 w-full",
13022
- isMobile ? "max-w-none h-full" : sizeClasses[size],
13023
- "bg-[var(--ds-bg-surface)] text-[var(--ds-text-primary)]",
13024
- GLASS_OVERLAY,
13025
- "border-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
13026
- isMobile ? "rounded-none" : "rounded-[var(--ds-radius-xl)]",
13027
- "overflow-hidden",
13028
- "animate-in fade-in-0 zoom-in-95"
13029
- ),
13030
- children: [
13031
- /* @__PURE__ */ jsx(
13032
- "button",
13033
- {
13034
- type: "button",
13035
- onClick: onClose,
13036
- ...{ [OVERLAY_CLOSE_ATTR]: "" },
13037
- className: "absolute right-4 top-4 text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] transition-colors cursor-pointer",
13038
- "aria-label": "Close",
13039
- children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 18 })
13040
- }
13041
- ),
13042
- /* @__PURE__ */ jsx("div", { className: "p-ds-6", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-ds-3", children: [
13043
- variant === "destructive" && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center shrink-0 w-10 h-10 rounded-[var(--ds-radius-full)] bg-[var(--ds-status-error-bg)]", children: /* @__PURE__ */ jsx(Icon, { name: "warning", size: 20, className: "text-[var(--ds-status-error)]" }) }),
13044
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
13045
- /* @__PURE__ */ jsx(
13046
- "h2",
13047
- {
13048
- id: titleId,
13049
- className: "font-[family-name:var(--ds-font-display)] text-lg font-[var(--ds-weight-heading)] leading-none tracking-tight",
13050
- children: title
13051
- }
13052
- ),
13053
- description && /* @__PURE__ */ jsx("p", { id: descriptionId, className: "mt-ds-2 text-sm text-[var(--ds-text-muted)]", children: description }),
13054
- children && /* @__PURE__ */ jsx("div", { className: "mt-ds-4", children: /* @__PURE__ */ jsx(OverlayCloseCtx, { value: onClose, children }) })
13055
- ] })
13056
- ] }) }),
13057
- /* @__PURE__ */ jsx(DiscardConfirm, { open: guard.confirming, onKeep: guard.keep, onDiscard: guard.discard }),
13058
- footer && /* @__PURE__ */ jsx("div", { className: cn(
13059
- "flex justify-end gap-ds-2 p-ds-4",
13060
- "border-t-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
13061
- "bg-[var(--ds-bg-muted)]",
13062
- "rounded-b-[var(--ds-radius-xl)]"
13063
- ), children: footer })
13064
- ]
13065
- }
13066
- )
13067
- ]
13068
- }
13069
- )
13070
- ]
13071
- }
13072
- ),
13073
- container ?? document.body
13074
- );
13075
- }
13076
13809
  function CommandPalette({ open, onOpenChange, commands, placeholder = "Type a command\u2026", loading = false, container }) {
13077
13810
  const [query, setQuery] = useState("");
13078
13811
  const [selectedIdx, setSelectedIdx] = useState(0);
13079
13812
  const inputRef = useRef(null);
13080
13813
  const listRef = useRef(null);
13814
+ const dialogRef = useRef(null);
13081
13815
  const listboxId = useId();
13082
13816
  const filtered = commands.filter(
13083
13817
  (c) => c.label.toLowerCase().includes(query.toLowerCase()) || c.group?.toLowerCase().includes(query.toLowerCase())
@@ -13106,20 +13840,26 @@ function CommandPalette({ open, onOpenChange, commands, placeholder = "Type a co
13106
13840
  if (open) requestAnimationFrame(() => inputRef.current?.focus());
13107
13841
  }, [open]);
13108
13842
  const isTopLayer = useOverlayLayer(open);
13843
+ useFocusTrap(dialogRef, open, isTopLayer);
13844
+ const returnFocus = useReturnFocus(open, dialogRef);
13845
+ const onOpenChangeRef = useLatest(onOpenChange);
13109
13846
  useEffect(() => {
13110
13847
  if (!open) return;
13111
13848
  const handler = (e) => {
13112
- if (e.key === "Escape" && isTopLayer()) onOpenChange(false);
13849
+ if (e.key !== "Escape" || e.defaultPrevented || !isTopLayer()) return;
13850
+ e.preventDefault();
13851
+ onOpenChangeRef.current(false);
13113
13852
  };
13114
13853
  document.addEventListener("keydown", handler);
13115
13854
  return () => document.removeEventListener("keydown", handler);
13116
- }, [open, onOpenChange, isTopLayer]);
13855
+ }, [open, onOpenChangeRef, isTopLayer]);
13117
13856
  const handleSelect = useCallback(
13118
13857
  (cmd) => {
13858
+ returnFocus();
13119
13859
  cmd.onSelect();
13120
13860
  onOpenChange(false);
13121
13861
  },
13122
- [onOpenChange]
13862
+ [onOpenChange, returnFocus]
13123
13863
  );
13124
13864
  const handleKeyDown = (e) => {
13125
13865
  if (e.key === "ArrowDown") {
@@ -13150,8 +13890,10 @@ function CommandPalette({ open, onOpenChange, commands, placeholder = "Type a co
13150
13890
  /* @__PURE__ */ jsxs(
13151
13891
  "div",
13152
13892
  {
13893
+ ref: dialogRef,
13153
13894
  role: "dialog",
13154
13895
  "aria-modal": "true",
13896
+ "aria-label": "Command palette",
13155
13897
  className: cn(
13156
13898
  "relative z-10 w-full max-w-lg",
13157
13899
  "bg-[var(--ds-bg-surface)] text-[var(--ds-text-primary)]",
@@ -13177,6 +13919,7 @@ function CommandPalette({ open, onOpenChange, commands, placeholder = "Type a co
13177
13919
  "aria-expanded": true,
13178
13920
  "aria-controls": listboxId,
13179
13921
  "aria-autocomplete": "list",
13922
+ "aria-activedescendant": flatFiltered[selectedIdx] ? `${listboxId}-opt-${selectedIdx}` : void 0,
13180
13923
  value: query,
13181
13924
  onChange: (e) => setQuery(e.target.value),
13182
13925
  onKeyDown: handleKeyDown,
@@ -13210,7 +13953,9 @@ function CommandPalette({ open, onOpenChange, commands, placeholder = "Type a co
13210
13953
  return /* @__PURE__ */ jsxs(
13211
13954
  "button",
13212
13955
  {
13956
+ id: `${listboxId}-opt-${myIdx}`,
13213
13957
  role: "option",
13958
+ tabIndex: -1,
13214
13959
  "aria-selected": myIdx === selectedIdx,
13215
13960
  onClick: () => handleSelect(cmd),
13216
13961
  onMouseEnter: () => setSelectedIdx(myIdx),
@@ -13321,7 +14066,9 @@ function ContextMenu({ items, children, className, ref, container }) {
13321
14066
  };
13322
14067
  const keyHandler = (e) => {
13323
14068
  if (e.key === "Escape") {
13324
- if (isTopLayer()) setOpen(false);
14069
+ if (e.defaultPrevented || !isTopLayer()) return;
14070
+ e.preventDefault();
14071
+ setOpen(false);
13325
14072
  return;
13326
14073
  }
13327
14074
  const actionable = items.filter((it) => !it.separator && !it.disabled);
@@ -13458,10 +14205,11 @@ function NotificationCenter({
13458
14205
  const [internalFilter, setInternalFilter] = useState("all");
13459
14206
  const isControlled = controlledOpen !== void 0;
13460
14207
  const open = isControlled ? controlledOpen : internalOpen;
14208
+ const onOpenChangeRef = useLatest(onOpenChange);
13461
14209
  const setOpen = useCallback((v) => {
13462
- if (isControlled) onOpenChange?.(v);
14210
+ if (isControlled) onOpenChangeRef.current?.(v);
13463
14211
  else setInternalOpen(v);
13464
- }, [isControlled, onOpenChange]);
14212
+ }, [isControlled, onOpenChangeRef]);
13465
14213
  const triggerRef = useRef(null);
13466
14214
  const panelRef = useRef(null);
13467
14215
  const pos = usePosition(triggerRef, panelRef, open, { side: "bottom", align: "end", offset: 8, flip: true });
@@ -13469,8 +14217,7 @@ function NotificationCenter({
13469
14217
  const setFilter = onFilterChange ?? setInternalFilter;
13470
14218
  const unreadCount = notifications.filter((n) => !n.read).length;
13471
14219
  const filteredNotifications = filter === "unread" ? notifications.filter((n) => !n.read) : notifications;
13472
- const isTopLayer = useOverlayLayer(open);
13473
- useCloseOnFocusOut(open, triggerRef, panelRef, () => setOpen(false), isTopLayer);
14220
+ const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: panelRef, close: () => setOpen(false), tabExits: "edges" });
13474
14221
  useEffect(() => {
13475
14222
  if (!open) return;
13476
14223
  const id = requestAnimationFrame(() => focusInitial(panelRef.current));
@@ -13479,10 +14226,10 @@ function NotificationCenter({
13479
14226
  useEffect(() => {
13480
14227
  if (!open) return;
13481
14228
  const onKey = (e) => {
13482
- if (e.key === "Escape" && isTopLayer()) {
13483
- setOpen(false);
13484
- triggerRef.current?.focus();
13485
- }
14229
+ if (e.key !== "Escape" || e.defaultPrevented || !isTopLayer()) return;
14230
+ e.preventDefault();
14231
+ returnFocus();
14232
+ setOpen(false);
13486
14233
  };
13487
14234
  const onClick = (e) => {
13488
14235
  const target = e.target;
@@ -13495,7 +14242,7 @@ function NotificationCenter({
13495
14242
  document.removeEventListener("keydown", onKey);
13496
14243
  document.removeEventListener("mousedown", onClick);
13497
14244
  };
13498
- }, [open, setOpen, isTopLayer]);
14245
+ }, [open, setOpen, isTopLayer, returnFocus]);
13499
14246
  const grouped = groupByDate(filteredNotifications);
13500
14247
  if (variant === "page") {
13501
14248
  return /* @__PURE__ */ jsxs("div", { ref, className: cn("w-full", className), children: [
@@ -13604,6 +14351,8 @@ function NotificationCenter({
13604
14351
  ref: triggerRef,
13605
14352
  onClick: () => setOpen(!open),
13606
14353
  "aria-label": `Notifications${unreadCount ? ` (${unreadCount} unread)` : ""}`,
14354
+ "aria-haspopup": "dialog",
14355
+ "aria-expanded": open,
13607
14356
  className: cn(
13608
14357
  "relative flex items-center justify-center w-9 h-9",
13609
14358
  "rounded-[var(--ds-radius-md)]",
@@ -13622,6 +14371,8 @@ function NotificationCenter({
13622
14371
  "div",
13623
14372
  {
13624
14373
  ref: panelRef,
14374
+ role: "dialog",
14375
+ "aria-label": "Notifications",
13625
14376
  className: cn(
13626
14377
  "fixed z-[var(--ds-z-popover)] w-[360px] max-h-[480px] flex flex-col",
13627
14378
  "bg-[var(--ds-bg-surface-elevated)]",
@@ -13768,17 +14519,30 @@ function Tour({ steps, open, onClose, onComplete, container }) {
13768
14519
  if (current > 0) setCurrent((c) => c - 1);
13769
14520
  }, [current]);
13770
14521
  const isTopLayer = useOverlayLayer(open);
14522
+ const nextRef = useRef(null);
14523
+ const titleId = useId();
14524
+ useFocusTrap(popoverRef, open, isTopLayer);
14525
+ useReturnFocus(open, popoverRef);
14526
+ useEffect(() => {
14527
+ if (!open) return;
14528
+ const raf = requestAnimationFrame(() => nextRef.current?.focus());
14529
+ return () => cancelAnimationFrame(raf);
14530
+ }, [open, current]);
14531
+ const onCloseRef = useLatest(onClose);
14532
+ const handleNextRef = useLatest(handleNext);
14533
+ const handlePrevRef = useLatest(handlePrev);
13771
14534
  useEffect(() => {
13772
14535
  if (!open) return;
13773
14536
  const onKey = (e) => {
13774
- if (!isTopLayer()) return;
13775
- if (e.key === "Escape") onClose();
13776
- else if (e.key === "ArrowRight") handleNext();
13777
- else if (e.key === "ArrowLeft") handlePrev();
14537
+ if (e.defaultPrevented || !isTopLayer()) return;
14538
+ const act = e.key === "Escape" ? onCloseRef.current : e.key === "ArrowRight" ? handleNextRef.current : e.key === "ArrowLeft" ? handlePrevRef.current : null;
14539
+ if (!act) return;
14540
+ e.preventDefault();
14541
+ act();
13778
14542
  };
13779
14543
  document.addEventListener("keydown", onKey);
13780
14544
  return () => document.removeEventListener("keydown", onKey);
13781
- }, [open, onClose, handleNext, handlePrev, isTopLayer]);
14545
+ }, [open, isTopLayer, onCloseRef, handleNextRef, handlePrevRef]);
13782
14546
  if (!open || !step) return null;
13783
14547
  const padding = 8;
13784
14548
  const spotlightRadius = 8;
@@ -13820,6 +14584,9 @@ function Tour({ steps, open, onClose, onComplete, container }) {
13820
14584
  "div",
13821
14585
  {
13822
14586
  ref: popoverRef,
14587
+ role: "dialog",
14588
+ "aria-modal": "true",
14589
+ "aria-labelledby": titleId,
13823
14590
  className: cn(
13824
14591
  "fixed z-[2] w-[300px]",
13825
14592
  "bg-[var(--ds-bg-surface-elevated)]",
@@ -13832,7 +14599,7 @@ function Tour({ steps, open, onClose, onComplete, container }) {
13832
14599
  children: [
13833
14600
  /* @__PURE__ */ jsxs("div", { className: "px-ds-4 pt-ds-4 pb-ds-3", children: [
13834
14601
  /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-ds-2 mb-ds-1", children: [
13835
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)]", children: step.title }),
14602
+ /* @__PURE__ */ jsx("h3", { id: titleId, className: "text-sm font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)]", children: step.title }),
13836
14603
  /* @__PURE__ */ jsx(
13837
14604
  "button",
13838
14605
  {
@@ -13868,6 +14635,7 @@ function Tour({ steps, open, onClose, onComplete, container }) {
13868
14635
  /* @__PURE__ */ jsx(
13869
14636
  "button",
13870
14637
  {
14638
+ ref: nextRef,
13871
14639
  onClick: handleNext,
13872
14640
  className: cn(
13873
14641
  "px-ds-3 py-ds-1.5 text-xs font-medium rounded-[var(--ds-radius-sm)]",
@@ -14238,7 +15006,7 @@ function useDestructiveAction(options) {
14238
15006
  }
14239
15007
  setOpen(true);
14240
15008
  };
14241
- const matches = !typedRequired || typed.trim().toLowerCase() === typedText.trim().toLowerCase();
15009
+ const matches2 = !typedRequired || typed.trim().toLowerCase() === typedText.trim().toLowerCase();
14242
15010
  const element = open ? /* @__PURE__ */ jsx(
14243
15011
  Dialog,
14244
15012
  {
@@ -14249,7 +15017,7 @@ function useDestructiveAction(options) {
14249
15017
  variant: "destructive",
14250
15018
  footer: /* @__PURE__ */ jsxs(Fragment, { children: [
14251
15019
  /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: close, disabled: busy, children: "Cancel" }),
14252
- /* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: () => void confirm(), disabled: !matches || busy, loading: busy, children: label })
15020
+ /* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: () => void confirm(), disabled: !matches2 || busy, loading: busy, children: label })
14253
15021
  ] }),
14254
15022
  children: typedRequired && /* @__PURE__ */ jsx(Field, { label: /* @__PURE__ */ jsxs(Fragment, { children: [
14255
15023
  "Type ",