@pk-design/react-tailwind 1.8.1 → 1.10.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 (63) hide show
  1. package/dist/cjs/components/Banner/Banner.js +1 -1
  2. package/dist/cjs/components/Banner/Banner.js.map +1 -1
  3. package/dist/cjs/components/BottomNavigation/BottomNavigation.js +14 -5
  4. package/dist/cjs/components/BottomNavigation/BottomNavigation.js.map +1 -1
  5. package/dist/cjs/components/Button/Button.js +88 -60
  6. package/dist/cjs/components/Button/Button.js.map +1 -1
  7. package/dist/cjs/components/Drawer/Drawer.js +6 -2
  8. package/dist/cjs/components/Drawer/Drawer.js.map +1 -1
  9. package/dist/cjs/components/MobileHeader/MobileHeader.js +21 -3
  10. package/dist/cjs/components/MobileHeader/MobileHeader.js.map +1 -1
  11. package/dist/cjs/components/PullToRefresh/PullToRefresh.js +145 -57
  12. package/dist/cjs/components/PullToRefresh/PullToRefresh.js.map +1 -1
  13. package/dist/cjs/components/SelectBox/SelectBox.js +25 -4
  14. package/dist/cjs/components/SelectBox/SelectBox.js.map +1 -1
  15. package/dist/cjs/components/StickyActionBar/StickyActionBar.js +2 -2
  16. package/dist/cjs/components/StickyActionBar/StickyActionBar.js.map +1 -1
  17. package/dist/cjs/components/Toast/Toast.js +1 -1
  18. package/dist/cjs/components/Toast/Toast.js.map +1 -1
  19. package/dist/cjs/hooks/use-breakpoint.js +48 -0
  20. package/dist/cjs/hooks/use-breakpoint.js.map +1 -0
  21. package/dist/cjs/index.js +5 -0
  22. package/dist/cjs/index.js.map +1 -1
  23. package/dist/es/components/Banner/Banner.js +1 -1
  24. package/dist/es/components/Banner/Banner.js.map +1 -1
  25. package/dist/es/components/BottomNavigation/BottomNavigation.js +14 -5
  26. package/dist/es/components/BottomNavigation/BottomNavigation.js.map +1 -1
  27. package/dist/es/components/Button/Button.js +89 -61
  28. package/dist/es/components/Button/Button.js.map +1 -1
  29. package/dist/es/components/Drawer/Drawer.js +6 -2
  30. package/dist/es/components/Drawer/Drawer.js.map +1 -1
  31. package/dist/es/components/MobileHeader/MobileHeader.js +21 -3
  32. package/dist/es/components/MobileHeader/MobileHeader.js.map +1 -1
  33. package/dist/es/components/PullToRefresh/PullToRefresh.js +146 -58
  34. package/dist/es/components/PullToRefresh/PullToRefresh.js.map +1 -1
  35. package/dist/es/components/SelectBox/SelectBox.js +26 -5
  36. package/dist/es/components/SelectBox/SelectBox.js.map +1 -1
  37. package/dist/es/components/StickyActionBar/StickyActionBar.js +2 -2
  38. package/dist/es/components/StickyActionBar/StickyActionBar.js.map +1 -1
  39. package/dist/es/components/Toast/Toast.js +1 -1
  40. package/dist/es/components/Toast/Toast.js.map +1 -1
  41. package/dist/es/hooks/use-breakpoint.js +43 -0
  42. package/dist/es/hooks/use-breakpoint.js.map +1 -0
  43. package/dist/es/index.js +1 -0
  44. package/dist/es/index.js.map +1 -1
  45. package/dist/react-tailwind.cjs.js +344 -135
  46. package/dist/react-tailwind.cjs.js.map +1 -1
  47. package/dist/react-tailwind.es.js +342 -137
  48. package/dist/react-tailwind.es.js.map +1 -1
  49. package/dist/types/components/BottomNavigation/BottomNavigation.d.ts +1 -1
  50. package/dist/types/components/BottomNavigation/BottomNavigation.types.d.ts +28 -3
  51. package/dist/types/components/Button/Button.types.d.ts +12 -6
  52. package/dist/types/components/Drawer/Drawer.types.d.ts +8 -4
  53. package/dist/types/components/MobileHeader/MobileHeader.d.ts +1 -1
  54. package/dist/types/components/MobileHeader/MobileHeader.types.d.ts +13 -0
  55. package/dist/types/components/PullToRefresh/PullToRefresh.d.ts +1 -1
  56. package/dist/types/components/PullToRefresh/PullToRefresh.types.d.ts +45 -6
  57. package/dist/types/components/SelectBox/SelectBox.types.d.ts +18 -1
  58. package/dist/types/components/StickyActionBar/StickyActionBar.types.d.ts +12 -0
  59. package/dist/types/hooks/use-breakpoint.d.ts +27 -0
  60. package/dist/types/hooks/use-breakpoint.test.d.ts +1 -0
  61. package/dist/types/index.d.ts +1 -0
  62. package/dist/types/test-utils/mock-viewport.d.ts +7 -0
  63. package/package.json +1 -1
@@ -151,59 +151,35 @@ TextContent.displayName = 'TextContent';
151
151
 
152
152
  /**
153
153
  * Tailwind CSS classes for theme + variant combinations.
154
+ *
155
+ * Only colours live here. Focus outline width/offset and disabled styling are
156
+ * applied uniformly in the component so they can be toggled in one place.
154
157
  */
155
158
  const THEMED_BTN_CLASS = {
156
159
  primary: {
157
- default: 'text-white bg-[var(--ui-primary)] hover:bg-[var(--ui-primary-hover)] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-[var(--ui-primary-ring)] disabled:bg-[var(--ui-primary-disabled)]',
158
- outlined: 'text-[var(--ui-primary-text)] hover:text-white border border-[var(--ui-primary)] hover:bg-[var(--ui-primary)] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-[var(--ui-primary-ring)] disabled:text-[var(--ui-primary-disabled)] disabled:border-[var(--ui-primary-disabled)]',
159
- plain: 'text-[var(--ui-primary-text)] hover:bg-blue-50/80 dark:hover:bg-blue-950/40 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-[var(--ui-primary-ring)] disabled:text-[var(--ui-primary-disabled)]',
160
- },
161
- danger: {
162
- default: 'text-white bg-[var(--ui-danger)] hover:bg-[var(--ui-danger-hover)] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-[var(--ui-danger-ring)] disabled:bg-[var(--ui-danger-disabled)]',
163
- outlined: 'text-[var(--ui-danger-text)] hover:text-white border border-[var(--ui-danger)] hover:bg-[var(--ui-danger)] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-[var(--ui-danger-ring)] disabled:text-[var(--ui-danger-disabled)] disabled:border-[var(--ui-danger-disabled)]',
164
- plain: 'text-[var(--ui-danger-text)] hover:bg-red-50/80 dark:hover:bg-red-950/40 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-[var(--ui-danger-ring)] disabled:text-[var(--ui-danger-disabled)]',
165
- },
166
- secondary: {
167
- default: 'text-gray-700 bg-white border border-[var(--ui-border)] hover:bg-gray-100 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-[var(--ui-focus-ring)] disabled:bg-gray-200 dark:disabled:bg-gray-700',
168
- outlined: 'text-gray-700 hover:text-white border border-[var(--ui-border)] hover:bg-gray-700 dark:text-gray-200 dark:hover:text-white dark:hover:bg-gray-600 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-[var(--ui-focus-ring)] disabled:text-gray-400 disabled:border-[var(--ui-border-muted)]',
169
- plain: 'text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-800 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-[var(--ui-focus-ring)] disabled:text-gray-400',
170
- },
171
- success: {
172
- default: 'text-white bg-emerald-600 hover:bg-emerald-700 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-emerald-500 disabled:bg-emerald-300 dark:disabled:bg-emerald-900',
173
- outlined: 'text-emerald-600 hover:text-white border border-emerald-600 hover:bg-emerald-600 dark:text-emerald-400 dark:border-emerald-500 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-emerald-500 disabled:opacity-50',
174
- plain: 'text-emerald-600 hover:bg-emerald-50 dark:text-emerald-400 dark:hover:bg-emerald-950/40 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-emerald-500 disabled:opacity-50',
175
- },
176
- warning: {
177
- default: 'text-white bg-amber-500 hover:bg-amber-600 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-amber-500 disabled:bg-amber-300 dark:disabled:bg-amber-900',
178
- outlined: 'text-amber-600 hover:text-white border border-amber-500 hover:bg-amber-500 dark:text-amber-400 dark:border-amber-500 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-amber-500 disabled:opacity-50',
179
- plain: 'text-amber-600 hover:bg-amber-50 dark:text-amber-400 dark:hover:bg-amber-950/40 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-amber-500 disabled:opacity-50',
180
- },
181
- };
182
- const LOADER_TEXT_CLASS = {
183
- primary: {
184
- default: 'text-white',
185
- outlined: 'text-[var(--ui-primary-text)]',
186
- plain: 'text-[var(--ui-primary-text)]',
160
+ default: 'text-white bg-[var(--ui-primary)] hover:bg-[var(--ui-primary-hover)] focus-visible:outline-[var(--ui-primary-ring)]',
161
+ outlined: 'text-[var(--ui-primary-text)] hover:text-white border border-[var(--ui-primary)] hover:bg-[var(--ui-primary)] focus-visible:outline-[var(--ui-primary-ring)]',
162
+ plain: 'text-[var(--ui-primary-text)] hover:bg-current/10 active:bg-current/15 focus-visible:outline-[var(--ui-primary-ring)]',
187
163
  },
188
164
  danger: {
189
- default: 'text-white',
190
- outlined: 'text-[var(--ui-danger-text)]',
191
- plain: 'text-[var(--ui-danger-text)]',
165
+ default: 'text-white bg-[var(--ui-danger)] hover:bg-[var(--ui-danger-hover)] focus-visible:outline-[var(--ui-danger-ring)]',
166
+ outlined: 'text-[var(--ui-danger-text)] hover:text-white border border-[var(--ui-danger)] hover:bg-[var(--ui-danger)] focus-visible:outline-[var(--ui-danger-ring)]',
167
+ plain: 'text-[var(--ui-danger-text)] hover:bg-current/10 active:bg-current/15 focus-visible:outline-[var(--ui-danger-ring)]',
192
168
  },
193
169
  secondary: {
194
- default: 'text-gray-700 dark:text-white',
195
- outlined: 'text-gray-700 dark:text-gray-200',
196
- plain: 'text-gray-700 dark:text-gray-200',
170
+ default: 'text-gray-700 bg-white border border-[var(--ui-border)] hover:bg-gray-100 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700 focus-visible:outline-[var(--ui-focus-ring)]',
171
+ outlined: 'text-gray-700 hover:text-white border border-[var(--ui-border)] hover:bg-gray-700 dark:text-gray-200 dark:hover:text-white dark:hover:bg-gray-600 focus-visible:outline-[var(--ui-focus-ring)]',
172
+ plain: 'text-gray-700 hover:bg-current/10 active:bg-current/15 dark:text-gray-200 focus-visible:outline-[var(--ui-focus-ring)]',
197
173
  },
198
174
  success: {
199
- default: 'text-white',
200
- outlined: 'text-emerald-600 dark:text-emerald-400',
201
- plain: 'text-emerald-600 dark:text-emerald-400',
175
+ default: 'text-white bg-emerald-600 hover:bg-emerald-700 focus-visible:outline-emerald-500',
176
+ outlined: 'text-emerald-700 hover:text-white border border-emerald-600 hover:bg-emerald-600 dark:text-emerald-400 dark:border-emerald-500 focus-visible:outline-emerald-500',
177
+ plain: 'text-emerald-700 hover:bg-current/10 active:bg-current/15 dark:text-emerald-400 focus-visible:outline-emerald-500',
202
178
  },
203
179
  warning: {
204
- default: 'text-white',
205
- outlined: 'text-amber-600 dark:text-amber-400',
206
- plain: 'text-amber-600 dark:text-amber-400',
180
+ default: 'text-white bg-amber-500 hover:bg-amber-600 focus-visible:outline-amber-500',
181
+ outlined: 'text-amber-700 hover:text-white border border-amber-500 hover:bg-amber-500 dark:text-amber-400 dark:border-amber-500 focus-visible:outline-amber-500',
182
+ plain: 'text-amber-700 hover:bg-current/10 active:bg-current/15 dark:text-amber-400 focus-visible:outline-amber-500',
207
183
  },
208
184
  };
