@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,9 +1,19 @@
1
1
  'use client';
2
- import { slicedToArray as _slicedToArray } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
- import { jsx } from 'react/jsx-runtime';
4
- import { useState } from 'react';
2
+ import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+ import { forwardRef, useState, useRef, useEffect, useCallback } from 'react';
5
+ import { Square, Volume2 } from 'lucide-react';
6
+ import { cn } from '../../utils/cn.js';
5
7
 
6
- var TextToSpeech = function TextToSpeech(_ref) {
8
+ var _excluded = ["text", "rate", "pitch", "onSpeak", "onError", "renderButton", "disabled", "className"];
9
+ var UNSUPPORTED_MESSAGE = "Text-to-speech is not supported in this browser";
10
+ // `interrupted` / `canceled` fire when playback is stopped on purpose (our own
11
+ // cancel(), or another utterance taking over) — not failures worth surfacing.
12
+ var NON_FATAL_ERRORS = new Set(["interrupted", "canceled"]);
13
+ var detectSupport = function detectSupport() {
14
+ return typeof window !== "undefined" && "speechSynthesis" in window && !!window.speechSynthesis && typeof window.SpeechSynthesisUtterance !== "undefined";
15
+ };
16
+ var TextToSpeech = /*#__PURE__*/forwardRef(function (_ref, ref) {
7
17
  var _ref$text = _ref.text,
8
18
  text = _ref$text === void 0 ? "" : _ref$text,
9
19
  _ref$rate = _ref.rate,
@@ -12,41 +22,124 @@ var TextToSpeech = function TextToSpeech(_ref) {
12
22
  pitch = _ref$pitch === void 0 ? 0.5 : _ref$pitch,
13
23
  _ref$onSpeak = _ref.onSpeak,
14
24
  onSpeak = _ref$onSpeak === void 0 ? function () {} : _ref$onSpeak,
15
- renderButton = _ref.renderButton;
16
- var _useState = useState(text),
17
- _useState2 = _slicedToArray(_useState, 1),
18
- inputText = _useState2[0];
19
- var _useState3 = useState(false),
25
+ _ref$onError = _ref.onError,
26
+ onError = _ref$onError === void 0 ? function () {} : _ref$onError,
27
+ renderButton = _ref.renderButton,
28
+ _ref$disabled = _ref.disabled,
29
+ disabled = _ref$disabled === void 0 ? false : _ref$disabled,
30
+ className = _ref.className,
31
+ rest = _objectWithoutProperties(_ref, _excluded);
32
+ var _useState = useState(false),
33
+ _useState2 = _slicedToArray(_useState, 2),
34
+ isSpeaking = _useState2[0],
35
+ setIsSpeaking = _useState2[1];
36
+ var _useState3 = useState(true),
20
37
  _useState4 = _slicedToArray(_useState3, 2),
21
- isSpeaking = _useState4[0],
22
- setIsSpeaking = _useState4[1];
23
- var handleSpeak = function handleSpeak() {
24
- var utterance = new SpeechSynthesisUtterance(inputText);
38
+ isSupported = _useState4[0],
39
+ setIsSupported = _useState4[1];
40
+ var isMountedRef = useRef(true);
41
+ var utteranceRef = useRef(null);
42
+ useEffect(function () {
43
+ setIsSupported(detectSupport());
44
+ }, []);
45
+ var stop = useCallback(function () {
46
+ if (detectSupport()) window.speechSynthesis.cancel();
47
+ utteranceRef.current = null;
48
+ setIsSpeaking(false);
49
+ }, []);
50
+ var speak = useCallback(function () {
51
+ if (!detectSupport()) {
52
+ setIsSupported(false);
53
+ return;
54
+ }
55
+ // Always speak the *current* `text` prop, not a value captured on mount.
56
+ var spokenText = text;
57
+ var utterance = new window.SpeechSynthesisUtterance(spokenText);
25
58
  utterance.rate = rate;
26
59
  utterance.pitch = pitch;
60
+ var isCurrent = function isCurrent() {
61
+ return isMountedRef.current && utteranceRef.current === utterance;
62
+ };
27
63
  utterance.onstart = function () {
28
- onSpeak(inputText);
64
+ if (!isCurrent()) return;
65
+ onSpeak(spokenText);
29
66
  setIsSpeaking(true);
30
67
  };
31
68
  utterance.onend = function () {
69
+ if (!isCurrent()) return;
70
+ utteranceRef.current = null;
32
71
  setIsSpeaking(false);
33
72
  };
34
- if (isSpeaking) {
35
- window.speechSynthesis.cancel();
73
+ utterance.onerror = function (event) {
74
+ if (!isCurrent()) return;
75
+ utteranceRef.current = null;
36
76
  setIsSpeaking(false);
37
- } else {
77
+ if (!NON_FATAL_ERRORS.has(event === null || event === void 0 ? void 0 : event.error)) {
78
+ onError(new Error((event === null || event === void 0 ? void 0 : event.error) || "Speech synthesis error"));
79
+ }
80
+ };
81
+ utteranceRef.current = utterance;
82
+ try {
38
83
  window.speechSynthesis.speak(utterance);
84
+ } catch (err) {
85
+ utteranceRef.current = null;
86
+ setIsSpeaking(false);
87
+ onError(err);
39
88
  }
40
- };
41
- return jsx("div", {
42
- children: renderButton && typeof renderButton === "function" ? renderButton({
43
- onClick: handleSpeak,
44
- isSpeaking: isSpeaking
45
- }) : jsx("button", {
46
- onClick: handleSpeak,
47
- children: isSpeaking ? "Stop" : "Speak"
48
- })
49
- });
50
- };
89
+ }, [text, rate, pitch, onSpeak, onError]);
90
+ var isDisabled = disabled || !isSupported;
91
+ var handleClick = useCallback(function () {
92
+ if (isDisabled) return;
93
+ if (isSpeaking) stop();else speak();
94
+ }, [isDisabled, isSpeaking, speak, stop]);
95
+ // Stop our own speech when unmounting so it doesn't outlive the component.
96
+ useEffect(function () {
97
+ isMountedRef.current = true;
98
+ return function () {
99
+ isMountedRef.current = false;
100
+ if (utteranceRef.current && detectSupport()) {
101
+ utteranceRef.current = null;
102
+ window.speechSynthesis.cancel();
103
+ }
104
+ };
105
+ }, []);
106
+ if (typeof renderButton === "function") {
107
+ // speak/stop only touch refs when invoked from event handlers, never while
108
+ // renderButton itself is rendering.
109
+ // eslint-disable-next-line react-hooks/refs
110
+ return renderButton({
111
+ onClick: handleClick,
112
+ isSpeaking: isSpeaking,
113
+ isSupported: isSupported,
114
+ disabled: isDisabled,
115
+ speak: speak,
116
+ stop: stop
117
+ });
118
+ }
119
+ return jsxs("button", _objectSpread2(_objectSpread2({
120
+ ref: ref,
121
+ type: "button",
122
+ onClick: handleClick,
123
+ disabled: isDisabled,
124
+ "aria-pressed": isSpeaking,
125
+ title: !isSupported ? UNSUPPORTED_MESSAGE : undefined,
126
+ className: cn("inline-flex items-center gap-2 rounded-md px-4 h-10 text-sm font-medium", "border border-gray-300 bg-transparent transition-colors motion-reduce:transition-none", "hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-primary-500", "disabled:opacity-50 disabled:cursor-not-allowed", {
127
+ "text-error-600 border-error-600": isSpeaking,
128
+ "text-gray-900": !isSpeaking
129
+ }, className)
130
+ }, rest), {}, {
131
+ children: [isSpeaking ? jsx(Square, {
132
+ className: "h-4 w-4",
133
+ "aria-hidden": "true"
134
+ }) : jsx(Volume2, {
135
+ className: "h-4 w-4",
136
+ "aria-hidden": "true"
137
+ }), isSpeaking ? "Stop" : "Speak", !isSupported && jsx("span", {
138
+ className: "sr-only",
139
+ children: " (".concat(UNSUPPORTED_MESSAGE, ")")
140
+ })]
141
+ }));
142
+ });
143
+ TextToSpeech.displayName = "TextToSpeech";
51
144
 
52
145
  export { TextToSpeech as default };
@@ -6,6 +6,24 @@ import { createPortal } from 'react-dom';
6
6
  import { cn } from '../../utils/cn.js';
7
7
 
8
8
  var _excluded = ["position", "trigger", "content", "customTrigger", "children", "open", "defaultOpen", "onChange", "delay", "offset", "textClass", "bgClass", "className", "portal", "id"];
9
+ // Grace period (ms) before a hover tooltip closes after the pointer leaves the
10
+ // trigger, so the pointer can cross the `offset` gap onto the tooltip itself
11
+ // (WCAG 1.4.13 "hoverable").
12
+ var HOVER_CLOSE_GRACE = 100;
13
+ /**
14
+ * Nearest ancestor that establishes the containing block for an absolutely
15
+ * positioned element. Uses `offsetParent` where the browser provides it and
16
+ * falls back to walking computed styles (jsdom has no layout).
17
+ */
18
+ var getContainingBlock = function getContainingBlock(el) {
19
+ if (el.offsetParent) return el.offsetParent;
20
+ for (var p = el.parentElement; p && p !== document.body; p = p.parentElement) {
21
+ var cs = window.getComputedStyle(p);
22
+ if (cs.position && cs.position !== "static") return p;
23
+ if (cs.transform && cs.transform !== "none") return p;
24
+ }
25
+ return null;
26
+ };
9
27
  var NATIVE_FOCUSABLE_TAGS = new Set(["button", "input", "select", "textarea"]);
10
28
  /**
11
29
  * True when `el` is a single intrinsic element that is keyboard-focusable on
@@ -62,9 +80,9 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
62
80
  _ref$offset = _ref.offset,
63
81
  offset = _ref$offset === void 0 ? 8 : _ref$offset,
64
82
  _ref$textClass = _ref.textClass,
65
- textClass = _ref$textClass === void 0 ? "text-black" : _ref$textClass,
83
+ textClass = _ref$textClass === void 0 ? "text-foreground" : _ref$textClass,
66
84
  _ref$bgClass = _ref.bgClass,
67
- bgClass = _ref$bgClass === void 0 ? "bg-white" : _ref$bgClass,
85
+ bgClass = _ref$bgClass === void 0 ? "bg-surface" : _ref$bgClass,
68
86
  _ref$className = _ref.className,
69
87
  className = _ref$className === void 0 ? "" : _ref$className,
70
88
  _ref$portal = _ref.portal,
@@ -74,6 +92,7 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
74
92
  var triggerRef = useRef(null);
75
93
  var tooltipRef = useRef(null);
76
94
  var delayRef = useRef(null);
95
+ var closeTimerRef = useRef(null);
77
96
  var _useState = useState(Boolean(controlledOpen !== null && controlledOpen !== void 0 ? controlledOpen : defaultOpen)),
78
97
  _useState2 = _slicedToArray(_useState, 2),
79
98
  open = _useState2[0],
@@ -176,10 +195,22 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
176
195
  var pad = 8;
177
196
  left = Math.max(pad, Math.min(left, winW - ttW - pad));
178
197
  top = Math.max(pad, Math.min(top, winH - ttH - pad));
198
+ // `left`/`top` are viewport coords; convert to the containing block of the
199
+ // absolutely positioned tooltip. Portaled into <body> (or inline with no
200
+ // positioned ancestor) that is the document, so add page scroll; inline
201
+ // inside a positioned ancestor, offset by that ancestor instead.
202
+ var cb = getContainingBlock(el);
203
+ var originX = -window.scrollX;
204
+ var originY = -window.scrollY;
205
+ if (cb && cb !== document.body && cb !== document.documentElement) {
206
+ var cr = cb.getBoundingClientRect();
207
+ originX = cr.left + (cb.clientLeft || 0) - cb.scrollLeft;
208
+ originY = cr.top + (cb.clientTop || 0) - cb.scrollTop;
209
+ }
179
210
  setPlacement(chosen);
180
211
  setCoords({
181
- left: Math.round(left) + window.scrollX,
182
- top: Math.round(top) + window.scrollY
212
+ left: Math.round(left - originX),
213
+ top: Math.round(top - originY)
183
214
  });
184
215
  }, [offset, position]);
185
216
  useEffect(function () {
@@ -202,9 +233,13 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
202
233
  };
203
234
  }, [open, computePosition]);
204
235
  // open/close helpers
236
+ var cancelScheduledClose = useCallback(function () {
237
+ clearTimeout(closeTimerRef.current);
238
+ }, []);
205
239
  var openTooltip = useCallback(function () {
206
240
  var immediate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
207
241
  clearTimeout(delayRef.current);
242
+ clearTimeout(closeTimerRef.current);
208
243
  if (immediate || !delay) {
209
244
  setOpen(true);
210
245
  notify(true);
@@ -217,9 +252,22 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
217
252
  }, [delay, notify]);
218
253
  var closeTooltip = useCallback(function () {
219
254
  clearTimeout(delayRef.current);
255
+ clearTimeout(closeTimerRef.current);
220
256
  setOpen(false);
221
257
  notify(false);
222
258
  }, [notify]);
259
+ // Hover close is deferred so the pointer can move onto the tooltip.
260
+ var scheduleClose = useCallback(function () {
261
+ clearTimeout(delayRef.current);
262
+ clearTimeout(closeTimerRef.current);
263
+ closeTimerRef.current = setTimeout(closeTooltip, HOVER_CLOSE_GRACE);
264
+ }, [closeTooltip]);
265
+ useEffect(function () {
266
+ return function () {
267
+ clearTimeout(delayRef.current);
268
+ clearTimeout(closeTimerRef.current);
269
+ };
270
+ }, []);
223
271
  // triggers normalization
224
272
  var triggers = useMemo(function () {
225
273
  return Array.isArray(trigger) ? trigger : [trigger];
@@ -228,34 +276,23 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
228
276
  useEffect(function () {
229
277
  var t = triggerRef.current;
230
278
  if (!t) return;
279
+ // The tooltip element itself keeps the tip open via its onMouseEnter /
280
+ // onMouseLeave props (see tooltipNode) — it doesn't exist yet here.
231
281
  if (triggers.includes("hover")) {
232
282
  var onMouseEnter = function onMouseEnter() {
233
283
  return openTooltip();
234
284
  };
235
285
  var onMouseLeave = function onMouseLeave() {
236
- return closeTooltip();
286
+ return scheduleClose();
237
287
  };
238
288
  t.addEventListener("mouseenter", onMouseEnter);
239
289
  t.addEventListener("mouseleave", onMouseLeave);
240
- // also attach to tooltip to prevent flicker
241
- var tip = function tip() {
242
- return tooltipRef.current;
243
- };
244
- var tryTip = tip();
245
- if (tryTip) {
246
- tryTip.addEventListener("mouseenter", onMouseEnter);
247
- tryTip.addEventListener("mouseleave", onMouseLeave);
248
- }
249
290
  return function () {
250
291
  t.removeEventListener("mouseenter", onMouseEnter);
251
292
  t.removeEventListener("mouseleave", onMouseLeave);
252
- if (tryTip) {
253
- tryTip.removeEventListener("mouseenter", onMouseEnter);
254
- tryTip.removeEventListener("mouseleave", onMouseLeave);
255
- }
256
293
  };
257
294
  }
258
- }, [triggers, openTooltip, closeTooltip]);
295
+ }, [triggers, openTooltip, scheduleClose]);
259
296
  // click handlers (toggle and outside click)
260
297
  useEffect(function () {
261
298
  var t = triggerRef.current;
@@ -283,16 +320,21 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
283
320
  };
284
321
  }
285
322
  }, [triggers, notify]);
286
- // focus handlers: use focusin / focusout which bubble and handle inner focusables
323
+ // focus handlers: use focusin / focusout which bubble and handle inner focusables.
324
+ // A hover tooltip also opens on keyboard focus (WCAG 1.4.13), unless click
325
+ // is a trigger too (focus-on-click would open it and the click toggle it shut).
326
+ var opensOnFocus = triggers.includes("focus") || triggers.includes("hover") && !triggers.includes("click");
287
327
  useEffect(function () {
288
328
  var t = triggerRef.current;
289
329
  if (!t) return;
290
- if (triggers.includes("focus")) {
330
+ if (opensOnFocus) {
291
331
  var onFocusIn = function onFocusIn() {
292
332
  return openTooltip(true);
293
333
  };
294
- var onFocusOut = function onFocusOut() {
295
- return closeTooltip();
334
+ var onFocusOut = function onFocusOut(e) {
335
+ var tip = tooltipRef.current;
336
+ if (e.relatedTarget && tip && tip.contains(e.relatedTarget)) return;
337
+ closeTooltip();
296
338
  };
297
339
  t.addEventListener("focusin", onFocusIn);
298
340
  t.addEventListener("focusout", onFocusOut);
@@ -301,7 +343,22 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
301
343
  t.removeEventListener("focusout", onFocusOut);
302
344
  };
303
345
  }
304
- }, [triggers, openTooltip, closeTooltip]);
346
+ }, [opensOnFocus, openTooltip, closeTooltip]);
347
+ // Dismiss on a pointer/tap outside. Touch has no mouseleave, so without this
348
+ // a tapped-open hover tooltip would never close.
349
+ useEffect(function () {
350
+ if (!open) return;
351
+ var onPointerDown = function onPointerDown(e) {
352
+ var t = triggerRef.current;
353
+ var tip = tooltipRef.current;
354
+ if (t && t.contains(e.target) || tip && tip.contains(e.target)) return;
355
+ closeTooltip();
356
+ };
357
+ document.addEventListener("pointerdown", onPointerDown);
358
+ return function () {
359
+ return document.removeEventListener("pointerdown", onPointerDown);
360
+ };
361
+ }, [open, closeTooltip]);
305
362
  // Escape key closes tooltip
306
363
  useEffect(function () {
307
364
  if (!open) return;
@@ -345,20 +402,35 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
345
402
  "aria-describedby": id,
346
403
  children: children
347
404
  });
405
+ // Hovering the tooltip itself keeps it open (attached as props so they are
406
+ // live as soon as the tip mounts); consumer handlers in ...rest still run.
407
+ var hoverable = triggers.includes("hover");
408
+ var restMouseEnter = rest.onMouseEnter,
409
+ restMouseLeave = rest.onMouseLeave;
410
+ var tipHoverProps = hoverable ? {
411
+ onMouseEnter: function onMouseEnter(e) {
412
+ cancelScheduledClose();
413
+ restMouseEnter === null || restMouseEnter === void 0 || restMouseEnter(e);
414
+ },
415
+ onMouseLeave: function onMouseLeave(e) {
416
+ scheduleClose();
417
+ restMouseLeave === null || restMouseLeave === void 0 || restMouseLeave(e);
418
+ }
419
+ } : {};
348
420
  // Tooltip box element
349
- var tooltipNode = open ? jsx("div", _objectSpread2(_objectSpread2({
421
+ var tooltipNode = open ? jsx("div", _objectSpread2(_objectSpread2(_objectSpread2({
350
422
  ref: tooltipRef,
351
423
  id: id,
352
424
  role: "tooltip"
353
- }, rest), {}, {
354
- className: cn("absolute z-50 transform-gpu transition-opacity duration-150", className),
425
+ }, rest), tipHoverProps), {}, {
426
+ className: cn("absolute z-50 transform-gpu transition-opacity duration-150 motion-reduce:transition-none", className),
355
427
  style: {
356
428
  left: coords.left,
357
429
  top: coords.top,
358
430
  position: "absolute"
359
431
  },
360
432
  children: jsx("div", {
361
- className: cn("max-w-xs rounded-md border border-gray-200 px-3 py-2 text-sm shadow-lg shadow-black/5", bgClass, textClass),
433
+ className: cn("max-w-xs rounded-md border border-line px-3 py-2 text-sm shadow-lg shadow-black/5", bgClass, textClass),
362
434
  children: renderContent()
363
435
  })
364
436
  })) : null;
@@ -4,8 +4,12 @@ import { jsx, jsxs } from 'react/jsx-runtime';
4
4
  import React from 'react';
5
5
  import { cn } from '../../utils/cn.js';
6
6
  import { Info, AlertTriangle, AlertCircle, CheckCircle, X } from 'lucide-react';
7
+ import { TOUCH_TARGET } from '../../utils/touchTarget.js';
7
8
 
8
9
  var _excluded = ["variant", "title", "children", "dismissible", "onDismiss", "className"];
10
+ // error / warning need attention now (role="alert", assertive); info / success
11
+ // are neutral and must not interrupt (role="status", polite).
12
+ var URGENT_VARIANTS = new Set(['error', 'warning']);
9
13
  var Alert = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
10
14
  var _ref$variant = _ref.variant,
11
15
  variant = _ref$variant === void 0 ? 'info' : _ref$variant,
@@ -20,7 +24,8 @@ var Alert = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
20
24
  success: {
21
25
  container: 'bg-success-50 border-success-200',
22
26
  icon: jsx(CheckCircle, {
23
- className: "h-5 w-5 text-success-500"
27
+ className: "h-5 w-5 text-success-500",
28
+ "aria-hidden": "true"
24
29
  }),
25
30
  title: 'text-success-800',
26
31
  content: 'text-success-700'
@@ -28,7 +33,8 @@ var Alert = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
28
33
  error: {
29
34
  container: 'bg-error-50 border-error-200',
30
35
  icon: jsx(AlertCircle, {
31
- className: "h-5 w-5 text-error-500"
36
+ className: "h-5 w-5 text-error-500",
37
+ "aria-hidden": "true"
32
38
  }),
33
39
  title: 'text-error-800',
34
40
  content: 'text-error-700'
@@ -36,7 +42,8 @@ var Alert = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
36
42
  warning: {
37
43
  container: 'bg-warning-50 border-warning-200',
38
44
  icon: jsx(AlertTriangle, {
39
- className: "h-5 w-5 text-warning-500"
45
+ className: "h-5 w-5 text-warning-500",
46
+ "aria-hidden": "true"
40
47
  }),
41
48
  title: 'text-warning-800',
42
49
  content: 'text-warning-700'
@@ -44,7 +51,8 @@ var Alert = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
44
51
  info: {
45
52
  container: 'bg-primary-50 border-primary-200',
46
53
  icon: jsx(Info, {
47
- className: "h-5 w-5 text-primary-500"
54
+ className: "h-5 w-5 text-primary-500",
55
+ "aria-hidden": "true"
48
56
  }),
49
57
  title: 'text-primary-800',
50
58
  content: 'text-primary-700'
@@ -54,7 +62,7 @@ var Alert = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
54
62
  return jsx("div", _objectSpread2(_objectSpread2({
55
63
  ref: ref,
56
64
  className: cn('rounded-md border p-4', variantStyles.container, className),
57
- role: "alert"
65
+ role: URGENT_VARIANTS.has(variant) ? 'alert' : 'status'
58
66
  }, props), {}, {
59
67
  children: jsxs("div", {
60
68
  className: "flex",
@@ -76,15 +84,17 @@ var Alert = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
76
84
  className: "ml-auto pl-3",
77
85
  children: jsx("button", {
78
86
  type: "button",
79
- className: cn('inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2', {
80
- 'text-success-500 hover:bg-success-100 focus:ring-success-600': variant === 'success',
81
- 'text-error-500 hover:bg-error-100 focus:ring-error-600': variant === 'error',
82
- 'text-warning-500 hover:bg-warning-100 focus:ring-warning-600': variant === 'warning',
83
- 'text-primary-500 hover:bg-primary-100 focus:ring-primary-600': variant === 'info'
87
+ className: cn('inline-flex items-center justify-center rounded-md p-1.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2', TOUCH_TARGET, {
88
+ 'text-success-500 hover:bg-success-100 focus-visible:ring-success-600': variant === 'success',
89
+ 'text-error-500 hover:bg-error-100 focus-visible:ring-error-600': variant === 'error',
90
+ 'text-warning-500 hover:bg-warning-100 focus-visible:ring-warning-600': variant === 'warning',
91
+ 'text-primary-500 hover:bg-primary-100 focus-visible:ring-primary-600': variant === 'info'
84
92
  }),
85
93
  onClick: onDismiss,
94
+ "aria-label": "Dismiss",
86
95
  children: jsx(X, {
87
- className: "h-4 w-4"
96
+ className: "h-4 w-4",
97
+ "aria-hidden": "true"
88
98
  })
89
99
  })
90
100
  })]