@dreamtree-org/twreact-ui 1.3.8 → 1.3.10

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 (109) hide show
  1. package/DESIGN.md +11 -3
  2. package/README.md +9 -1
  3. package/ai-skills/dreamtree-ui.md +2 -2
  4. package/dist/ai-schema.json +19 -8
  5. package/dist/charts/index.js +69 -24
  6. package/dist/components/blocks/AppShell.d.ts.map +1 -1
  7. package/dist/components/charts/chartTheme.d.ts +2 -5
  8. package/dist/components/charts/chartTheme.d.ts.map +1 -1
  9. package/dist/components/core/Accordion.d.ts.map +1 -1
  10. package/dist/components/core/ColorPicker.d.ts.map +1 -1
  11. package/dist/components/core/DatePicker.d.ts.map +1 -1
  12. package/dist/components/core/Form.d.ts.map +1 -1
  13. package/dist/components/core/Input.d.ts.map +1 -1
  14. package/dist/components/core/PriceRangePicker.d.ts +0 -29
  15. package/dist/components/core/PriceRangePicker.d.ts.map +1 -1
  16. package/dist/components/core/Radio.d.ts.map +1 -1
  17. package/dist/components/core/Rate.d.ts.map +1 -1
  18. package/dist/components/core/RoundedTag.d.ts.map +1 -1
  19. package/dist/components/core/Table/ColumnMenu.d.ts.map +1 -1
  20. package/dist/components/core/Table/Pagination.d.ts.map +1 -1
  21. package/dist/components/core/Table/Table.d.ts.map +1 -1
  22. package/dist/components/core/Table/TableRow.d.ts.map +1 -1
  23. package/dist/components/core/Table/helpers.d.ts +2 -0
  24. package/dist/components/core/Table/helpers.d.ts.map +1 -1
  25. package/dist/components/core/Table/useClickOutside.d.ts +5 -2
  26. package/dist/components/core/Table/useClickOutside.d.ts.map +1 -1
  27. package/dist/components/core/Tabs.d.ts.map +1 -1
  28. package/dist/components/core/TextToSpeech.d.ts +2 -7
  29. package/dist/components/core/TextToSpeech.d.ts.map +1 -1
  30. package/dist/components/core/Tooltip.d.ts.map +1 -1
  31. package/dist/components/feedback/Alert.d.ts.map +1 -1
  32. package/dist/components/feedback/Dialog.d.ts.map +1 -1
  33. package/dist/components/feedback/Toast.d.ts.map +1 -1
  34. package/dist/components/navigation/Sidebar.d.ts.map +1 -1
  35. package/dist/components/utility/Avatar.d.ts.map +1 -1
  36. package/dist/components/utility/Carousel.d.ts.map +1 -1
  37. package/dist/components/utility/Portal.d.ts +6 -0
  38. package/dist/components/utility/Portal.d.ts.map +1 -0
  39. package/dist/esm/components/blocks/AppShell.js +24 -18
  40. package/dist/esm/components/charts/CartesianChart.js +2 -1
  41. package/dist/esm/components/charts/DonutChart.js +2 -1
  42. package/dist/esm/components/charts/Sparkline.js +2 -1
  43. package/dist/esm/components/charts/chartTheme.js +1 -22
  44. package/dist/esm/components/core/Accordion.js +40 -9
  45. package/dist/esm/components/core/Button.js +8 -8
  46. package/dist/esm/components/core/ColorPicker.js +19 -7
  47. package/dist/esm/components/core/DatePicker.js +97 -58
  48. package/dist/esm/components/core/Form.js +35 -8
  49. package/dist/esm/components/core/Input.js +3 -1
  50. package/dist/esm/components/core/PriceRangePicker.js +205 -218
  51. package/dist/esm/components/core/Radio.js +36 -6
  52. package/dist/esm/components/core/Rate.js +41 -50
  53. package/dist/esm/components/core/RoundedTag.js +25 -8
  54. package/dist/esm/components/core/Table/ColumnMenu.js +2 -4
  55. package/dist/esm/components/core/Table/Pagination.js +15 -18
  56. package/dist/esm/components/core/Table/Table.js +11 -5
  57. package/dist/esm/components/core/Table/TableRow.js +2 -2
  58. package/dist/esm/components/core/Table/helpers.js +33 -1
  59. package/dist/esm/components/core/Tabs.js +6 -5
  60. package/dist/esm/components/core/TextToSpeech.js +121 -28
  61. package/dist/esm/components/core/Tooltip.js +100 -28
  62. package/dist/esm/components/feedback/Alert.js +21 -11
  63. package/dist/esm/components/feedback/Dialog.js +44 -100
  64. package/dist/esm/components/feedback/Toast.js +24 -5
  65. package/dist/esm/components/navigation/Sidebar.js +13 -61
  66. package/dist/esm/components/utility/Avatar.js +23 -10
  67. package/dist/esm/components/utility/Carousel.js +34 -17
  68. package/dist/esm/components/utility/Pagination.js +2 -2
  69. package/dist/esm/hooks/useClickOutside.js +51 -0
  70. package/dist/esm/hooks/useFocusTrap.js +10 -2
  71. package/dist/esm/hooks/useMediaQuery.js +61 -0
  72. package/dist/esm/hooks/useReducedMotion.js +17 -0
  73. package/dist/esm/hooks/useScrollLock.js +76 -9
  74. package/dist/esm/hooks/useTheme.js +6 -1
  75. package/dist/esm/utils/position.js +6 -0
  76. package/dist/hooks/useClickOutside.d.ts +14 -0
  77. package/dist/hooks/useClickOutside.d.ts.map +1 -0
  78. package/dist/hooks/useFloatingPosition.d.ts +37 -0
  79. package/dist/hooks/useFloatingPosition.d.ts.map +1 -0
  80. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  81. package/dist/hooks/useMediaQuery.d.ts +22 -0
  82. package/dist/hooks/useMediaQuery.d.ts.map +1 -0
  83. package/dist/hooks/usePresence.d.ts +16 -0
  84. package/dist/hooks/usePresence.d.ts.map +1 -0
  85. package/dist/hooks/usePresentation.d.ts +9 -0
  86. package/dist/hooks/usePresentation.d.ts.map +1 -0
  87. package/dist/hooks/useReducedMotion.d.ts +10 -0
  88. package/dist/hooks/useReducedMotion.d.ts.map +1 -0
  89. package/dist/hooks/useScrollLock.d.ts +11 -1
  90. package/dist/hooks/useScrollLock.d.ts.map +1 -1
  91. package/dist/hooks/useTheme.d.ts.map +1 -1
  92. package/dist/index.css +1 -1
  93. package/dist/index.esm.css +1 -1
  94. package/dist/index.esm.js +1719 -1201
  95. package/dist/index.js +1718 -1200
  96. package/dist/tokens/tailwind3-preset.cjs +101 -8
  97. package/dist/tokens/theme.css +27 -0
  98. package/dist/tokens/tokens.json +175 -0
  99. package/dist/tokens/vars.css +2 -0
  100. package/dist/types/index.d.ts +8 -2
  101. package/dist/types/index.d.ts.map +1 -1
  102. package/dist/utils/position.d.ts.map +1 -1
  103. package/dist/utils/touchTarget.d.ts +1 -0
  104. package/dist/utils/touchTarget.d.ts.map +1 -1
  105. package/mcp/catalog.snapshot.json +83 -53
  106. package/package.json +2 -1
  107. package/tokens/lib.cjs +35 -0
  108. package/tokens/tokens.json +58 -2
  109. package/dist/esm/components/core/Table/useClickOutside.js +0 -26