209
185
  const SIZE_CLASSES = {
@@ -223,22 +199,32 @@ const LOADER_SIZE = {
223
199
  };
224
200
  /**
225
201
  * Base shared tailwind classes.
202
+ *
203
+ * Disabled styling is keyed off both the native `disabled` attribute and
204
+ * `aria-disabled`, so non-button elements rendered via `as` look disabled too.
226
205
  */
227
206
  const BASE_BTN_CLASS = 'font-medium inline-flex items-center justify-center shrink-0 cursor-pointer select-none ' +
228
- 'disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-75 ' +
229
- 'active:scale-[0.98] transition-all duration-150 ease-out focus:outline-none';
207
+ 'transition-[color,background-color,border-color,box-shadow,opacity,transform] duration-150 ease-out ' +
208
+ 'active:scale-[0.98] focus:outline-none ' +
209
+ 'disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none ' +
210
+ 'aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:shadow-none';
230
211
  const Button = React.forwardRef(({ as, type, ...props }, ref) => {
231
- var _a, _b, _c, _d;
232
212
  const { className = '', children, theme = 'primary', variant = 'default', size = 'md', leftIcon, rightIcon, fullWidth = false, loading = false, loadingText = '', rounded = false, disabled, iconOnly = false, noOutlineOnFocus = false, active, onClick, ...restProps } = props;
233
213
  const Component = as || 'button';
234
214
  const isNativeButton = Component === 'button';
215
+ // Clean invalid props when not rendering a real button
216
+ const { type: _ignoreType, disabled: _ignoreDisabled, role, tabIndex, onKeyDown, onKeyUp, ...domProps } = restProps;
217
+ const ariaLabel = domProps['aria-label'];
218
+ const ariaLabelledBy = domProps['aria-labelledby'];
235
219
  // Accessibility guard for icon-only buttons
236
- if (iconOnly && !restProps['aria-label']) {
237
- console.warn('[ui] <Button iconOnly> requires aria-label for accessibility.');
238
- }
220
+ React.useEffect(() => {
221
+ if (iconOnly && !ariaLabel && !ariaLabelledBy) {
222
+ console.warn('[ui] <Button iconOnly> requires aria-label or aria-labelledby for accessibility.');
223
+ }
224
+ }, [iconOnly, ariaLabel, ariaLabelledBy]);
239
225
  const resolvedDisabled = Boolean(disabled || loading);
240
- // Clean invalid props when not rendering a real button
241
- const { type: _ignoreType, disabled: _ignoreDisabled, ...cleanRest } = restProps;
226
+ // Links (`href`, or `to` for router Link components) keep their native semantics.
227
+ const isLink = !isNativeButton && (domProps.href != null || domProps.to != null);
242
228
  const handleClick = (e) => {
243
229
  if (resolvedDisabled) {
244
230
  e.preventDefault();
@@ -247,31 +233,73 @@ const Button = React.forwardRef(({ as, type, ...props }, ref) => {
247
233
  }
248
234
  onClick === null || onClick === void 0 ? void 0 : onClick(e);
249
235
  };
236
+ // Mirror native <button> keyboard activation for non-button elements:
237
+ // Enter activates on keydown, Space on keyup (and must not scroll the page).
238
+ const handleKeyDown = (e) => {
239
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(e);
240
+ if (e.defaultPrevented || e.target !== e.currentTarget)
241
+ return;
242
+ if (e.key === 'Enter') {
243
+ e.preventDefault();
244
+ e.currentTarget.click();
245
+ }
246
+ else if (e.key === ' ') {
247
+ e.preventDefault();
248
+ }
249
+ };
250
+ const handleKeyUp = (e) => {
251
+ onKeyUp === null || onKeyUp === void 0 ? void 0 : onKeyUp(e);
252
+ if (e.defaultPrevented || e.target !== e.currentTarget)
253
+ return;
254
+ if (e.key === ' ') {
255
+ e.preventDefault();
256
+ e.currentTarget.click();
257
+ }
258
+ };
250
259
  const elementProps = isNativeButton
251
260
  ? {
252
261
  type: type !== null && type !== void 0 ? type : 'button',
253
262
  disabled: resolvedDisabled,
263
+ role,
264
+ tabIndex,
254
265
  onClick: handleClick,
266
+ onKeyDown,
267
+ onKeyUp,
255
268
  }
256
- : {
257
- role: (_a = cleanRest.role) !== null && _a !== void 0 ? _a : 'button',
258
- tabIndex: resolvedDisabled ? -1 : ((_b = cleanRest.tabIndex) !== null && _b !== void 0 ? _b : 0),
259
- 'aria-disabled': resolvedDisabled ? true : undefined,
260
- onClick: handleClick,
261
- };
269
+ : isLink
270
+ ? {
271
+ role,
272
+ tabIndex: resolvedDisabled ? -1 : tabIndex,
273
+ 'aria-disabled': resolvedDisabled ? true : undefined,
274
+ onClick: handleClick,
275
+ onKeyDown,
276
+ onKeyUp,
277
+ }
278
+ : {
279
+ role: role !== null && role !== void 0 ? role : 'button',
280
+ tabIndex: resolvedDisabled ? -1 : (tabIndex !== null && tabIndex !== void 0 ? tabIndex : 0),
281
+ 'aria-disabled': resolvedDisabled ? true : undefined,
282
+ onClick: handleClick,
283
+ onKeyDown: handleKeyDown,
284
+ onKeyUp: handleKeyUp,
285
+ };
262
286
  const sizeObj = SIZE_CLASSES[size] || SIZE_CLASSES.md;
263
287
  const classes = React.useMemo(() => {
264
288
  var _a;
265
- return buildClassName(BASE_BTN_CLASS, ((_a = THEMED_BTN_CLASS[theme]) === null || _a === void 0 ? void 0 : _a[variant]) || THEMED_BTN_CLASS.primary.default, iconOnly ? sizeObj.iconOnly : sizeObj.normal, !noOutlineOnFocus && 'focus-visible:ring-2 focus-visible:ring-offset-2', rounded && 'rounded-full', fullWidth && 'w-full', variant === 'default' && 'shadow-xs hover:shadow-none disabled:shadow-none', active && 'ring-2 ring-offset-2 ring-current', className);
289
+ return buildClassName(BASE_BTN_CLASS, ((_a = THEMED_BTN_CLASS[theme]) === null || _a === void 0 ? void 0 : _a[variant]) || THEMED_BTN_CLASS.primary.default, iconOnly ? sizeObj.iconOnly : sizeObj.normal,
290
+ // An outline (not a ring) so the offset gap stays transparent on any surface/mode.
291
+ !noOutlineOnFocus &&
292
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid', rounded && 'rounded-full', fullWidth && 'w-full', variant === 'default' && 'shadow-xs hover:shadow-none', active && 'ring-2 ring-current', className);
266
293
  }, [theme, variant, sizeObj, iconOnly, noOutlineOnFocus, rounded, fullWidth, active, className]);
267
- const loaderIcon = (jsxRuntime.jsx(Loader, { "data-testid": "btn-loader-icon", size: LOADER_SIZE[size] || 'xs', className: buildClassName((_c = LOADER_TEXT_CLASS[theme]) === null || _c === void 0 ? void 0 : _c[variant]) }));
294
+ // The spinner and loading text inherit the button's text colour.
295
+ const loaderIcon = (jsxRuntime.jsx(Loader, { "data-testid": "btn-loader-icon", size: LOADER_SIZE[size] || 'xs', className: "text-current" }));
268
296
  /**
269
297
  * SPECIAL CASE: iconOnly + loading → show spinner only
270
298
  */
271
299
  if (loading && iconOnly) {
272
- return (jsxRuntime.jsx(Component, { ref: ref, className: classes, "aria-busy": "true", "aria-pressed": active !== undefined ? active : undefined, ...elementProps, ...cleanRest, children: loaderIcon }));
300
+ return (jsxRuntime.jsx(Component, { ref: ref, className: classes, "aria-busy": "true", "aria-pressed": active !== undefined ? active : undefined, ...domProps, ...elementProps, children: loaderIcon }));
273
301
  }
274
- return (jsxRuntime.jsx(Component, { ref: ref, className: classes, "aria-busy": loading ? 'true' : undefined, "aria-pressed": active !== undefined ? active : undefined, ...elementProps, ...cleanRest, children: loading ? (jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-2", "aria-live": "polite", children: [loaderIcon, loadingText ? (jsxRuntime.jsx(TextContent, { className: buildClassName((_d = LOADER_TEXT_CLASS[theme]) === null || _d === void 0 ? void 0 : _d[variant]), children: loadingText })) : (children)] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [leftIcon && jsxRuntime.jsx("span", { className: "shrink-0 inline-flex items-center", children: leftIcon }), children, rightIcon && jsxRuntime.jsx("span", { className: "shrink-0 inline-flex items-center", children: rightIcon })] })) }));
302
+ return (jsxRuntime.jsx(Component, { ref: ref, className: classes, "aria-busy": loading ? 'true' : undefined, "aria-pressed": active !== undefined ? active : undefined, ...domProps, ...elementProps, children: loading ? (jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-2", "aria-live": "polite", children: [loaderIcon, loadingText ? (jsxRuntime.jsx(TextContent, { className: "text-current", children: loadingText })) : (children)] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [leftIcon && jsxRuntime.jsx("span", { className: "shrink-0 inline-flex items-center", children: leftIcon }), children, rightIcon && jsxRuntime.jsx("span", { className: "shrink-0 inline-flex items-center", children: rightIcon })] })) }));
275
303
  });
276
304
  Button.displayName = 'Button';
277
305
 
@@ -313,6 +341,45 @@ var SvgX = function SvgX(props) {
313
341
  })));
314
342
  };
315
343
 
344
+ const BREAKPOINT_QUERIES = {
345
+ medium: '(min-width: 768px)',
346
+ expanded: '(min-width: 1024px)',
347
+ };
348
+ // jsdom and some embedded webviews have no matchMedia; treat every query as
349
+ // unmatched there (the compact layout) rather than throwing.
350
+ const hasMatchMedia = () => typeof window !== 'undefined' && typeof window.matchMedia === 'function';
351
+ /** Live result of a CSS media query. `false` during server rendering or without `matchMedia`. */
352
+ const useMediaQuery = (query) => {
353
+ const subscribe = React.useCallback((onChange) => {
354
+ if (!hasMatchMedia())
355
+ return () => { };
356
+ const list = window.matchMedia(query);
357
+ list.addEventListener('change', onChange);
358
+ return () => list.removeEventListener('change', onChange);
359
+ }, [query]);
360
+ return React.useSyncExternalStore(subscribe, () => hasMatchMedia() && window.matchMedia(query).matches, () => false);
361
+ };
362
+ /** Current layout range; `compact` during server rendering. */
363
+ const useBreakpoint = () => {
364
+ const isMedium = useMediaQuery(BREAKPOINT_QUERIES.medium);
365
+ const isExpanded = useMediaQuery(BREAKPOINT_QUERIES.expanded);
366
+ if (isExpanded)
367
+ return 'expanded';
368
+ return isMedium ? 'medium' : 'compact';
369
+ };
370
+ /** Resolves a `ResponsiveValue` for the current viewport. */
371
+ const useResponsiveValue = (value) => {
372
+ var _a, _b, _c;
373
+ const breakpoint = useBreakpoint();
374
+ if (typeof value !== 'object' || value === null || !('base' in value))
375
+ return value;
376
+ if (breakpoint === 'expanded')
377
+ return (_b = (_a = value.lg) !== null && _a !== void 0 ? _a : value.md) !== null && _b !== void 0 ? _b : value.base;
378
+ if (breakpoint === 'medium')
379
+ return (_c = value.md) !== null && _c !== void 0 ? _c : value.base;
380
+ return value.base;
381
+ };
382
+
316
383
  const alignmentMap = {
317
384
  center: 'items-center justify-center h-full',
318
385
  start: 'justify-start h-full',
@@ -325,7 +392,8 @@ const panelAlignmentMap = {
325
392
  start: 'data-[closed]:-translate-x-1/2 rounded-r-2xl',
326
393
  end: 'data-[closed]:translate-x-1/2 rounded-l-2xl',
327
394
  top: 'data-[closed]:-translate-y-1/2 rounded-b-2xl',
328
- bottom: 'data-[closed]:translate-y-full rounded-t-2xl md:rounded-t-3xl max-h-[88vh] max-h-[88dvh]',
395
+ // Bottom sheets clear the home indicator on notched phones (0 elsewhere).
396
+ bottom: 'data-[closed]:translate-y-full rounded-t-2xl md:rounded-t-3xl max-h-[88vh] max-h-[88dvh] pb-[env(safe-area-inset-bottom,0px)]',
329
397
  };
330
398
  const panelSizeMap = {
331
399
  xs: 'max-w-xs',
@@ -336,7 +404,9 @@ const panelSizeMap = {
336
404
  '2xl': 'max-w-2xl',
337
405
  full: 'w-full',
338
406
  };
339
- const Drawer = ({ isOpen, onClose = () => { }, closeOnOutsideClick = true, align = 'end', size = 'md', backdrop = true, title, description, children, footer, footerSticky = false, panelClass = '', contentClass = '', titleClass = '', descriptionClass = '', titleSticky = false, showBackButton = false, showCloseButton = true, dragHandle, }) => {
407
+ const Drawer = ({ isOpen, onClose = () => { }, closeOnOutsideClick = true, align: alignProp = 'end', size: sizeProp = 'md', backdrop = true, title, description, children, footer, footerSticky = false, panelClass = '', contentClass = '', titleClass = '', descriptionClass = '', titleSticky = false, showBackButton = false, showCloseButton = true, dragHandle, }) => {
408
+ const align = useResponsiveValue(alignProp);
409
+ const size = useResponsiveValue(sizeProp);
340
410
  const showTitleSection = Boolean(title || description || showBackButton || showCloseButton);
341
411
  const shouldShowDragHandle = dragHandle !== null && dragHandle !== void 0 ? dragHandle : align === 'bottom';
342
412
  const [viewportHeight, setViewportHeight] = React.useState(null);
@@ -595,7 +665,7 @@ const Banner = React.forwardRef((props, ref) => {
595
665
  const { type = 'info', iconSize = 'sm', children, icon, removable = false, onRemove, className, ...restProps } = props;
596
666
  const DefaultIcon = IconMap[type];
597
667
  const iconNode = icon !== undefined ? (icon) : (jsxRuntime.jsx(DefaultIcon, { "data-testid": `banner-${type}-icon`, className: buildClassName('shrink-0', sizeMap[iconSize]) }));
598
- return (jsxRuntime.jsxs("div", { ref: ref, className: buildClassName('text-sm rounded-md flex w-full p-3 items-center gap-3 ring-1 ring-inset', TypeClassMap[type], className), "data-testid": `banner-${type}`, ...restProps, children: [iconNode, jsxRuntime.jsx("div", { className: "flex items-center w-full justify-between", children: children }), removable && (jsxRuntime.jsx(Button, { iconOnly: true, "aria-label": "Dismiss banner", size: "xs", variant: "plain", theme: "secondary", className: "hover:bg-black/10 dark:hover:bg-white/10 shrink-0", onClick: () => onRemove === null || onRemove === void 0 ? void 0 : onRemove(), "data-testid": "banner-remove-btn", children: jsxRuntime.jsx(SvgX, { className: "size-4.5", "data-testid": "banner-remove-icon" }) }))] }));
668
+ return (jsxRuntime.jsxs("div", { ref: ref, className: buildClassName('text-sm rounded-md flex w-full p-3 items-center gap-3 ring-1 ring-inset', TypeClassMap[type], className), "data-testid": `banner-${type}`, ...restProps, children: [iconNode, jsxRuntime.jsx("div", { className: "flex items-center w-full justify-between", children: children }), removable && (jsxRuntime.jsx(Button, { iconOnly: true, "aria-label": "Dismiss banner", size: "xs", variant: "plain", className: "text-current focus-visible:outline-current shrink-0", onClick: () => onRemove === null || onRemove === void 0 ? void 0 : onRemove(), "data-testid": "banner-remove-btn", children: jsxRuntime.jsx(SvgX, { className: "size-4.5", "data-testid": "banner-remove-icon" }) }))] }));
599
669
  });
600
670
  Banner.displayName = 'Banner';
601
671
 
@@ -651,20 +721,23 @@ const BodyText = React.forwardRef((props, ref) => {
651
721
  });
652
722
  BodyText.displayName = 'BodyText';
653
723
 
654
- function BottomNavigation({ items, activeId, onChange, fixed = true, showLabels = 'always', theme = 'light', activeIndicatorStyle = 'line', containerClass = '', itemClass = '', }) {
724
+ function BottomNavigation({ items, activeId, onChange, fixed = true, showLabels = 'always', theme = 'light', activeIndicatorStyle = 'line', containerClass = '', itemClass = '', linkAs = 'a', linkHrefProp = 'href', ariaLabel = 'Bottom Navigation', hidden = false, }) {
655
725
  const handleTabClick = (item) => {
656
726
  var _a;
657
727
  if (item.disabled)
658
728
  return;
659
729
  (_a = item.onClick) === null || _a === void 0 ? void 0 : _a.call(item);
660
- onChange(item.id);
730
+ onChange === null || onChange === void 0 ? void 0 : onChange(item.id);
661
731
  };
732
+ // Removed from the tree, not just covered, so hidden tabs can't take focus.
733
+ if (hidden)
734
+ return null;
662
735
  const themeStyles = {
663
736
  light: 'bg-white dark:bg-gray-900 border-t border-[var(--ui-border)] text-gray-600 dark:text-gray-400',
664
737
  dark: 'bg-gray-900 text-gray-400 border-t border-gray-800',
665
738
  primary: 'bg-blue-600 text-blue-100 border-t border-blue-700',
666
739
  }[theme];
667
- return (jsxRuntime.jsx("nav", { "aria-label": "Bottom Navigation", className: buildClassName('w-full flex items-center justify-around select-none shadow-lg transition-colors relative', fixed && 'fixed bottom-0 left-0 right-0 z-40', 'pb-[env(safe-area-inset-bottom,6px)] px-2', themeStyles, containerClass), "data-testid": "bottom-navigation", children: items.map((item) => {
740
+ return (jsxRuntime.jsx("nav", { "aria-label": ariaLabel, className: buildClassName('w-full flex items-center justify-around select-none shadow-lg transition-colors relative', fixed && 'fixed bottom-0 left-0 right-0 z-40', 'pb-[env(safe-area-inset-bottom,6px)] px-2', themeStyles, containerClass), "data-testid": "bottom-navigation", children: items.map((item) => {
668
741
  const isActive = item.id === activeId;
669
742
  const showLabelText = showLabels === 'always' || (showLabels === 'active' && isActive);
670
743
  const activeItemColor = {
@@ -687,11 +760,17 @@ function BottomNavigation({ items, activeId, onChange, fixed = true, showLabels
687
760
  info: 'bg-blue-500 text-white',
688
761
  success: 'bg-emerald-500 text-white',
689
762
  }[item.badgeVariant || 'danger'];
690
- return (jsxRuntime.jsxs("button", { type: "button", disabled: item.disabled, onClick: () => handleTabClick(item), className: buildClassName('flex-1 flex flex-col items-center justify-center pt-2 pb-1.5 px-1 transition-all duration-200 relative cursor-pointer group', item.disabled && 'opacity-40 cursor-not-allowed pointer-events-none', isActive ? activeItemColor : inactiveItemColor, itemClass), "data-testid": `bottom-nav-item-${item.id}`, "aria-selected": isActive, children: [activeIndicatorStyle === 'line' && isActive && (jsxRuntime.jsx("span", { className: buildClassName('absolute -top-[1px] h-0.75 w-9 rounded-full transition-all duration-200 z-10', theme === 'primary' ? 'bg-white' : 'bg-blue-600 dark:bg-blue-400') })), jsxRuntime.jsxs("div", { className: buildClassName('relative inline-flex items-center justify-center transition-all duration-200', activeIndicatorStyle === 'pill'
763
+ // A disabled item stays a button: a link can't be disabled.
764
+ const isLink = Boolean(item.href) && !item.disabled;
765
+ const ItemElement = isLink ? linkAs : 'button';
766
+ const elementProps = isLink
767
+ ? { [linkHrefProp]: item.href }
768
+ : { type: 'button', disabled: item.disabled };
769
+ return (jsxRuntime.jsxs(ItemElement, { ...elementProps, onClick: () => handleTabClick(item), className: buildClassName('flex-1 flex flex-col items-center justify-center pt-2 pb-1.5 px-1 transition-all duration-200 relative cursor-pointer group', item.disabled && 'opacity-40 cursor-not-allowed pointer-events-none', isActive ? activeItemColor : inactiveItemColor, itemClass), "data-testid": `bottom-nav-item-${item.id}`, "aria-current": isActive ? 'page' : undefined, "aria-label": !showLabelText && typeof item.label === 'string' ? item.label : undefined, children: [activeIndicatorStyle === 'line' && isActive && (jsxRuntime.jsx("span", { className: buildClassName('absolute -top-[1px] h-0.75 w-9 rounded-full transition-all duration-200 z-10', theme === 'primary' ? 'bg-white' : 'bg-blue-600 dark:bg-blue-400') })), jsxRuntime.jsxs("div", { className: buildClassName('relative inline-flex items-center justify-center transition-all duration-200', activeIndicatorStyle === 'pill'
691
770
  ? buildClassName('px-4 py-1 rounded-full', isActive
692
771
  ? pillActiveStyles
693
772
  : 'group-hover:bg-gray-100/50 dark:group-hover:bg-gray-800/40')
694
- : 'px-1 py-0.5'), children: [jsxRuntime.jsx("span", { className: "text-xl transition-transform duration-150 group-active:scale-95", children: isActive && item.activeIcon ? item.activeIcon : item.icon }), item.badge !== undefined && item.badge !== null && (jsxRuntime.jsx("span", { className: buildClassName('absolute -top-1 -right-1 min-w-[16px] h-4 px-1 rounded-full text-[10px] font-bold flex items-center justify-center shadow-xs z-10', badgeColor), "data-testid": `bottom-nav-badge-${item.id}`, children: item.badge }))] }), showLabelText && (jsxRuntime.jsx("span", { className: buildClassName('text-[11px] leading-tight mt-1 truncate max-w-full transition-colors duration-150', isActive ? 'font-bold' : 'font-medium opacity-90'), children: item.label })), activeIndicatorStyle === 'dot' && isActive && (jsxRuntime.jsx("span", { className: buildClassName('size-1.5 rounded-full mt-1 transition-all duration-200', theme === 'primary' ? 'bg-white' : 'bg-blue-600 dark:bg-blue-400') }))] }, item.id));
773
+ : 'px-1 py-0.5'), children: [jsxRuntime.jsx("span", { className: "text-xl transition-transform duration-150 group-active:scale-95", "aria-hidden": "true", children: isActive && item.activeIcon ? item.activeIcon : item.icon }), item.badge !== undefined && item.badge !== null && (jsxRuntime.jsx("span", { className: buildClassName('absolute -top-1 -right-1 min-w-[16px] h-4 px-1 rounded-full text-[10px] font-bold flex items-center justify-center shadow-xs z-10', badgeColor), "data-testid": `bottom-nav-badge-${item.id}`, children: item.badge }))] }), showLabelText && (jsxRuntime.jsx("span", { className: buildClassName('text-[11px] leading-tight mt-1 truncate max-w-full transition-colors duration-150', isActive ? 'font-bold' : 'font-medium opacity-90'), children: item.label })), activeIndicatorStyle === 'dot' && isActive && (jsxRuntime.jsx("span", { className: buildClassName('size-1.5 rounded-full mt-1 transition-all duration-200', theme === 'primary' ? 'bg-white' : 'bg-blue-600 dark:bg-blue-400') }))] }, item.id));
695
774
  }) }));
696
775
  }
697
776
  BottomNavigation.displayName = 'BottomNavigation';
@@ -2335,77 +2414,165 @@ function PinInput({ length = 4, value: externalValue, onChange, onComplete, mask
2335
2414
  }
2336
2415
  PinInput.displayName = 'PinInput';
2337
2416
 
2338
- function PullArrowIcon({ progress }) {
2339
- const rotation = Math.min(180, progress * 180);
2340
- return (jsxRuntime.jsx("svg", { className: "size-4 text-blue-600 dark:text-blue-400 transition-transform duration-150", style: { transform: `rotate(${rotation}deg)` }, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 14l-7 7m0 0l-7-7m7 7V3" }) }));
2417
+ // Resistance applied to the finger's travel, so the pull feels elastic.
2418
+ const RESISTANCE = 0.45;
2419
+ // Finger travel (px) before deciding whether a gesture is a pull or a sideways swipe.
2420
+ const AXIS_LOCK_SLOP = 8;
2421
+ // Overlay badge size and the gap it rests at below the container's top edge.
2422
+ const BADGE_SIZE = 40;
2423
+ const BADGE_TOP_GAP = 10;
2424
+ function PullArrowIcon({ progress, className }) {
2425
+ return (jsxRuntime.jsx("svg", { className: className, style: { transform: `rotate(${Math.min(180, progress * 180)}deg)` }, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 14l-7 7m0 0l-7-7m7 7V3" }) }));
2341
2426
  }
2342
- function PullToRefresh({ onRefresh, pullThreshold = 70, maxPullDistance = 120, disabled = false, refreshing: externalRefreshing, pullingContent, refreshingContent, children, className = '', contentClass = '', }) {
2427
+ function PullToRefresh({ onRefresh, onError, variant = 'push', pullThreshold = 70, maxPullDistance = 120, disabled = false, refreshing: externalRefreshing, pullingContent, refreshingContent, pullLabel = 'Pull down to refresh', releaseLabel = 'Release to refresh', refreshingLabel = 'Refreshing...', refreshedLabel = 'Content refreshed', children, className = '', contentClass = '', }) {
2343
2428
  const containerRef = React.useRef(null);
2344
- const startYRef = React.useRef(0);
2345
- const isPullingRef = React.useRef(false);
2346
2429
  const [pullDistance, setPullDistance] = React.useState(0);
2430
+ const [isDragging, setIsDragging] = React.useState(false);
2347
2431
  const [internalRefreshing, setInternalRefreshing] = React.useState(false);
2432
+ const [announcement, setAnnouncement] = React.useState('');
2348
2433
  const isRefreshing = externalRefreshing !== undefined ? externalRefreshing : internalRefreshing;
2349
- const handleTouchStart = React.useCallback((e) => {
2350
- if (disabled || isRefreshing)
2351
- return;
2352
- const container = containerRef.current;
2353
- if (container && container.scrollTop === 0) {
2354
- startYRef.current = e.touches[0].clientY;
2355
- isPullingRef.current = true;
2356
- }
2357
- }, [disabled, isRefreshing]);
2358
- const handleTouchMove = React.useCallback((e) => {
2359
- if (!isPullingRef.current || disabled || isRefreshing)
2360
- return;
2434
+ // The touch listeners are attached once, natively (see below), so they read
2435
+ // the latest props and state through this ref instead of stale closures.
2436
+ const latest = React.useRef({
2437
+ disabled,
2438
+ isRefreshing,
2439
+ pullThreshold,
2440
+ maxPullDistance,
2441
+ onRefresh,
2442
+ onError,
2443
+ refreshedLabel,
2444
+ });
2445
+ React.useLayoutEffect(() => {
2446
+ latest.current = {
2447
+ disabled,
2448
+ isRefreshing,
2449
+ pullThreshold,
2450
+ maxPullDistance,
2451
+ onRefresh,
2452
+ onError,
2453
+ refreshedLabel,
2454
+ };
2455
+ });
2456
+ React.useEffect(() => {
2361
2457
  const container = containerRef.current;
2362
- if (!container || container.scrollTop > 0)
2458
+ if (!container)
2363
2459
  return;
2364
- const currentY = e.touches[0].clientY;
2365
- const rawDistance = currentY - startYRef.current;
2366
- if (rawDistance > 0) {
2367
- // Rubber-band resistance formula
2368
- const dampened = Math.min(maxPullDistance, rawDistance * 0.45);
2369
- setPullDistance(dampened);
2370
- if (e.cancelable) {
2371
- e.preventDefault();
2372
- }
2373
- }
2374
- else {
2375
- setPullDistance(0);
2376
- }
2377
- }, [disabled, isRefreshing, maxPullDistance]);
2378
- const handleTouchEnd = React.useCallback(async () => {
2379
- if (!isPullingRef.current || disabled || isRefreshing)
2380
- return;
2381
- isPullingRef.current = false;
2382
- if (pullDistance >= pullThreshold) {
2383
- setPullDistance(pullThreshold);
2460
+ let mounted = true;
2461
+ let pulling = false;
2462
+ let axis = 'pending';
2463
+ let startX = 0;
2464
+ let startY = 0;
2465
+ let distance = 0;
2466
+ const setDistance = (next) => {
2467
+ distance = next;
2468
+ setPullDistance(next);
2469
+ };
2470
+ // Ends a gesture without refreshing: a cancelled touch, a sideways swipe,
2471
+ // or content that scrolled away from the top.
2472
+ const reset = () => {
2473
+ pulling = false;
2474
+ setIsDragging(false);
2475
+ setDistance(0);
2476
+ };
2477
+ const refresh = async () => {
2478
+ const { pullThreshold: threshold, onRefresh: run, onError: report } = latest.current;
2479
+ setDistance(threshold);
2384
2480
  setInternalRefreshing(true);
2385
2481
  try {
2386
- await onRefresh();
2482
+ await run();
2483
+ if (mounted)
2484
+ setAnnouncement(latest.current.refreshedLabel);
2485
+ }
2486
+ catch (error) {
2487
+ report === null || report === void 0 ? void 0 : report(error);
2387
2488
  }
2388
2489
  finally {
2389
- setInternalRefreshing(false);
2390
- setPullDistance(0);
2490
+ if (mounted) {
2491
+ setInternalRefreshing(false);
2492
+ setDistance(0);
2493
+ }
2391
2494
  }
2392
- }
2393
- else {
2394
- setPullDistance(0);
2395
- }
2396
- }, [disabled, isRefreshing, onRefresh, pullDistance, pullThreshold]);
2495
+ };
2496
+ const handleTouchStart = (e) => {
2497
+ var _a, _b;
2498
+ const { disabled: isDisabled, isRefreshing: busy } = latest.current;
2499
+ if (isDisabled || busy || container.scrollTop > 0 || e.touches.length !== 1)
2500
+ return;
2501
+ startX = (_a = e.touches[0].clientX) !== null && _a !== void 0 ? _a : 0;
2502
+ startY = (_b = e.touches[0].clientY) !== null && _b !== void 0 ? _b : 0;
2503
+ axis = 'pending';
2504
+ pulling = true;
2505
+ setAnnouncement('');
2506
+ setIsDragging(true);
2507
+ setDistance(0);
2508
+ };
2509
+ const handleTouchMove = (e) => {
2510
+ var _a, _b, _c, _d;
2511
+ if (!pulling)
2512
+ return;
2513
+ if (latest.current.disabled || latest.current.isRefreshing || container.scrollTop > 0) {
2514
+ reset();
2515
+ return;
2516
+ }
2517
+ const dx = ((_b = (_a = e.touches[0]) === null || _a === void 0 ? void 0 : _a.clientX) !== null && _b !== void 0 ? _b : 0) - startX;
2518
+ const dy = ((_d = (_c = e.touches[0]) === null || _c === void 0 ? void 0 : _c.clientY) !== null && _d !== void 0 ? _d : 0) - startY;
2519
+ if (axis === 'pending') {
2520
+ if (Math.abs(dx) < AXIS_LOCK_SLOP && Math.abs(dy) < AXIS_LOCK_SLOP)
2521
+ return;
2522
+ axis = dy > 0 && Math.abs(dy) > Math.abs(dx) ? 'vertical' : 'horizontal';
2523
+ // A sideways swipe or an upward scroll belongs to the content, not to us.
2524
+ if (axis === 'horizontal') {
2525
+ reset();
2526
+ return;
2527
+ }
2528
+ }
2529
+ const next = Math.min(latest.current.maxPullDistance, Math.max(0, dy) * RESISTANCE);
2530
+ setDistance(next);
2531
+ // Needs the non-passive listener: stops the browser's own pull-to-refresh/overscroll.
2532
+ if (next > 0 && e.cancelable)
2533
+ e.preventDefault();
2534
+ };
2535
+ const handleTouchEnd = () => {
2536
+ if (!pulling)
2537
+ return;
2538
+ pulling = false;
2539
+ setIsDragging(false);
2540
+ if (distance >= latest.current.pullThreshold)
2541
+ void refresh();
2542
+ else
2543
+ setDistance(0);
2544
+ };
2545
+ // React registers touch listeners as passive, where preventDefault() is
2546
+ // ignored, so these are attached natively with touchmove non-passive.
2547
+ container.addEventListener('touchstart', handleTouchStart, { passive: true });
2548
+ container.addEventListener('touchmove', handleTouchMove, { passive: false });
2549
+ container.addEventListener('touchend', handleTouchEnd);
2550
+ container.addEventListener('touchcancel', reset);
2551
+ return () => {
2552
+ mounted = false;
2553
+ container.removeEventListener('touchstart', handleTouchStart);
2554
+ container.removeEventListener('touchmove', handleTouchMove);
2555
+ container.removeEventListener('touchend', handleTouchEnd);
2556
+ container.removeEventListener('touchcancel', reset);
2557
+ };
2558
+ }, []);
2397
2559
  const progress = Math.min(1, pullDistance / pullThreshold);
2398
2560
  const isReady = pullDistance >= pullThreshold;
2399
- return (jsxRuntime.jsxs("div", { ref: containerRef, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, className: buildClassName('relative overflow-y-auto overscroll-none', className), "data-testid": "pull-to-refresh-container", children: [jsxRuntime.jsx("div", { className: "left-0 right-0 top-0 flex items-center justify-center pointer-events-none transition-all duration-150 z-20", style: {
2400
- height: isRefreshing ? `${pullThreshold}px` : `${pullDistance}px`,
2401
- opacity: pullDistance > 0 || isRefreshing ? 1 : 0,
2402
- }, "data-testid": "pull-to-refresh-indicator", children: jsxRuntime.jsx("div", { className: "flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-white dark:bg-gray-800 border border-[var(--ui-border)] shadow-md text-xs font-semibold text-gray-700 dark:text-gray-200", children: isRefreshing ? (refreshingContent || (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Loader, { size: "sm" }), jsxRuntime.jsx("span", { children: "Refreshing..." })] }))) : pullingContent ? (pullingContent) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(PullArrowIcon, { progress: progress }), jsxRuntime.jsx("span", { children: isReady ? 'Release to refresh' : 'Pull down to refresh' })] })) }) }), jsxRuntime.jsx("div", { className: buildClassName('transition-transform duration-150', contentClass), style: {
2403
- transform: isRefreshing
2404
- ? `translateY(${pullThreshold}px)`
2405
- : pullDistance > 0
2406
- ? `translateY(${pullDistance}px)`
2407
- : 'translateY(0px)',
2408
- }, "data-testid": "pull-to-refresh-content", children: children })] }));
2561
+ const offset = isRefreshing ? pullThreshold : pullDistance;
2562
+ // Follow the finger exactly while dragging; animate only the settle afterwards.
2563
+ const motionClass = isDragging
2564
+ ? 'transition-none'
2565
+ : 'transition-transform duration-200 ease-out motion-reduce:transition-none';
2566
+ return (jsxRuntime.jsxs("div", { ref: containerRef, className: buildClassName('relative overflow-y-auto overscroll-none', className), "data-testid": "pull-to-refresh-container", children: [jsxRuntime.jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: isRefreshing ? refreshingLabel : announcement }), variant === 'overlay' ? (jsxRuntime.jsx("div", { className: buildClassName('pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center', motionClass), style: {
2567
+ transform: `translateY(${offset - BADGE_SIZE + BADGE_TOP_GAP}px)`,
2568
+ opacity: offset > 0 ? 1 : 0,
2569
+ }, "aria-hidden": "true", "data-testid": "pull-to-refresh-indicator", children: jsxRuntime.jsx("div", { className: "grid size-10 place-items-center rounded-full bg-[var(--ui-primary)] text-white shadow-lg", children: isRefreshing
2570
+ ? (refreshingContent !== null && refreshingContent !== void 0 ? refreshingContent : jsxRuntime.jsx(Loader, { size: "xs", className: "text-white" }))
2571
+ : (pullingContent !== null && pullingContent !== void 0 ? pullingContent : jsxRuntime.jsx(PullArrowIcon, { progress: progress, className: "size-4" })) }) })) : (
2572
+ // Absolutely positioned, so the content's own translate is the only shift.
2573
+ jsxRuntime.jsx("div", { className: "pointer-events-none absolute inset-x-0 top-0 z-20 flex items-center justify-center overflow-hidden", style: { height: `${offset}px`, opacity: offset > 0 ? 1 : 0 }, "aria-hidden": "true", "data-testid": "pull-to-refresh-indicator", children: jsxRuntime.jsx("div", { className: "flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-white dark:bg-gray-800 border border-[var(--ui-border)] shadow-md text-xs font-semibold text-gray-700 dark:text-gray-200", children: isRefreshing
2574
+ ? (refreshingContent !== null && refreshingContent !== void 0 ? refreshingContent : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Loader, { size: "sm" }), jsxRuntime.jsx("span", { children: refreshingLabel })] })))
2575
+ : (pullingContent !== null && pullingContent !== void 0 ? pullingContent : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(PullArrowIcon, { progress: progress, className: "size-4 text-[var(--ui-primary)] dark:text-blue-400" }), jsxRuntime.jsx("span", { children: isReady ? releaseLabel : pullLabel })] }))) }) })), jsxRuntime.jsx("div", { className: buildClassName(variant === 'push' && motionClass, contentClass), style: variant === 'push' ? { transform: `translateY(${offset}px)` } : undefined, "data-testid": "pull-to-refresh-content", children: children })] }));
2409
2576
  }
2410
2577
  PullToRefresh.displayName = 'PullToRefresh';
2411
2578
 
@@ -3318,12 +3485,23 @@ function ChevronLeftIcon$1() {
3318
3485
  function SearchIcon$1() {
3319
3486
  return (jsxRuntime.jsx("svg", { className: "size-4 text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" }) }));
3320
3487
  }
3488
+ function MoreIcon() {
3489
+ return (jsxRuntime.jsxs("svg", { className: "size-5", fill: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: [jsxRuntime.jsx("circle", { cx: "12", cy: "5", r: "1.75" }), jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "1.75" }), jsxRuntime.jsx("circle", { cx: "12", cy: "19", r: "1.75" })] }));
3490
+ }
3321
3491
  function ClearIcon() {
3322
3492
  return (jsxRuntime.jsx("svg", { className: "size-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }));
3323
3493
  }
3324
- function MobileHeader({ title, subtitle, titleAlign = 'center', onBack, backLabel = 'Back', leading, actions, searchable = false, searchValue: externalSearchValue, onSearchChange, searchPlaceholder = 'Search...', sticky = true, transparent = false, bottomSlot, className = '', }) {
3494
+ function MobileHeader({ title, subtitle, titleAlign = 'center', onBack, backLabel = 'Back', leading, actions, maxVisibleActions, overflowLabel = 'More actions', trailing, searchable = false, searchValue: externalSearchValue, onSearchChange, searchPlaceholder = 'Search...', sticky = true, transparent = false, bottomSlot, className = '', }) {
3325
3495
  const [isSearchActive, setIsSearchActive] = React.useState(false);
3326
3496
  const [internalSearchValue, setInternalSearchValue] = React.useState('');
3497
+ const [isOverflowOpen, setIsOverflowOpen] = React.useState(false);
3498
+ // The overflow button takes one of the visible slots.
3499
+ const allActions = actions !== null && actions !== void 0 ? actions : [];
3500
+ const shouldOverflow = maxVisibleActions !== undefined && allActions.length > maxVisibleActions;
3501
+ const visibleActions = shouldOverflow
3502
+ ? allActions.slice(0, Math.max(maxVisibleActions - 1, 0))
3503
+ : allActions;
3504
+ const overflowActions = shouldOverflow ? allActions.slice(visibleActions.length) : [];
3327
3505
  const searchValue = externalSearchValue !== undefined ? externalSearchValue : internalSearchValue;
3328
3506
  const handleSearchInput = (e) => {
3329
3507
  const val = e.target.value;
@@ -3338,14 +3516,20 @@ function MobileHeader({ title, subtitle, titleAlign = 'center', onBack, backLabe
3338
3516
  ? 'bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-[var(--ui-border-muted)]'
3339
3517
  : 'bg-white dark:bg-gray-900 border-b border-[var(--ui-border)] shadow-xs', 'pt-[env(safe-area-inset-top,0px)]', className), "data-testid": "mobile-header", children: [jsxRuntime.jsx("div", { className: "h-14 px-3.5 flex items-center justify-between gap-3 relative", children: searchable && isSearchActive ? (jsxRuntime.jsxs("div", { className: "flex-1 flex items-center gap-2", "data-testid": "mobile-header-search-container", children: [jsxRuntime.jsxs("div", { className: "relative flex-1 flex items-center", children: [jsxRuntime.jsx("span", { className: "absolute left-3 pointer-events-none", children: jsxRuntime.jsx(SearchIcon$1, {}) }), jsxRuntime.jsx("input", { type: "text", value: searchValue, onChange: handleSearchInput, placeholder: searchPlaceholder, autoFocus: true, className: "w-full pl-9 pr-8 py-1.5 text-xs bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white rounded-full border border-transparent focus:border-blue-500 focus:outline-hidden transition-all", "data-testid": "mobile-header-search-input" }), searchValue && (jsxRuntime.jsx("button", { type: "button", onClick: handleClearSearch, "aria-label": "Clear search", className: "absolute right-2.5 p-0.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 cursor-pointer", children: jsxRuntime.jsx(ClearIcon, {}) }))] }), jsxRuntime.jsx("button", { type: "button", onClick: () => setIsSearchActive(false), className: "text-xs font-semibold text-blue-600 dark:text-blue-400 px-2 py-1.5 rounded-lg hover:bg-blue-50 dark:hover:bg-blue-950/40 cursor-pointer", children: "Cancel" })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "flex items-center gap-1 min-w-[40px] shrink-0 z-10", children: onBack ? (jsxRuntime.jsxs("button", { type: "button", onClick: onBack, "aria-label": backLabel, className: "flex items-center gap-0.5 text-blue-600 dark:text-blue-400 hover:text-blue-700 p-1 -ml-1 rounded-xl transition-colors cursor-pointer", "data-testid": "mobile-header-back-btn", children: [jsxRuntime.jsx(ChevronLeftIcon$1, {}), jsxRuntime.jsx("span", { className: "text-xs font-semibold hidden sm:inline", children: backLabel })] })) : (leading) }), jsxRuntime.jsxs("div", { className: buildClassName('flex-1 min-w-0 px-2', titleAlign === 'center'
3340
3518
  ? 'absolute inset-x-12 text-center pointer-events-none flex flex-col items-center justify-center'
3341
- : 'text-left'), children: [title && (jsxRuntime.jsx("div", { className: "text-sm font-bold text-gray-900 dark:text-white truncate pointer-events-auto", "data-testid": "mobile-header-title", children: title })), subtitle && (jsxRuntime.jsx("div", { className: "text-[11px] text-gray-500 dark:text-gray-400 truncate pointer-events-auto", "data-testid": "mobile-header-subtitle", children: subtitle }))] }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1 min-w-[40px] shrink-0 justify-end z-10", children: [searchable && (jsxRuntime.jsx("button", { type: "button", onClick: () => setIsSearchActive(true), "aria-label": "Search", className: "p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl transition-colors cursor-pointer", "data-testid": "mobile-header-search-toggle", children: jsxRuntime.jsx(SearchIcon$1, {}) })), actions === null || actions === void 0 ? void 0 : actions.map((action) => {
3519
+ : 'text-left'), children: [title && (jsxRuntime.jsx("div", { className: "text-sm font-bold text-gray-900 dark:text-white truncate pointer-events-auto", "data-testid": "mobile-header-title", children: title })), subtitle && (jsxRuntime.jsx("div", { className: "text-[11px] text-gray-500 dark:text-gray-400 truncate pointer-events-auto", "data-testid": "mobile-header-subtitle", children: subtitle }))] }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1 min-w-[40px] shrink-0 justify-end z-10", children: [searchable && (jsxRuntime.jsx("button", { type: "button", onClick: () => setIsSearchActive(true), "aria-label": "Search", className: "p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl transition-colors cursor-pointer", "data-testid": "mobile-header-search-toggle", children: jsxRuntime.jsx(SearchIcon$1, {}) })), visibleActions.map((action) => {
3342
3520
  const badgeColor = {
3343
3521
  danger: 'bg-red-500 text-white',
3344
3522
  info: 'bg-blue-500 text-white',
3345
3523
  success: 'bg-emerald-500 text-white',
3346
3524
  }[action.badgeVariant || 'danger'];
3347
3525
  return (jsxRuntime.jsxs("button", { type: "button", disabled: action.disabled, onClick: action.onClick, "aria-label": action.label, className: buildClassName('p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl transition-colors relative cursor-pointer', action.disabled && 'opacity-40 cursor-not-allowed pointer-events-none'), "data-testid": `mobile-header-action-${action.id}`, children: [jsxRuntime.jsx("span", { className: "text-base", children: action.icon }), action.badge !== undefined && action.badge !== null && (jsxRuntime.jsx("span", { className: buildClassName('absolute top-1 right-1 min-w-[14px] h-3.5 px-0.5 rounded-full text-[9px] font-bold flex items-center justify-center shadow-xs', badgeColor), "data-testid": `mobile-header-badge-${action.id}`, children: action.badge }))] }, action.id));
3348
- })] })] })) }), bottomSlot && (jsxRuntime.jsx("div", { className: "px-3 py-2 border-t border-[var(--ui-border-muted)]", "data-testid": "mobile-header-bottom-slot", children: bottomSlot }))] }));
3526
+ }), overflowActions.length > 0 && (jsxRuntime.jsx("button", { type: "button", onClick: () => setIsOverflowOpen(true), "aria-label": overflowLabel, "aria-haspopup": "dialog", "aria-expanded": isOverflowOpen, className: "p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl transition-colors cursor-pointer", "data-testid": "mobile-header-overflow", children: jsxRuntime.jsx(MoreIcon, {}) })), trailing] })] })) }), overflowActions.length > 0 && (jsxRuntime.jsx(ActionSheet, { isOpen: isOverflowOpen, onClose: () => setIsOverflowOpen(false), actions: overflowActions.map((action) => ({
3527
+ id: action.id,
3528
+ label: action.label,
3529
+ icon: action.icon,
3530
+ disabled: action.disabled,
3531
+ onClick: action.onClick,
3532
+ })) })), bottomSlot && (jsxRuntime.jsx("div", { className: "px-3 py-2 border-t border-[var(--ui-border-muted)]", "data-testid": "mobile-header-bottom-slot", children: bottomSlot }))] }));
3349
3533
  }
3350
3534
  MobileHeader.displayName = 'MobileHeader';
3351
3535
 
@@ -3848,9 +4032,18 @@ const sizeClasses$1 = {
3848
4032
  },
3849
4033
  };
3850
4034
  const SelectBox = React.forwardRef((props, forwardedRef) => {
3851
- const { labelKey = 'label', valueKey = 'value', name, id = name, options = EMPTY_ARRAY, groups, selected, size = 'md', label = null, labelWrapperClass = '', labelClass = '', labelHint = null, disabled = false, containerClass = '', onChange = NOOP, placeholder, dropdownContainerClass = 'z-20', multiple = false, maxSelection, showSelectAll = false, immediate = true, showErrorMessage = true, error = null, helperText, leftGroup, renderOption, async = false, searchable = true, required = false, allowAdd = false, addNewText = 'Create', onAdd = NOOP, allowClear = false, allowFreeText = false, modalDropdown = false, noOptionsText = 'No options available', } = props;
4035
+ const { labelKey = 'label', valueKey = 'value', name, id = name, options = EMPTY_ARRAY, groups, selected: selectedProp, defaultSelected, size = 'md', label = null, labelWrapperClass = '', labelClass = '', labelHint = null, disabled = false, containerClass = '', onChange: onChangeProp = NOOP, placeholder, dropdownContainerClass = 'z-20', dropdownMinWidth, multiple = false, maxSelection, showSelectAll = false, immediate = true, showErrorMessage = true, error = null, helperText, leftGroup, renderOption, async = false, searchable = true, required = false, allowAdd = false, addNewText = 'Create', onAdd = NOOP, allowClear = false, allowFreeText = false, modalDropdown = false, noOptionsText = 'No options available', } = props;
4036
+ // Uncontrolled when `selected` is not passed: keep the selection internally.
4037
+ const isControlled = selectedProp !== undefined;
4038
+ const [internalSelected, setInternalSelected] = React.useState(defaultSelected !== null && defaultSelected !== void 0 ? defaultSelected : null);
4039
+ const selected = isControlled ? selectedProp : internalSelected;
4040
+ const onChange = React.useCallback((value) => {
4041
+ if (!isControlled)
4042
+ setInternalSelected(value);
4043
+ onChangeProp(value);
4044
+ }, [isControlled, onChangeProp]);
3852
4045
  if (props.asBottomSheet || props.mobileMode === 'sheet') {
3853
- return jsxRuntime.jsx(MobilePicker, { ...props });
4046
+ return jsxRuntime.jsx(MobilePicker, { ...props, selected: selected, onChange: onChange });
3854
4047
  }
3855
4048
  const hasError = Boolean(error);
3856
4049
  const [query, setQuery] = React.useState('');
@@ -4048,6 +4241,16 @@ const SelectBox = React.forwardRef((props, forwardedRef) => {
4048
4241
  }
4049
4242
  }, [isAllSelected, onChange, allOptions]);
4050
4243
  const hasLeftGroup = leftGroup != null && leftGroup !== undefined;
4244
+ // Reserve room for the chevron (and the clear button when shown) so long values
4245
+ // don't run underneath them.
4246
+ const showClearButton = allowClear && !multiple && !isEmpty(normalizedSelected);
4247
+ const rightPaddingClass = showClearButton
4248
+ ? 'pe-16'
4249
+ : size === 'lg'
4250
+ ? 'pe-11'
4251
+ : size === 'sm'
4252
+ ? 'pe-9'
4253
+ : 'pe-10';
4051
4254
  const hasFreeTextOption = Boolean(allowFreeText &&
4052
4255
  query.trim() &&
4053
4256
  !allOptions.some((opt) => { var _a; return ((_a = getDisplayValue(opt)) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === query.trim().toLowerCase(); }));
@@ -4060,7 +4263,7 @@ const SelectBox = React.forwardRef((props, forwardedRef) => {
4060
4263
  const isOptSelected = selectedValueSet.has(optVal);
4061
4264
  return (jsxRuntime.jsx(react.ComboboxOption, { disabled: option.disabled, value: option, className: "group rounded-lg py-1.5 px-3 select-none data-[focus]:bg-gray-100 dark:data-[focus]:bg-white/10 data-disabled:opacity-50 cursor-pointer data-disabled:pointer-events-none", children: renderOption ? (renderOption(option, Boolean(isOptSelected))) : (jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2 text-gray-900 dark:text-white group-data-[selected]:text-blue-600", children: [jsxRuntime.jsx("span", { className: "text-sm/6", children: getDisplayValue(option) }), jsxRuntime.jsx(SvgCheck, { className: "invisible size-5 dark:stroke-gray-300 group-data-[selected]:visible" })] })) }, optVal));
4062
4265
  };
4063
- return (jsxRuntime.jsxs("div", { className: buildClassName('group space-y-2', hasError && 'has-error', containerClass), ref: containerRef, children: [label && (jsxRuntime.jsxs("div", { className: buildClassName('flex items-center justify-between', labelWrapperClass), children: [jsxRuntime.jsx(Label, { className: labelClass, htmlFor: id, "aria-required": required, children: label }), labelHint && jsxRuntime.jsx(TextContent, { xsmall: true, children: labelHint })] })), jsxRuntime.jsxs(react.Combobox, { multiple: multiple, immediate: immediate, name: name, value: normalizedSelected, onChange: comboboxOnChangeWrapper, onClose: () => setQuery(''), disabled: disabled, by: (a, b) => (a && b ? getValue(a) === getValue(b) : a === b), children: [multiple ? (jsxRuntime.jsxs("div", { className: buildClassName(baseInputClass, 'flex flex-wrap gap-2 peer relative', hasLeftGroup ? (size === 'lg' ? 'ps-12' : size === 'sm' ? 'ps-8' : 'ps-10') : ''), ref: forwardedRef, children: [hasLeftGroup && (jsxRuntime.jsx("span", { className: buildClassName('absolute left-0 top-0 h-full flex items-center justify-center text-gray-400 dark:text-gray-500', currentSize.group), children: leftGroup })), isArray(normalizedSelected) &&
4266
+ return (jsxRuntime.jsxs("div", { className: buildClassName('group space-y-2', hasError && 'has-error', containerClass), ref: containerRef, children: [label && (jsxRuntime.jsxs("div", { className: buildClassName('flex items-center justify-between', labelWrapperClass), children: [jsxRuntime.jsx(Label, { className: labelClass, htmlFor: id, "aria-required": required, children: label }), labelHint && jsxRuntime.jsx(TextContent, { xsmall: true, children: labelHint })] })), jsxRuntime.jsxs(react.Combobox, { multiple: multiple, immediate: immediate, name: name, value: normalizedSelected, onChange: comboboxOnChangeWrapper, onClose: () => setQuery(''), disabled: disabled, by: (a, b) => (a && b ? getValue(a) === getValue(b) : a === b), children: [multiple ? (jsxRuntime.jsxs("div", { className: buildClassName(baseInputClass, 'flex flex-wrap gap-2 peer relative', rightPaddingClass, hasLeftGroup ? (size === 'lg' ? 'ps-12' : size === 'sm' ? 'ps-8' : 'ps-10') : ''), ref: forwardedRef, children: [hasLeftGroup && (jsxRuntime.jsx("span", { className: buildClassName('absolute left-0 top-0 h-full flex items-center justify-center text-gray-400 dark:text-gray-500', currentSize.group), children: leftGroup })), isArray(normalizedSelected) &&
4064
4267
  (normalizedSelected === null || normalizedSelected === void 0 ? void 0 : normalizedSelected.length) > 0 &&
4065
4268
  normalizedSelected.map((option, idx) => (jsxRuntime.jsx(Badge, { theme: "secondary", size: currentSize.badgeSize, removable: !disabled, onRemove: () => removeSelection(idx), children: getDisplayValue(option) }, `${getValue(option)}-${idx}`))), jsxRuntime.jsx(react.ComboboxInput, { className: "outline-none flex-1 dark:placeholder-gray-400 placeholder-gray-400 bg-transparent", "aria-label": name, value: query, autoComplete: "off", placeholder: (normalizedSelected === null || normalizedSelected === void 0 ? void 0 : normalizedSelected.length) ? '' : placeholder, onChange: (e) => {
4066
4269
  const val = e.target.value;
@@ -4100,7 +4303,9 @@ const SelectBox = React.forwardRef((props, forwardedRef) => {
4100
4303
  }
4101
4304
  }, onFocus: () => {
4102
4305
  setQuery('');
4103
- }, className: buildClassName(baseInputClass, hasLeftGroup ? (size === 'lg' ? 'ps-12' : size === 'sm' ? 'ps-8' : 'ps-10') : ''), disabled: disabled, id: id, readOnly: !searchable, ref: inputRef, "data-testid": "combobox-input" }), allowClear && !isEmpty(normalizedSelected) && (jsxRuntime.jsx(Button, { size: "xs", iconOnly: true, "aria-label": "Clear selection", theme: "secondary", variant: "plain", className: "group absolute inset-y-0 right-8 h-6 top-2", onClick: () => handleOnChangeInternal(multiple ? [] : null), disabled: disabled, "data-testid": "combobox-clear-button", noOutlineOnFocus: true, children: jsxRuntime.jsx(SvgX, { className: "size-4 stroke-gray-500 dark:stroke-gray-300" }) })), jsxRuntime.jsx(react.ComboboxButton, { className: "group absolute inset-y-0 right-0 px-2.5 flex items-center justify-center", "data-testid": "combobox-button", children: jsxRuntime.jsx(SvgChevronDown, { className: chevronIconClass }) })] })), jsxRuntime.jsxs(react.ComboboxOptions, { anchor: { to: 'bottom', gap: 5 }, as: "div", modal: modalDropdown, className: buildClassName('w-[var(--input-width)] rounded-lg border border-[var(--ui-border)] bg-white dark:bg-gray-800 p-1 shadow-lg max-h-80 overflow-y-auto z-50 [--anchor-gap:var(--spacing-1)]', '[--anchor-max-height:20rem]', 'origin-top border transition duration-200 ease-[cubic-bezier(0.4, 0.0, 0.2, 1)] data-[closed]:translate-y-[-15%] data-[closed]:opacity-0', dropdownContainerClass), transition: true, "data-testid": "combobox-options", children: [multiple && showSelectAll && allOptions.length > 0 && (jsxRuntime.jsxs("div", { className: "pb-1 mb-1 border-b border-[var(--ui-border-muted)] px-3 pt-1 flex items-center justify-between", children: [jsxRuntime.jsx("button", { type: "button", onClick: handleToggleSelectAll, className: "text-xs font-semibold text-blue-600 dark:text-blue-400 hover:underline cursor-pointer", children: isAllSelected ? 'Deselect All' : 'Select All' }), jsxRuntime.jsxs("span", { className: "text-[11px] font-medium text-gray-400", children: [Array.isArray(normalizedSelected) ? normalizedSelected.length : 0, " of", ' ', allOptions.length] })] })), optionsLoading ? (jsxRuntime.jsx(BodyText, { "data-testid": "options-loading", className: "flex items-center justify-center py-2 px-3 select-none", children: jsxRuntime.jsx(Loader, { size: "sm" }) })) : shouldShowTypeToSearch ? (jsxRuntime.jsx(BodyText, { className: "flex items-center justify-between py-2 px-3 select-none", children: "Type to search..." })) : (jsxRuntime.jsxs("div", { children: [allowFreeText &&
4306
+ }, className: buildClassName(baseInputClass, 'text-ellipsis', rightPaddingClass, hasLeftGroup ? (size === 'lg' ? 'ps-12' : size === 'sm' ? 'ps-8' : 'ps-10') : ''), disabled: disabled, id: id, readOnly: !searchable, ref: inputRef, "data-testid": "combobox-input" }), showClearButton && (jsxRuntime.jsx(Button, { size: "xs", iconOnly: true, "aria-label": "Clear selection", theme: "secondary", variant: "plain", className: "group absolute inset-y-0 right-8 h-6 top-2", onClick: () => handleOnChangeInternal(multiple ? [] : null), disabled: disabled, "data-testid": "combobox-clear-button", children: jsxRuntime.jsx(SvgX, { className: "size-4 stroke-gray-500 dark:stroke-gray-300" }) })), jsxRuntime.jsx(react.ComboboxButton, { className: "group absolute inset-y-0 right-0 px-2.5 flex items-center justify-center", "data-testid": "combobox-button", children: jsxRuntime.jsx(SvgChevronDown, { className: chevronIconClass }) })] })), jsxRuntime.jsxs(react.ComboboxOptions, { anchor: { to: 'bottom start', gap: 5, padding: 8 }, as: "div", modal: modalDropdown,
4307
+ // Matches the input width by default; min-width lets the panel grow wider than a narrow input.
4308
+ style: dropdownMinWidth != null ? { minWidth: dropdownMinWidth } : undefined, className: buildClassName('w-[var(--input-width)] rounded-lg border border-[var(--ui-border)] bg-white dark:bg-gray-800 p-1 shadow-lg max-h-80 overflow-y-auto z-50 [--anchor-gap:var(--spacing-1)]', '[--anchor-max-height:20rem]', 'origin-top border transition duration-200 ease-[cubic-bezier(0.4, 0.0, 0.2, 1)] data-[closed]:translate-y-[-15%] data-[closed]:opacity-0', dropdownContainerClass), transition: true, "data-testid": "combobox-options", children: [multiple && showSelectAll && allOptions.length > 0 && (jsxRuntime.jsxs("div", { className: "pb-1 mb-1 border-b border-[var(--ui-border-muted)] px-3 pt-1 flex items-center justify-between", children: [jsxRuntime.jsx("button", { type: "button", onClick: handleToggleSelectAll, className: "text-xs font-semibold text-blue-600 dark:text-blue-400 hover:underline cursor-pointer", children: isAllSelected ? 'Deselect All' : 'Select All' }), jsxRuntime.jsxs("span", { className: "text-[11px] font-medium text-gray-400", children: [Array.isArray(normalizedSelected) ? normalizedSelected.length : 0, " of", ' ', allOptions.length] })] })), optionsLoading ? (jsxRuntime.jsx(BodyText, { "data-testid": "options-loading", className: "flex items-center justify-center py-2 px-3 select-none", children: jsxRuntime.jsx(Loader, { size: "sm" }) })) : shouldShowTypeToSearch ? (jsxRuntime.jsx(BodyText, { className: "flex items-center justify-between py-2 px-3 select-none", children: "Type to search..." })) : (jsxRuntime.jsxs("div", { children: [allowFreeText &&
4104
4309
  query.trim() &&
4105
4310
  !allOptions.some((opt) => { var _a; return ((_a = getDisplayValue(opt)) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === query.trim().toLowerCase(); }) && (jsxRuntime.jsx(react.ComboboxOption, { value: createFreeTextOption(query.trim()), className: "group flex items-center gap-2 rounded-lg py-1.5 px-3 select-none text-sm data-[focus]:bg-blue-50 dark:data-[focus]:bg-blue-950/40 text-blue-600 dark:text-blue-400 font-medium cursor-pointer w-full", "data-testid": "freetext-option", children: jsxRuntime.jsxs("span", { children: ["Use \"", query.trim(), "\""] }) }, `freetext-${query.trim()}`)), filteredGroups
4106
4311
  ? filteredGroups.map((grp, gIdx) => (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-[11px] font-bold uppercase tracking-wider text-gray-400 dark:text-gray-500 px-3 py-1 select-none", children: grp.group }), grp.options.map(renderOptionItem)] }, gIdx)))
@@ -4300,14 +4505,14 @@ StatusPill.displayName = 'StatusPill';
4300
4505
 
4301
4506
  const ChevronUpIcon = ({ className }) => (jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: className, "aria-hidden": "true", children: jsxRuntime.jsx("polyline", { points: "18 15 12 9 6 15" }) }));
4302
4507
  const StickyActionBar = React.forwardRef((props, ref) => {
4303
- const { summaryContent, actionsContent, drawerContent, drawerTitle = 'Summary Details', position = 'fixed', containerMaxWidth = 'max-w-5xl', className, ...restProps } = props;
4508
+ const { summaryContent, actionsContent, drawerContent, drawerTitle = 'Summary Details', position = 'fixed', containerMaxWidth = 'max-w-5xl', safeArea = true, bottomOffset, className, style, ...restProps } = props;
4304
4509
  const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);
4305
4510
  const positionClass = position === 'fixed'
4306
4511
  ? 'fixed bottom-0 left-0 right-0 z-30'
4307
4512
  : position === 'sticky'
4308
4513
  ? 'sticky bottom-0 z-30'
4309
4514
  : 'relative z-10';
4310
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { ref: ref, className: buildClassName(positionClass, 'border-t border-[var(--ui-border)] bg-white/90 p-3.5 backdrop-blur-md shadow-lg dark:bg-gray-900/90 transition-all', className), "data-testid": "sticky-action-bar", ...restProps, children: jsxRuntime.jsxs("div", { className: buildClassName('mx-auto flex items-center justify-between gap-3', containerMaxWidth), children: [summaryContent && (jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: drawerContent ? (jsxRuntime.jsxs("button", { type: "button", onClick: () => setIsDrawerOpen(true), className: "flex items-center gap-2 text-left transition-opacity hover:opacity-80 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-primary-ring)] rounded-lg p-1 -m-1", "aria-label": "Expand summary details", "data-testid": "summary-trigger", children: [jsxRuntime.jsx("div", { children: summaryContent }), jsxRuntime.jsx(ChevronUpIcon, { className: "size-4 text-[var(--ui-primary)] dark:text-blue-400 shrink-0" })] })) : (jsxRuntime.jsx("div", { children: summaryContent })) })), actionsContent && (jsxRuntime.jsx("div", { className: "flex items-center gap-2 shrink-0 ml-auto", "data-testid": "actions-stack", children: actionsContent }))] }) }), drawerContent && (jsxRuntime.jsx(Drawer, { isOpen: isDrawerOpen, onClose: () => setIsDrawerOpen(false), align: "bottom", size: "full", title: drawerTitle, showBackButton: true, contentClass: "px-4 pb-6 max-h-[calc(100svh-6rem)] overflow-y-auto", children: drawerContent }))] }));
4515
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { ref: ref, className: buildClassName(positionClass, 'border-t border-[var(--ui-border)] bg-white/90 p-3.5 backdrop-blur-md shadow-lg dark:bg-gray-900/90 transition-all', safeArea && 'pb-[calc(0.875rem+env(safe-area-inset-bottom,0px))]', className), style: bottomOffset && position !== 'relative' ? { bottom: bottomOffset, ...style } : style, "data-testid": "sticky-action-bar", ...restProps, children: jsxRuntime.jsxs("div", { className: buildClassName('mx-auto flex items-center justify-between gap-3', containerMaxWidth), children: [summaryContent && (jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: drawerContent ? (jsxRuntime.jsxs("button", { type: "button", onClick: () => setIsDrawerOpen(true), className: "flex items-center gap-2 text-left transition-opacity hover:opacity-80 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-primary-ring)] rounded-lg p-1 -m-1", "aria-label": "Expand summary details", "data-testid": "summary-trigger", children: [jsxRuntime.jsx("div", { children: summaryContent }), jsxRuntime.jsx(ChevronUpIcon, { className: "size-4 text-[var(--ui-primary)] dark:text-blue-400 shrink-0" })] })) : (jsxRuntime.jsx("div", { children: summaryContent })) })), actionsContent && (jsxRuntime.jsx("div", { className: "flex items-center gap-2 shrink-0 ml-auto", "data-testid": "actions-stack", children: actionsContent }))] }) }), drawerContent && (jsxRuntime.jsx(Drawer, { isOpen: isDrawerOpen, onClose: () => setIsDrawerOpen(false), align: "bottom", size: "full", title: drawerTitle, showBackButton: true, contentClass: "px-4 pb-6 max-h-[calc(100svh-6rem)] overflow-y-auto", children: drawerContent }))] }));
4311
4516
  });
4312
4517
  StickyActionBar.displayName = 'StickyActionBar';
4313
4518
 
@@ -4570,7 +4775,7 @@ const Toast = ({ id, title, message, type, variant = 'accent', action, icon, sho
4570
4775
  const renderedIcon = icon !== null && icon !== void 0 ? icon : (variant === 'filled' ? filledIconMap[type] : iconMap[type]);
4571
4776
  return (jsxRuntime.jsx(react.Transition, { appear: true, show: true, enter: "transform transition duration-300 ease-out", enterFrom: enterFrom, enterTo: "opacity-100 translate-y-0 scale-100", leave: "transition duration-200 ease-in", leaveFrom: "opacity-100 translate-y-0 scale-100", leaveTo: leaveTo, children: jsxRuntime.jsxs("div", { role: "status", id: id.toString(), className: buildClassName('relative overflow-hidden flex items-start gap-3 rounded-xl px-4 py-3.5 min-w-[280px] max-w-md transition-all', variantStyleMap[variant][type], className), children: [jsxRuntime.jsx("div", { className: "shrink-0 mt-0.5", children: variant === 'filled' ? (renderedIcon) : (jsxRuntime.jsx(TextContent, { className: buildClassName(textColorMap[type]), children: renderedIcon })) }), jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col gap-0.5", children: [title && (jsxRuntime.jsx("div", { className: buildClassName('font-semibold text-sm leading-snug', variant === 'filled' ? 'text-white' : 'text-gray-900 dark:text-gray-100'), children: title })), jsxRuntime.jsx("div", { className: buildClassName('text-sm font-medium leading-relaxed', variant === 'filled' ? 'text-white/95' : 'text-gray-700 dark:text-gray-300'), children: message }), action && (jsxRuntime.jsx("div", { className: "mt-1.5", children: jsxRuntime.jsx("button", { type: "button", onClick: action.onClick, className: buildClassName('text-xs font-semibold underline underline-offset-2 transition-opacity focus:outline-none', variant === 'filled'
4572
4777
  ? 'text-white hover:opacity-85'
4573
- : 'text-[var(--ui-primary)] hover:opacity-80'), children: action.label }) }))] }), jsxRuntime.jsx(Button, { onClick: onClose, "aria-label": "Close toast", className: buildClassName('shrink-0 mt-0.5 opacity-60 hover:opacity-100 transition', variant === 'filled' && 'text-white hover:text-white'), iconOnly: true, size: "xs", variant: "plain", theme: variant === 'filled' ? 'primary' : 'secondary', noOutlineOnFocus: true, children: jsxRuntime.jsx(SvgX, { className: "size-4" }) }), showProgressBar && duration > 0 && (jsxRuntime.jsx("div", { className: "absolute bottom-0 left-0 right-0 h-1 bg-black/10 dark:bg-white/10 overflow-hidden", children: jsxRuntime.jsx("div", { className: buildClassName('h-full transition-all duration-75 ease-linear', variant === 'filled' ? 'bg-white/70' : progressBarColorMap[type]), style: { width: `${Math.max(0, progress)}%` } }) }))] }) }));
4778
+ : 'text-[var(--ui-primary)] hover:opacity-80'), children: action.label }) }))] }), jsxRuntime.jsx(Button, { onClick: onClose, "aria-label": "Close toast", className: buildClassName('shrink-0 mt-0.5 opacity-60 hover:opacity-100 focus-visible:opacity-100 transition', variant === 'filled' && 'text-white hover:text-white focus-visible:outline-current'), iconOnly: true, size: "xs", variant: "plain", theme: variant === 'filled' ? 'primary' : 'secondary', children: jsxRuntime.jsx(SvgX, { className: "size-4" }) }), showProgressBar && duration > 0 && (jsxRuntime.jsx("div", { className: "absolute bottom-0 left-0 right-0 h-1 bg-black/10 dark:bg-white/10 overflow-hidden", children: jsxRuntime.jsx("div", { className: buildClassName('h-full transition-all duration-75 ease-linear', variant === 'filled' ? 'bg-white/70' : progressBarColorMap[type]), style: { width: `${Math.max(0, progress)}%` } }) }))] }) }));
4574
4779
  };