@@ -1,38 +1,21 @@
1
1
  'use client';
2
2
  import { objectWithoutProperties as _objectWithoutProperties, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
3
  import { jsx, jsxs } from 'react/jsx-runtime';
4
- import React, { useRef, useEffect } from 'react';
4
+ import React, { useRef, useId, useEffect } from 'react';
5
5
  import { createPortal } from 'react-dom';
6
6
  import { cn } from '../../utils/cn.js';
7
7
  import { X } from 'lucide-react';
8
8
  import { useLatestRef } from '../../hooks/useLatestRef.js';
9
+ import { useFocusTrap } from '../../hooks/useFocusTrap.js';
10
+ import { useScrollLock } from '../../hooks/useScrollLock.js';
11
+ import { setRefs } from '../../utils/position.js';
9
12
  import { TOUCH_TARGET } from '../../utils/touchTarget.js';
10
13
 
11
14
  var _excluded = ["isOpen", "onClose", "title", "children", "size", "dismissible", "showCloseButton", "footer", "className", "onOpen"],
12
15
  _excluded2 = ["children", "className"],
13
16
  _excluded3 = ["children", "className"],
14
17
  _excluded4 = ["children", "className"];
15
- // Elements that can receive keyboard focus. Used to drive the focus trap and to
16
- // pick the first element to focus when the dialog opens (WAI-ARIA APG dialog).
17
- var FOCUSABLE_SELECTOR = ['a[href]', 'button:not([disabled])', 'textarea:not([disabled])', 'input:not([disabled])', 'select:not([disabled])', '[tabindex]:not([tabindex="-1"])'].join(',');
18
- var getFocusable = function getFocusable(root) {
19
- return root ? Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter(function (el) {
20
- return el.getAttribute('aria-hidden') !== 'true';
21
- }) : [];
22
- };
23
- // Assign one node to several refs (callback or object). Lets the dialog keep
24
- // its internal `dialogRef` for the focus trap while still exposing the panel
25
- // node to a consumer-supplied `ref` (issue #56).
26
- var setRefs = function setRefs(node) {
27
- for (var _len = arguments.length, refs = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
28
- refs[_key - 1] = arguments[_key];
29
- }
30
- refs.forEach(function (ref) {
31
- if (!ref) return;
32
- if (typeof ref === 'function') ref(node);else ref.current = node;
33
- });
34
- };
35
- var Dialog = /*#__PURE__*/React.forwardRef(function (_ref, _ref3) {
18
+ var Dialog = /*#__PURE__*/React.forwardRef(function (_ref, _ref2) {
36
19
  var isOpen = _ref.isOpen,
37
20
  onClose = _ref.onClose,
38
21
  title = _ref.title,
@@ -48,66 +31,27 @@ var Dialog = /*#__PURE__*/React.forwardRef(function (_ref, _ref3) {
48
31
  onOpen = _ref.onOpen,
49
32
  props = _objectWithoutProperties(_ref, _excluded);
50
33
  var dialogRef = useRef(null);
51
- // The element that had focus when the dialog opened, so we can restore it on close.
52
- var lastFocusedRef = useRef(null);
34
+ // Unique per instance so two open dialogs never share an aria-labelledby target.
35
+ var titleId = "dialog-title-".concat(useId());
53
36
  // Latest onOpen without re-running the open effect when the prop identity changes.
54
37
  var onOpenRef = useLatestRef(onOpen);
38
+ // Ref-counted body lock: closing a Dialog stacked over a Drawer (or another
39
+ // overlay) leaves the page locked until the last overlay closes.
40
+ useScrollLock(isOpen);
41
+ // Shared focus management: initial focus, Tab trap, Escape (top layer only),
42
+ // and focus restore to the opener on close.
43
+ useFocusTrap(dialogRef, {
44
+ active: isOpen,
45
+ onEscape: function onEscape() {
46
+ if (dismissible) onClose === null || onClose === void 0 || onClose();
47
+ }
48
+ });
55
49
  useEffect(function () {
56
- var _ref2, _focusables$, _onOpenRef$current;
57
- if (!isOpen) return undefined;
58
- // Remember the opener so focus can return to it when the dialog closes.
59
- lastFocusedRef.current = document.activeElement;
60
- document.body.style.overflow = 'hidden';
61
- // Move focus into the panel: first focusable descendant, else the panel itself.
62
- var panel = dialogRef.current;
63
- var focusables = getFocusable(panel);
64
- (_ref2 = (_focusables$ = focusables[0]) !== null && _focusables$ !== void 0 ? _focusables$ : panel) === null || _ref2 === void 0 || _ref2.focus();
65
- (_onOpenRef$current = onOpenRef.current) === null || _onOpenRef$current === void 0 || _onOpenRef$current.call(onOpenRef, {
66
- dialogRef: panel
50
+ var _onOpenRef$current;
51
+ if (isOpen) (_onOpenRef$current = onOpenRef.current) === null || _onOpenRef$current === void 0 || _onOpenRef$current.call(onOpenRef, {
52
+ dialogRef: dialogRef.current
67
53
  });
68
- return function () {
69
- var _lastFocusedRef$curre, _lastFocusedRef$curre2;
70
- document.body.style.overflow = 'unset';
71
- // Restore focus to whatever opened the dialog (APG focus-restore requirement).
72
- (_lastFocusedRef$curre = lastFocusedRef.current) === null || _lastFocusedRef$curre === void 0 || (_lastFocusedRef$curre2 = _lastFocusedRef$curre.focus) === null || _lastFocusedRef$curre2 === void 0 || _lastFocusedRef$curre2.call(_lastFocusedRef$curre);
73
- };
74
54
  }, [isOpen, onOpenRef]);
75
- // Escape-to-close + Tab focus trap. An aria-modal element must keep keyboard
76
- // focus inside the panel; without this, Tab walks into the page behind it.
77
- useEffect(function () {
78
- if (!isOpen) return undefined;
79
- var handleKeyDown = function handleKeyDown(e) {
80
- if (e.key === 'Escape') {
81
- if (dismissible) onClose === null || onClose === void 0 || onClose();
82
- return;
83
- }
84
- if (e.key !== 'Tab') return;
85
- var panel = dialogRef.current;
86
- if (!panel) return;
87
- var focusables = getFocusable(panel);
88
- if (focusables.length === 0) {
89
- // Nothing focusable inside — pin focus to the panel.
90
- e.preventDefault();
91
- panel.focus();
92
- return;
93
- }
94
- var first = focusables[0];
95
- var last = focusables[focusables.length - 1];
96
- var active = document.activeElement;
97
- var outside = !panel.contains(active) || active === panel;
98
- if (e.shiftKey && (active === first || outside)) {
99
- e.preventDefault();
100
- last.focus();
101
- } else if (!e.shiftKey && (active === last || outside)) {
102
- e.preventDefault();
103
- first.focus();
104
- }
105
- };
106
- document.addEventListener('keydown', handleKeyDown);
107
- return function () {
108
- return document.removeEventListener('keydown', handleKeyDown);
109
- };
110
- }, [isOpen, dismissible, onClose]);
111
55
  if (!isOpen) return null;
112
56
  var sizeClasses = {
113
57
  sm: 'max-w-md',
@@ -122,27 +66,27 @@ var Dialog = /*#__PURE__*/React.forwardRef(function (_ref, _ref3) {
122
66
  }
123
67
  };
124
68
  return /*#__PURE__*/createPortal(jsx("div", {
125
- className: "fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4",
69
+ className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4",
126
70
  onClick: handleBackdropClick,
127
71
  children: jsxs("div", _objectSpread2(_objectSpread2({
128
72
  ref: function ref(node) {
129
- return setRefs(node, dialogRef, _ref3);
73
+ return setRefs(node, dialogRef, _ref2);
130
74
  },
131
- className: cn('w-full rounded-lg bg-white shadow-xl animate-scale-in motion-reduce:animate-none', sizeClasses[size], className),
75
+ className: cn('w-full rounded-lg border border-line bg-surface text-foreground shadow-xl focus:outline-none animate-scale-in motion-reduce:animate-none', sizeClasses[size], className),
132
76
  tabIndex: -1,
133
77
  role: "dialog",
134
78
  "aria-modal": "true",
135
- "aria-labelledby": title ? 'dialog-title' : undefined
79
+ "aria-labelledby": title ? titleId : undefined
136
80
  }, props), {}, {
137
81
  children: [(title || showCloseButton) && jsxs("div", {
138
- className: "flex items-center justify-between border-b border-gray-200 px-6 py-4",
82
+ className: "flex items-center justify-between border-b border-line px-6 py-4",
139
83
  children: [title && jsx("h2", {
140
- id: "dialog-title",
141
- className: "text-lg font-semibold text-primary-900",
84
+ id: titleId,
85
+ className: "text-lg font-semibold text-foreground",
142
86
  children: title
143
87
  }), showCloseButton && jsx("button", {
144
88
  type: "button",
145
- className: cn('inline-flex items-center justify-center rounded-md p-1 text-primary-400 hover:text-primary-600 hover:bg-gray-100', TOUCH_TARGET),
89
+ className: cn('inline-flex items-center justify-center rounded-md p-1 text-muted hover:bg-gray-100 hover:text-foreground dark:hover:bg-gray-800', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500', TOUCH_TARGET),
146
90
  onClick: onClose,
147
91
  "aria-label": "Close dialog",
148
92
  children: jsx(X, {
@@ -154,7 +98,7 @@ var Dialog = /*#__PURE__*/React.forwardRef(function (_ref, _ref3) {
154
98
  className: "px-6 py-4",
155
99
  children: children
156
100
  }), footer && jsx("div", {
157
- className: "flex items-center justify-end gap-3 border-t border-gray-200 px-6 py-4",
101
+ className: "flex items-center justify-end gap-3 border-t border-line px-6 py-4",
158
102
  children: footer
159
103
  })]
160
104
  }))
@@ -162,32 +106,32 @@ var Dialog = /*#__PURE__*/React.forwardRef(function (_ref, _ref3) {
162
106
  });
163
107
  Dialog.displayName = 'Dialog';
164
108
  // Dialog components for composition
165
- var DialogHeader = function DialogHeader(_ref4) {
166
- var children = _ref4.children,
167
- className = _ref4.className,
168
- props = _objectWithoutProperties(_ref4, _excluded2);
109
+ var DialogHeader = function DialogHeader(_ref3) {
110
+ var children = _ref3.children,
111
+ className = _ref3.className,
112
+ props = _objectWithoutProperties(_ref3, _excluded2);
169
113
  return jsx("div", _objectSpread2(_objectSpread2({
170
- className: cn('border-b border-gray-200 px-6 py-4', className)
114
+ className: cn('border-b border-line px-6 py-4', className)
171
115
  }, props), {}, {
172
116
  children: children
173
117
  }));
174
118
  };
175
- var DialogBody = function DialogBody(_ref5) {
176
- var children = _ref5.children,
177
- className = _ref5.className,
178
- props = _objectWithoutProperties(_ref5, _excluded3);
119
+ var DialogBody = function DialogBody(_ref4) {
120
+ var children = _ref4.children,
121
+ className = _ref4.className,
122
+ props = _objectWithoutProperties(_ref4, _excluded3);
179
123
  return jsx("div", _objectSpread2(_objectSpread2({
180
124
  className: cn('px-6 py-4', className)
181
125
  }, props), {}, {
182
126
  children: children
183
127
  }));
184
128
  };
185
- var DialogFooter = function DialogFooter(_ref6) {
186
- var children = _ref6.children,
187
- className = _ref6.className,
188
- props = _objectWithoutProperties(_ref6, _excluded4);
129
+ var DialogFooter = function DialogFooter(_ref5) {
130
+ var children = _ref5.children,
131
+ className = _ref5.className,
132
+ props = _objectWithoutProperties(_ref5, _excluded4);
189
133
  return jsx("div", _objectSpread2(_objectSpread2({
190
- className: cn('flex items-center justify-end gap-3 border-t border-gray-200 px-6 py-4', className)
134
+ className: cn('flex items-center justify-end gap-3 border-t border-line px-6 py-4', className)
191
135
  }, props), {}, {
192
136
  children: children
193
137
  }));
@@ -5,8 +5,23 @@ import { forwardRef, useState, useRef, useCallback, useEffect, useContext, creat
5
5
  import { createPortal } from 'react-dom';
6
6
  import { cn } from '../../utils/cn.js';
7
7
  import { Bell, Info, AlertTriangle, XCircle, CheckCircle2, X } from 'lucide-react';
8
+ import { TOUCH_TARGET } from '../../utils/touchTarget.js';
8
9
 
9
10
  var _excluded = ["id", "title", "message", "description", "type", "duration", "onClose", "position", "statusIcon", "actions", "color", "footerNote", "showFooterNote", "portal", "className"];
11
+ // Errors interrupt (role="alert", assertive); everything else waits for the
12
+ // screen reader to finish (role="status", polite). Consumers can still pass
13
+ // their own role / aria-live, which win because ...props spreads last.
14
+ var liveRegionProps = function liveRegionProps(type) {
15
+ return type === 'error' ? {
16
+ role: 'alert',
17
+ 'aria-live': 'assertive',
18
+ 'aria-atomic': 'true'
19
+ } : {
20
+ role: 'status',
21
+ 'aria-live': 'polite',
22
+ 'aria-atomic': 'true'
23
+ };
24
+ };
10
25
  var STATUS_CONFIG = {
11
26
  success: {
12
27
  icon: CheckCircle2,
@@ -147,7 +162,7 @@ var Toast = /*#__PURE__*/forwardRef(function (_ref, ref) {
147
162
  var statusStyle = STATUS_CONFIG[type] || STATUS_CONFIG.info || STATUS_CONFIG["default"];
148
163
  var StatusIconComponent = statusStyle.icon;
149
164
  var bodyText = description || message;
150
- var toastContent = jsxs("div", _objectSpread2(_objectSpread2({
165
+ var toastContent = jsxs("div", _objectSpread2(_objectSpread2(_objectSpread2({
151
166
  ref: ref,
152
167
  onMouseEnter: function onMouseEnter() {
153
168
  return setIsPaused(true);
@@ -155,14 +170,15 @@ var Toast = /*#__PURE__*/forwardRef(function (_ref, ref) {
155
170
  onMouseLeave: function onMouseLeave() {
156
171
  return setIsPaused(false);
157
172
  },
158
- className: cn('relative overflow-hidden rounded-xl border border-slate-200/90 bg-white/95 shadow-xl backdrop-blur-md transition-all duration-300', 'dark:border-slate-800/80 dark:bg-[#18181b] dark:text-slate-100 dark:shadow-2xl', 'max-w-md w-full', isLeaving ? 'opacity-0 scale-95 translate-y-1' : 'animate-slide-up opacity-100 scale-100', portal ? ['fixed z-50', STANDALONE_POSITION_CLASSES[position] || STANDALONE_POSITION_CLASSES['top-right']] : '', className)
159
- }, props), {}, {
173
+ className: cn('relative overflow-hidden rounded-xl border border-slate-200/90 bg-white/95 shadow-xl backdrop-blur-md transition-all duration-300', 'dark:border-slate-800/80 dark:bg-[#18181b] dark:text-slate-100 dark:shadow-2xl', 'max-w-md w-full motion-reduce:transition-none', isLeaving ? 'opacity-0 scale-95 translate-y-1' : 'animate-slide-up motion-reduce:animate-none opacity-100 scale-100', portal ? ['fixed z-50', STANDALONE_POSITION_CLASSES[position] || STANDALONE_POSITION_CLASSES['top-right']] : '', className)
174
+ }, liveRegionProps(type)), props), {}, {
160
175
  children: [jsx("div", {
161
176
  className: "p-4",
162
177
  children: jsxs("div", {
163
178
  className: "flex items-start gap-3 justify-between",
164
179
  children: [jsx("span", {
165
180
  className: "flex-shrink-0 mt-0.5",
181
+ "aria-hidden": "true",
166
182
  children: statusIcon ? statusIcon : jsx(StatusIconComponent, {
167
183
  className: cn('h-5 w-5', statusStyle.iconClass)
168
184
  })
@@ -194,10 +210,11 @@ var Toast = /*#__PURE__*/forwardRef(function (_ref, ref) {
194
210
  }), jsx("button", {
195
211
  type: "button",
196
212
  "aria-label": "Close notification",
197
- className: "flex-shrink-0 rounded-lg p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-zinc-800/70 transition-colors outline-none focus:ring-2 focus:ring-primary-500",
213
+ className: cn('inline-flex flex-shrink-0 items-center justify-center rounded-lg p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-zinc-800/70 transition-colors motion-reduce:transition-none outline-none focus-visible:ring-2 focus-visible:ring-primary-500', TOUCH_TARGET),
198
214
  onClick: handleClose,
199
215
  children: jsx(X, {
200
- className: "h-4 w-4"
216
+ className: "h-4 w-4",
217
+ "aria-hidden": "true"
201
218
  })
202
219
  })]
203
220
  })
@@ -250,6 +267,8 @@ var ToastStack = function ToastStack(_ref2) {
250
267
  }
251
268
  var posClass = POSITION_CLASSES[position] || POSITION_CLASSES['top-right'];
252
269
  return /*#__PURE__*/createPortal(jsx("div", {
270
+ role: "region",
271
+ "aria-label": "Notifications",
253
272
  className: cn('fixed z-50 flex flex-col gap-3 max-h-[calc(100vh-2rem)] overflow-y-auto overflow-x-hidden p-2 pointer-events-none', '[scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden', posClass, className),
254
273
  children: toasts.map(function (t) {
255
274
  return jsx("div", {
@@ -6,6 +6,8 @@ import { cn } from '../../utils/cn.js';
6
6
  import { ChevronRight, ChevronLeft, ChevronDown } from 'lucide-react';
7
7
  import Tooltip from '../core/Tooltip.js';
8
8
  import { useLatestRef } from '../../hooks/useLatestRef.js';
9
+ import { useFocusTrap } from '../../hooks/useFocusTrap.js';
10
+ import { useScrollLock } from '../../hooks/useScrollLock.js';
9
11
 
10
12
  var _excluded = ["items", "collapsed", "onToggle", "className", "containerClassName", "stickyOffset", "logo", "footer", "user", "onUserClick", "drawerPosition", "isMobileOpen", "setIsMobileOpen", "showCollapsedTooltips", "tooltipOptions", "linkAs", "expandedIds", "defaultExpandedIds", "onExpandedChange"];
11
13
  // useLayoutEffect warns during SSR; fall back to useEffect on the server.
@@ -15,13 +17,6 @@ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect
15
17
  var isModifiedClick = function isModifiedClick(e) {
16
18
  return Boolean(e && (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || typeof e.button === "number" && e.button !== 0));
17
19
  };
18
- // Focus trap for the mobile drawer (same selector as feedback/Dialog).
19
- var FOCUSABLE_SELECTOR = ["a[href]", "button:not([disabled])", "textarea:not([disabled])", "input:not([disabled])", "select:not([disabled])", '[tabindex]:not([tabindex="-1"])'].join(",");
20
- var getFocusable = function getFocusable(root) {
21
- return root ? Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter(function (el) {
22
- return el.getAttribute("aria-hidden") !== "true";
23
- }) : [];
24
- };
25
20
  var hasChildItems = function hasChildItems(item) {
26
21
  return Array.isArray(item.children) && item.children.length > 0;
27
22
  };
@@ -171,12 +166,8 @@ var Sidebar = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
171
166
  var setIsMobileOpenRef = useLatestRef(setIsMobileOpen);
172
167
  var mobileOpen = isMobileOpen !== null && isMobileOpen !== void 0 ? isMobileOpen : false;
173
168
  var effectiveCollapsed = mobileOpen ? false : collapsed;
174
- useEffect(function () {
175
- if (mobileOpen) document.body.style.overflow = "hidden";else document.body.style.overflow = "";
176
- return function () {
177
- document.body.style.overflow = "";
178
- };
179
- }, [mobileOpen]);
169
+ // Ref-counted, so a Dialog/Drawer stacked on top never unlocks the page early.
170
+ useScrollLock(mobileOpen);
180
171
  // The drawer stays mounted (it slides in/out), so when it is shut it must
181
172
  // leave the tab order and the a11y tree. `inert` is set imperatively: React
182
173
  // 18 does not know the attribute and React 19 treats `inert=""` as false.
@@ -185,54 +176,15 @@ var Sidebar = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
185
176
  if (!drawer) return;
186
177
  if (mobileOpen) drawer.removeAttribute("inert");else drawer.setAttribute("inert", "");
187
178
  }, [mobileOpen]);
188
- // Open drawer = modal dialog: initial focus inside, Tab trapped, Escape
189
- // closes, focus returns to the opener on close (WAI-ARIA APG dialog).
190
- useEffect(function () {
191
- var _ref2, _ref2$focus, _focusables$;
192
- if (!mobileOpen) return undefined;
193
- var drawer = drawerRef.current;
194
- var previouslyFocused = document.activeElement;
195
- var focusables = getFocusable(drawer);
196
- (_ref2 = (_focusables$ = focusables[0]) !== null && _focusables$ !== void 0 ? _focusables$ : drawer) === null || _ref2 === void 0 || (_ref2$focus = _ref2.focus) === null || _ref2$focus === void 0 || _ref2$focus.call(_ref2);
197
- var handleKeyDown = function handleKeyDown(e) {
198
- if (e.key === "Escape") {
199
- var _setIsMobileOpenRef$c;
200
- (_setIsMobileOpenRef$c = setIsMobileOpenRef.current) === null || _setIsMobileOpenRef$c === void 0 || _setIsMobileOpenRef$c.call(setIsMobileOpenRef, false);
201
- return;
202
- }
203
- if (e.key !== "Tab" || !drawer) return;
204
- // md+ viewports hide the drawer (md:hidden): nothing to trap.
205
- if (window.getComputedStyle(drawer).display === "none") return;
206
- var nodes = getFocusable(drawer);
207
- if (nodes.length === 0) {
208
- e.preventDefault();
209
- drawer.focus();
210
- return;
211
- }
212
- var first = nodes[0];
213
- var last = nodes[nodes.length - 1];
214
- var active = document.activeElement;
215
- var outside = !drawer.contains(active) || active === drawer;
216
- if (e.shiftKey && (active === first || outside)) {
217
- e.preventDefault();
218
- last.focus();
219
- } else if (!e.shiftKey && (active === last || outside)) {
220
- e.preventDefault();
221
- first.focus();
222
- }
223
- };
224
- document.addEventListener("keydown", handleKeyDown);
225
- return function () {
226
- document.removeEventListener("keydown", handleKeyDown);
227
- // Restore focus only if it is still in the drawer (or was dropped to
228
- // <body> when the drawer went inert) — never steal it from elsewhere.
229
- var active = document.activeElement;
230
- var focusLost = !active || active === document.body || (drawer === null || drawer === void 0 ? void 0 : drawer.contains(active));
231
- if (focusLost && previouslyFocused && previouslyFocused !== document.body && typeof previouslyFocused.focus === "function" && document.contains(previouslyFocused)) {
232
- previouslyFocused.focus();
233
- }
234
- };
235
- }, [mobileOpen, setIsMobileOpenRef]);
179
+ // Open drawer = modal dialog: initial focus inside, Tab trapped (skipped
180
+ // while md:hidden hides it), Escape closes, focus returns to the opener.
181
+ useFocusTrap(drawerRef, {
182
+ active: mobileOpen,
183
+ onEscape: function onEscape() {
184
+ var _setIsMobileOpenRef$c;
185
+ return (_setIsMobileOpenRef$c = setIsMobileOpenRef.current) === null || _setIsMobileOpenRef$c === void 0 ? void 0 : _setIsMobileOpenRef$c.call(setIsMobileOpenRef, false);
186
+ }
187
+ });
236
188
  var closeMobile = function closeMobile() {
237
189
  if (mobileOpen && setIsMobileOpen) setIsMobileOpen(false);
238
190
  };
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { objectWithoutProperties as _objectWithoutProperties, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
2
+ import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
3
  import { jsxs, jsx } from 'react/jsx-runtime';
4
4
  import React from 'react';
5
5
  import { cn } from '../../utils/cn.js';
@@ -17,6 +17,22 @@ var Avatar = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
17
17
  status = _ref.status,
18
18
  className = _ref.className,
19
19
  props = _objectWithoutProperties(_ref, _excluded);
20
+ // State-driven fallback: a broken image swaps to initials / icon without
21
+ // touching the DOM directly. Reset whenever `src` changes
22
+ // (adjust-during-render pattern).
23
+ var _React$useState = React.useState(false),
24
+ _React$useState2 = _slicedToArray(_React$useState, 2),
25
+ imgFailed = _React$useState2[0],
26
+ setImgFailed = _React$useState2[1];
27
+ var _React$useState3 = React.useState(src),
28
+ _React$useState4 = _slicedToArray(_React$useState3, 2),
29
+ prevSrc = _React$useState4[0],
30
+ setPrevSrc = _React$useState4[1];
31
+ if (src !== prevSrc) {
32
+ setPrevSrc(src);
33
+ setImgFailed(false);
34
+ }
35
+ var showImage = Boolean(src) && !imgFailed;
20
36
  var sizes = {
21
37
  xs: 'h-6 w-6 text-xs',
22
38
  sm: 'h-8 w-8 text-sm',
@@ -48,14 +64,14 @@ var Avatar = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
48
64
  return jsxs("div", {
49
65
  ref: ref,
50
66
  className: "relative inline-block",
51
- children: [jsxs("div", _objectSpread2(_objectSpread2({
67
+ children: [jsx("div", _objectSpread2(_objectSpread2({
52
68
  className: cn('flex items-center justify-center bg-gray-100 text-primary-600 font-medium', sizes[size], {
53
69
  'rounded-full': shape === 'circle',
54
70
  'rounded-md': shape === 'square',
55
71
  'rounded-lg': shape === 'rounded'
56
72
  }, className)
57
73
  }, props), {}, {
58
- children: [src ? jsx("img", {
74
+ children: showImage ? jsx("img", {
59
75
  src: src,
60
76
  alt: alt || name,
61
77
  className: cn('h-full w-full object-cover', {
@@ -63,22 +79,19 @@ var Avatar = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
63
79
  'rounded-md': shape === 'square',
64
80
  'rounded-lg': shape === 'rounded'
65
81
  }),
66
- onError: function onError(e) {
67
- e.target.style.display = 'none';
68
- e.target.nextSibling.style.display = 'flex';
82
+ onError: function onError() {
83
+ return setImgFailed(true);
69
84
  }
70
- }) : null, jsx("div", {
85
+ }) : jsx("div", {
71
86
  className: cn('flex items-center justify-center h-full w-full', {
72
87
  'rounded-full': shape === 'circle',
73
88
  'rounded-md': shape === 'square',
74
89
  'rounded-lg': shape === 'rounded'
75
- }, {
76
- 'hidden': src
77
90
  }),
78
91
  children: name ? getInitials(name) : jsx(User, {
79
92
  className: "h-1/2 w-1/2"
80
93
  })
81
- })]
94
+ })
82
95
  })), status && jsx("span", {
83
96
  className: cn('absolute bottom-0 right-0 rounded-full border-2 border-white', statusColors[status], statusSizes[size])
84
97
  })]
@@ -1,10 +1,15 @@
1
1
  'use client';
2
- import { slicedToArray as _slicedToArray } from '../../_virtual/_rollupPluginBabelHelpers.js';
2
+ import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
3
  import { jsxs, jsx } from 'react/jsx-runtime';
4
4
  import React from 'react';
5
5
  import { ChevronLeft, ChevronRight } from 'lucide-react';
6
6
  import { cn } from '../../utils/cn.js';
7
+ import { TOUCH_TARGET } from '../../utils/touchTarget.js';
7
8
 
9
+ var _excluded = ["children", "autoPlay", "interval", "showDots", "className", "itemClassName", "onMouseEnter", "onMouseLeave"];
10
+ // Arrows stay visible at rest (touch devices have no hover) and use the surface /
11
+ // foreground role tokens so they read on light and dark themes alike.
12
+ var ARROW_CLASSES = "absolute top-1/2 -translate-y-1/2 z-10 inline-flex items-center justify-center p-2 rounded-full bg-surface/80 backdrop-blur-md border border-line text-foreground shadow-lg transition-colors motion-reduce:transition-none hover:bg-surface focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500";
8
13
  var Carousel = /*#__PURE__*/React.forwardRef(function Carousel(props, ref) {
9
14
  var children = props.children,
10
15
  _props$autoPlay = props.autoPlay,
@@ -14,7 +19,10 @@ var Carousel = /*#__PURE__*/React.forwardRef(function Carousel(props, ref) {
14
19
  _props$showDots = props.showDots,
15
20
  showDots = _props$showDots === void 0 ? true : _props$showDots,
16
21
  className = props.className,
17
- itemClassName = props.itemClassName;
22
+ itemClassName = props.itemClassName,
23
+ _onMouseEnter = props.onMouseEnter,
24
+ _onMouseLeave = props.onMouseLeave,
25
+ rest = _objectWithoutProperties(props, _excluded);
18
26
  var _React$useState = React.useState(0),
19
27
  _React$useState2 = _slicedToArray(_React$useState, 2),
20
28
  currentIndex = _React$useState2[0],
@@ -44,17 +52,20 @@ var Carousel = /*#__PURE__*/React.forwardRef(function Carousel(props, ref) {
44
52
  };
45
53
  }, [autoPlay, isPaused, next, interval]);
46
54
  var childrenArray = React.Children.toArray(children);
47
- return jsxs("div", {
55
+ return jsxs("div", _objectSpread2(_objectSpread2({
48
56
  ref: ref,
49
- className: cn("relative group overflow-hidden rounded-xl bg-card border border-border/50 shadow-sm", className),
50
- onMouseEnter: function onMouseEnter() {
51
- return setIsPaused(true);
52
- },
53
- onMouseLeave: function onMouseLeave() {
54
- return setIsPaused(false);
57
+ className: cn("relative overflow-hidden rounded-xl bg-surface border border-line shadow-sm", className),
58
+ onMouseEnter: function onMouseEnter(e) {
59
+ setIsPaused(true);
60
+ _onMouseEnter === null || _onMouseEnter === void 0 || _onMouseEnter(e);
55
61
  },
62
+ onMouseLeave: function onMouseLeave(e) {
63
+ setIsPaused(false);
64
+ _onMouseLeave === null || _onMouseLeave === void 0 || _onMouseLeave(e);
65
+ }
66
+ }, rest), {}, {
56
67
  children: [jsx("div", {
57
- className: "flex transition-transform duration-500 ease-out",
68
+ className: "flex transition-transform duration-500 ease-out motion-reduce:transition-none",
58
69
  style: {
59
70
  transform: "translateX(-".concat(currentIndex * 100, "%)")
60
71
  },
@@ -65,32 +76,38 @@ var Carousel = /*#__PURE__*/React.forwardRef(function Carousel(props, ref) {
65
76
  }, index);
66
77
  })
67
78
  }), jsx("button", {
79
+ type: "button",
68
80
  onClick: prev,
69
- className: "absolute left-4 top-1/2 -translate-y-1/2 z-10 p-2 rounded-full bg-background/80 backdrop-blur-md border border-border/50 text-foreground opacity-0 group-hover:opacity-100 transition-all hover:bg-background shadow-lg focus:outline-none focus:ring-2 focus:ring-primary",
81
+ className: cn(ARROW_CLASSES, TOUCH_TARGET, "left-4"),
70
82
  "aria-label": "Previous slide",
71
83
  children: jsx(ChevronLeft, {
72
- className: "w-5 h-5 text-white"
84
+ className: "w-5 h-5",
85
+ "aria-hidden": "true"
73
86
  })
74
87
  }), jsx("button", {
88
+ type: "button",
75
89
  onClick: next,
76
- className: "absolute right-4 top-1/2 -translate-y-1/2 z-10 p-2 rounded-full bg-background/80 backdrop-blur-md border border-border/50 text-foreground opacity-0 group-hover:opacity-100 transition-all hover:bg-background shadow-lg focus:outline-none focus:ring-2 focus:ring-primary",
90
+ className: cn(ARROW_CLASSES, TOUCH_TARGET, "right-4"),
77
91
  "aria-label": "Next slide",
78
92
  children: jsx(ChevronRight, {
79
- className: "w-5 h-5 text-white"
93
+ className: "w-5 h-5",
94
+ "aria-hidden": "true"
80
95
  })
81
96
  }), showDots && jsx("div", {
82
97
  className: "absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2 z-10",
83
98
  children: childrenArray.map(function (_, index) {
84
99
  return jsx("button", {
100
+ type: "button",
85
101
  onClick: function onClick() {
86
102
  return setCurrentIndex(index);
87
103
  },
88
- className: cn("w-2 h-2 rounded-full transition-all duration-300", currentIndex === index ? "bg-primary w-6" : "bg-muted-foreground/30 hover:bg-muted-foreground/50"),
89
- "aria-label": "Go to slide ".concat(index + 1)
104
+ className: cn("h-2 rounded-full transition-all duration-300 motion-reduce:transition-none focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2", currentIndex === index ? "bg-primary-600 w-6" : "w-2 bg-muted/40 hover:bg-muted/70"),
105
+ "aria-label": "Go to slide ".concat(index + 1),
106
+ "aria-current": currentIndex === index ? "true" : undefined
90
107
  }, index);
91
108
  })
92
109
  })]
93
- });
110
+ }));
94
111
  });
95
112
  Carousel.displayName = "Carousel";
96
113
 
@@ -50,7 +50,7 @@ var Pagination = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
50
50
  className: cn('flex items-center justify-center space-x-1', className),
51
51
  "aria-label": "Pagination"
52
52
  }, props), {}, {
53
- children: [showFirstLast && currentPage > 1 && jsx("button", {
53
+ children: [showFirstLast && visiblePages[0] > 1 && jsx("button", {
54
54
  className: "rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2",
55
55
  onClick: function onClick() {
56
56
  return handlePageClick(1);
@@ -96,7 +96,7 @@ var Pagination = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
96
96
  children: jsx(ChevronRight, {
97
97
  className: "h-4 w-4"
98
98
  })
99
- }), showFirstLast && currentPage < totalPages && jsx("button", {
99
+ }), showFirstLast && visiblePages[visiblePages.length - 1] < totalPages && jsx("button", {
100
100
  className: "rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2",
101
101
  onClick: function onClick() {
102
102
  return handlePageClick(totalPages);
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+ import { useEffect } from 'react';
3
+ import { useLatestRef } from './useLatestRef.js';
4
+
5
+ // Internal (not exported). Calls `onClickOutside` when a press lands outside
6
+ // every element in `refs`.
7
+ /** Events to listen to: `pointerdown` where supported, else mouse + touch. */
8
+ function outsidePressEvents() {
9
+ if (typeof window !== "undefined" && typeof window.PointerEvent === "function") {
10
+ return ["pointerdown"];
11
+ }
12
+ return ["mousedown", "touchstart"];
13
+ }
14
+ /**
15
+ * @param {React.RefObject|React.RefObject[]} refs - one ref or an array of
16
+ * refs; a press inside any of them is "inside". Refs with no node are ignored.
17
+ * @param {(event: Event) => void} onClickOutside - called with the event.
18
+ * @param {{ enabled?: boolean }} [options] - `enabled` (default true) attaches
19
+ * or detaches the listeners.
20
+ */
21
+ function useClickOutside(refs, onClickOutside) {
22
+ var _ref = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
23
+ _ref$enabled = _ref.enabled,
24
+ enabled = _ref$enabled === void 0 ? true : _ref$enabled;
25
+ var refsRef = useLatestRef(refs);
26
+ var handlerRef = useLatestRef(onClickOutside);
27
+ useEffect(function () {
28
+ if (!enabled || typeof document === "undefined") return undefined;
29
+ var handler = function handler(event) {
30
+ var _handlerRef$current;
31
+ var current = refsRef.current;
32
+ var list = Array.isArray(current) ? current : [current];
33
+ var inside = list.some(function (ref) {
34
+ var node = ref === null || ref === void 0 ? void 0 : ref.current;
35
+ return Boolean(node && typeof node.contains === "function" && node.contains(event.target));
36
+ });
37
+ if (!inside) (_handlerRef$current = handlerRef.current) === null || _handlerRef$current === void 0 || _handlerRef$current.call(handlerRef, event);
38
+ };
39
+ var events = outsidePressEvents();
40
+ events.forEach(function (type) {
41
+ return document.addEventListener(type, handler, true);
42
+ });
43
+ return function () {
44
+ return events.forEach(function (type) {
45
+ return document.removeEventListener(type, handler, true);
46
+ });
47
+ };
48
+ }, [enabled, refsRef, handlerRef]);
49
+ }
50
+
51
+ export { outsidePressEvents, useClickOutside };