4575
4780
  Toast.displayName = 'Toast';
4576
4781
 
@@ -4903,6 +5108,7 @@ const useIsTabletOrMobile = () => {
4903
5108
  exports.Accordion = Accordion;
4904
5109
  exports.ActionSheet = ActionSheet;
4905
5110
  exports.Alert = Alert;
5111
+ exports.BREAKPOINT_QUERIES = BREAKPOINT_QUERIES;
4906
5112
  exports.Badge = Badge;
4907
5113
  exports.Banner = Banner;
4908
5114
  exports.BodyText = BodyText;
@@ -4983,12 +5189,15 @@ exports.isArray = isArray;
4983
5189
  exports.isEmpty = isEmpty;
4984
5190
  exports.nextSortDirection = nextSortDirection;
4985
5191
  exports.updateSortQuery = updateSortQuery;
5192
+ exports.useBreakpoint = useBreakpoint;
4986
5193
  exports.useDebounce = useDebounce;
4987
5194
  exports.useDebounceFn = useDebounceFn;
4988
5195
  exports.useIsMobile = useIsMobile;
4989
5196
  exports.useIsTablet = useIsTablet;
4990
5197
  exports.useIsTabletOrMobile = useIsTabletOrMobile;
5198
+ exports.useMediaQuery = useMediaQuery;
4991
5199
  exports.usePagination = usePagination;
5200
+ exports.useResponsiveValue = useResponsiveValue;
4992
5201
  exports.useRowSelection = useRowSelection;
4993
5202
  exports.useToast = useToast;
4994
5203
  //# sourceMappingURL=react-tailwind.cjs.js.map