@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
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
- import React__default, { useId, useMemo, useState, forwardRef, Children, isValidElement, cloneElement, useRef, useImperativeHandle, useEffect, useCallback, Fragment as Fragment$1, createContext, useContext, useLayoutEffect } from 'react';
3
+ import React__default, { useId, useMemo, useState, forwardRef, useEffect, useCallback, useSyncExternalStore, Children, isValidElement, cloneElement, useRef, useImperativeHandle, Fragment as Fragment$1, createContext, useContext, useLayoutEffect } from 'react';
4
4
  import { twMerge } from 'tailwind-merge';
5
5
  import { Dialog, DialogBackdrop, DialogPanel, DialogTitle, Menu, MenuButton, MenuItems, MenuItem, Combobox, ComboboxInput, ComboboxButton, ComboboxOptions, ComboboxOption, TabGroup, TabList, Tab, TabPanels, TabPanel, Transition } from '@headlessui/react';
6
6
  import { FixedSizeList } from 'react-window';
@@ -131,59 +131,35 @@ TextContent.displayName = 'TextContent';
131
131
 
132
132
  /**
133
133
  * Tailwind CSS classes for theme + variant combinations.
134
+ *
135
+ * Only colours live here. Focus outline width/offset and disabled styling are
136
+ * applied uniformly in the component so they can be toggled in one place.
134
137
  */
135
138
  const THEMED_BTN_CLASS = {
136
139
  primary: {
137
- 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)]',
138
- 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)]',
139
- 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)]',
140
- },
141
- danger: {
142
- 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)]',
143
- 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)]',
144
- 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)]',
145
- },
146
- secondary: {
147
- 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',
148
- 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)]',
149
- 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',
150
- },
151
- success: {
152
- 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',
153
- 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',
154
- 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',
155
- },
156
- warning: {
157
- 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',
158
- 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',
159
- 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',
160
- },
161
- };
162
- const LOADER_TEXT_CLASS = {
163
- primary: {
164
- default: 'text-white',
165
- outlined: 'text-[var(--ui-primary-text)]',
166
- plain: 'text-[var(--ui-primary-text)]',
140
+ default: 'text-white bg-[var(--ui-primary)] hover:bg-[var(--ui-primary-hover)] focus-visible:outline-[var(--ui-primary-ring)]',
141
+ 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)]',
142
+ plain: 'text-[var(--ui-primary-text)] hover:bg-current/10 active:bg-current/15 focus-visible:outline-[var(--ui-primary-ring)]',
167
143
  },
168
144
  danger: {
169
- default: 'text-white',
170
- outlined: 'text-[var(--ui-danger-text)]',
171
- plain: 'text-[var(--ui-danger-text)]',
145
+ default: 'text-white bg-[var(--ui-danger)] hover:bg-[var(--ui-danger-hover)] focus-visible:outline-[var(--ui-danger-ring)]',
146
+ 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)]',
147
+ plain: 'text-[var(--ui-danger-text)] hover:bg-current/10 active:bg-current/15 focus-visible:outline-[var(--ui-danger-ring)]',
172
148
  },
173
149
  secondary: {
174
- default: 'text-gray-700 dark:text-white',
175
- outlined: 'text-gray-700 dark:text-gray-200',
176
- plain: 'text-gray-700 dark:text-gray-200',
150
+ 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)]',
151
+ 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)]',
152
+ plain: 'text-gray-700 hover:bg-current/10 active:bg-current/15 dark:text-gray-200 focus-visible:outline-[var(--ui-focus-ring)]',
177
153
  },
178
154
  success: {
179
- default: 'text-white',
180
- outlined: 'text-emerald-600 dark:text-emerald-400',
181
- plain: 'text-emerald-600 dark:text-emerald-400',
155
+ default: 'text-white bg-emerald-600 hover:bg-emerald-700 focus-visible:outline-emerald-500',
156
+ 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',
157
+ plain: 'text-emerald-700 hover:bg-current/10 active:bg-current/15 dark:text-emerald-400 focus-visible:outline-emerald-500',
182
158
  },
183
159
  warning: {
184
- default: 'text-white',
185
- outlined: 'text-amber-600 dark:text-amber-400',
186
- plain: 'text-amber-600 dark:text-amber-400',
160
+ default: 'text-white bg-amber-500 hover:bg-amber-600 focus-visible:outline-amber-500',
161
+ 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',
162
+ plain: 'text-amber-700 hover:bg-current/10 active:bg-current/15 dark:text-amber-400 focus-visible:outline-amber-500',
187
163
  },
188
164
  };
189
165
  const SIZE_CLASSES = {
@@ -203,22 +179,32 @@ const LOADER_SIZE = {
203
179
  };
204
180
  /**
205
181
  * Base shared tailwind classes.
182
+ *
183
+ * Disabled styling is keyed off both the native `disabled` attribute and
184
+ * `aria-disabled`, so non-button elements rendered via `as` look disabled too.
206
185
  */
207
186
  const BASE_BTN_CLASS = 'font-medium inline-flex items-center justify-center shrink-0 cursor-pointer select-none ' +
208
- 'disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-75 ' +
209
- 'active:scale-[0.98] transition-all duration-150 ease-out focus:outline-none';
187
+ 'transition-[color,background-color,border-color,box-shadow,opacity,transform] duration-150 ease-out ' +
188
+ 'active:scale-[0.98] focus:outline-none ' +
189
+ 'disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none ' +
190
+ 'aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:shadow-none';
210
191
  const Button = forwardRef(({ as, type, ...props }, ref) => {
211
- var _a, _b, _c, _d;
212
192
  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;
213
193
  const Component = as || 'button';
214
194
  const isNativeButton = Component === 'button';
195
+ // Clean invalid props when not rendering a real button
196
+ const { type: _ignoreType, disabled: _ignoreDisabled, role, tabIndex, onKeyDown, onKeyUp, ...domProps } = restProps;
197
+ const ariaLabel = domProps['aria-label'];
198
+ const ariaLabelledBy = domProps['aria-labelledby'];
215
199
  // Accessibility guard for icon-only buttons
216
- if (iconOnly && !restProps['aria-label']) {
217
- console.warn('[ui] <Button iconOnly> requires aria-label for accessibility.');
218
- }
200
+ useEffect(() => {
201
+ if (iconOnly && !ariaLabel && !ariaLabelledBy) {
202
+ console.warn('[ui] <Button iconOnly> requires aria-label or aria-labelledby for accessibility.');
203
+ }
204
+ }, [iconOnly, ariaLabel, ariaLabelledBy]);
219
205
  const resolvedDisabled = Boolean(disabled || loading);
220
- // Clean invalid props when not rendering a real button
221
- const { type: _ignoreType, disabled: _ignoreDisabled, ...cleanRest } = restProps;
206
+ // Links (`href`, or `to` for router Link components) keep their native semantics.
207
+ const isLink = !isNativeButton && (domProps.href != null || domProps.to != null);
222
208
  const handleClick = (e) => {
223
209
  if (resolvedDisabled) {
224
210
  e.preventDefault();
@@ -227,31 +213,73 @@ const Button = forwardRef(({ as, type, ...props }, ref) => {
227
213
  }
228
214
  onClick === null || onClick === void 0 ? void 0 : onClick(e);
229
215
  };
216
+ // Mirror native <button> keyboard activation for non-button elements:
217
+ // Enter activates on keydown, Space on keyup (and must not scroll the page).
218
+ const handleKeyDown = (e) => {
219
+ onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(e);
220
+ if (e.defaultPrevented || e.target !== e.currentTarget)
221
+ return;
222
+ if (e.key === 'Enter') {
223
+ e.preventDefault();
224
+ e.currentTarget.click();
225
+ }
226
+ else if (e.key === ' ') {
227
+ e.preventDefault();
228
+ }
229
+ };
230
+ const handleKeyUp = (e) => {
231
+ onKeyUp === null || onKeyUp === void 0 ? void 0 : onKeyUp(e);
232
+ if (e.defaultPrevented || e.target !== e.currentTarget)
233
+ return;
234
+ if (e.key === ' ') {
235
+ e.preventDefault();
236
+ e.currentTarget.click();
237
+ }
238
+ };
230
239
  const elementProps = isNativeButton
231
240
  ? {
232
241
  type: type !== null && type !== void 0 ? type : 'button',
233
242
  disabled: resolvedDisabled,
243
+ role,
244
+ tabIndex,
234
245
  onClick: handleClick,
246
+ onKeyDown,
247
+ onKeyUp,
235
248
  }
236
- : {
237
- role: (_a = cleanRest.role) !== null && _a !== void 0 ? _a : 'button',
238
- tabIndex: resolvedDisabled ? -1 : ((_b = cleanRest.tabIndex) !== null && _b !== void 0 ? _b : 0),
239
- 'aria-disabled': resolvedDisabled ? true : undefined,
240
- onClick: handleClick,
241
- };
249
+ : isLink
250
+ ? {
251
+ role,
252
+ tabIndex: resolvedDisabled ? -1 : tabIndex,
253
+ 'aria-disabled': resolvedDisabled ? true : undefined,
254
+ onClick: handleClick,
255
+ onKeyDown,
256
+ onKeyUp,
257
+ }
258
+ : {
259
+ role: role !== null && role !== void 0 ? role : 'button',
260
+ tabIndex: resolvedDisabled ? -1 : (tabIndex !== null && tabIndex !== void 0 ? tabIndex : 0),
261
+ 'aria-disabled': resolvedDisabled ? true : undefined,
262
+ onClick: handleClick,
263
+ onKeyDown: handleKeyDown,
264
+ onKeyUp: handleKeyUp,
265
+ };
242
266
  const sizeObj = SIZE_CLASSES[size] || SIZE_CLASSES.md;
243
267
  const classes = useMemo(() => {
244
268
  var _a;
245
- 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);
269
+ 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,
270
+ // An outline (not a ring) so the offset gap stays transparent on any surface/mode.
271
+ !noOutlineOnFocus &&
272
+ '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);
246
273
  }, [theme, variant, sizeObj, iconOnly, noOutlineOnFocus, rounded, fullWidth, active, className]);
247
- const loaderIcon = (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]) }));
274
+ // The spinner and loading text inherit the button's text colour.
275
+ const loaderIcon = (jsx(Loader, { "data-testid": "btn-loader-icon", size: LOADER_SIZE[size] || 'xs', className: "text-current" }));
248
276
  /**
249
277
  * SPECIAL CASE: iconOnly + loading → show spinner only
250
278
  */
251
279
  if (loading && iconOnly) {
252
- return (jsx(Component, { ref: ref, className: classes, "aria-busy": "true", "aria-pressed": active !== undefined ? active : undefined, ...elementProps, ...cleanRest, children: loaderIcon }));
280
+ return (jsx(Component, { ref: ref, className: classes, "aria-busy": "true", "aria-pressed": active !== undefined ? active : undefined, ...domProps, ...elementProps, children: loaderIcon }));
253
281
  }
254
- return (jsx(Component, { ref: ref, className: classes, "aria-busy": loading ? 'true' : undefined, "aria-pressed": active !== undefined ? active : undefined, ...elementProps, ...cleanRest, children: loading ? (jsxs("span", { className: "inline-flex items-center gap-2", "aria-live": "polite", children: [loaderIcon, loadingText ? (jsx(TextContent, { className: buildClassName((_d = LOADER_TEXT_CLASS[theme]) === null || _d === void 0 ? void 0 : _d[variant]), children: loadingText })) : (children)] })) : (jsxs(Fragment, { children: [leftIcon && jsx("span", { className: "shrink-0 inline-flex items-center", children: leftIcon }), children, rightIcon && jsx("span", { className: "shrink-0 inline-flex items-center", children: rightIcon })] })) }));
282
+ return (jsx(Component, { ref: ref, className: classes, "aria-busy": loading ? 'true' : undefined, "aria-pressed": active !== undefined ? active : undefined, ...domProps, ...elementProps, children: loading ? (jsxs("span", { className: "inline-flex items-center gap-2", "aria-live": "polite", children: [loaderIcon, loadingText ? (jsx(TextContent, { className: "text-current", children: loadingText })) : (children)] })) : (jsxs(Fragment, { children: [leftIcon && jsx("span", { className: "shrink-0 inline-flex items-center", children: leftIcon }), children, rightIcon && jsx("span", { className: "shrink-0 inline-flex items-center", children: rightIcon })] })) }));
255
283
  });
256
284
  Button.displayName = 'Button';
257
285
 
@@ -293,6 +321,45 @@ var SvgX = function SvgX(props) {
293
321
  })));
294
322
  };
295
323
 
324
+ const BREAKPOINT_QUERIES = {
325
+ medium: '(min-width: 768px)',
326
+ expanded: '(min-width: 1024px)',
327
+ };
328
+ // jsdom and some embedded webviews have no matchMedia; treat every query as
329
+ // unmatched there (the compact layout) rather than throwing.
330
+ const hasMatchMedia = () => typeof window !== 'undefined' && typeof window.matchMedia === 'function';
331
+ /** Live result of a CSS media query. `false` during server rendering or without `matchMedia`. */
332
+ const useMediaQuery = (query) => {
333
+ const subscribe = useCallback((onChange) => {
334
+ if (!hasMatchMedia())
335
+ return () => { };
336
+ const list = window.matchMedia(query);
337
+ list.addEventListener('change', onChange);
338
+ return () => list.removeEventListener('change', onChange);
339
+ }, [query]);
340
+ return useSyncExternalStore(subscribe, () => hasMatchMedia() && window.matchMedia(query).matches, () => false);
341
+ };
342
+ /** Current layout range; `compact` during server rendering. */
343
+ const useBreakpoint = () => {
344
+ const isMedium = useMediaQuery(BREAKPOINT_QUERIES.medium);
345
+ const isExpanded = useMediaQuery(BREAKPOINT_QUERIES.expanded);
346
+ if (isExpanded)
347
+ return 'expanded';
348
+ return isMedium ? 'medium' : 'compact';
349
+ };
350
+ /** Resolves a `ResponsiveValue` for the current viewport. */
351
+ const useResponsiveValue = (value) => {
352
+ var _a, _b, _c;
353
+ const breakpoint = useBreakpoint();
354
+ if (typeof value !== 'object' || value === null || !('base' in value))
355
+ return value;
356
+ if (breakpoint === 'expanded')
357
+ return (_b = (_a = value.lg) !== null && _a !== void 0 ? _a : value.md) !== null && _b !== void 0 ? _b : value.base;
358
+ if (breakpoint === 'medium')
359
+ return (_c = value.md) !== null && _c !== void 0 ? _c : value.base;
360
+ return value.base;
361
+ };
362
+
296
363
  const alignmentMap = {
297
364
  center: 'items-center justify-center h-full',
298
365
  start: 'justify-start h-full',
@@ -305,7 +372,8 @@ const panelAlignmentMap = {
305
372
  start: 'data-[closed]:-translate-x-1/2 rounded-r-2xl',
306
373
  end: 'data-[closed]:translate-x-1/2 rounded-l-2xl',
307
374
  top: 'data-[closed]:-translate-y-1/2 rounded-b-2xl',
308
- bottom: 'data-[closed]:translate-y-full rounded-t-2xl md:rounded-t-3xl max-h-[88vh] max-h-[88dvh]',
375
+ // Bottom sheets clear the home indicator on notched phones (0 elsewhere).
376
+ 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)]',
309
377
  };
310
378
  const panelSizeMap = {
311
379
  xs: 'max-w-xs',
@@ -316,7 +384,9 @@ const panelSizeMap = {
316
384
  '2xl': 'max-w-2xl',
317
385
  full: 'w-full',
318
386
  };
319
- 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, }) => {
387
+ 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, }) => {
388
+ const align = useResponsiveValue(alignProp);
389
+ const size = useResponsiveValue(sizeProp);
320
390
  const showTitleSection = Boolean(title || description || showBackButton || showCloseButton);
321
391
  const shouldShowDragHandle = dragHandle !== null && dragHandle !== void 0 ? dragHandle : align === 'bottom';
322
392
  const [viewportHeight, setViewportHeight] = React__default.useState(null);
@@ -575,7 +645,7 @@ const Banner = forwardRef((props, ref) => {
575
645
  const { type = 'info', iconSize = 'sm', children, icon, removable = false, onRemove, className, ...restProps } = props;
576
646
  const DefaultIcon = IconMap[type];
577
647
  const iconNode = icon !== undefined ? (icon) : (jsx(DefaultIcon, { "data-testid": `banner-${type}-icon`, className: buildClassName('shrink-0', sizeMap[iconSize]) }));
578
- return (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, jsx("div", { className: "flex items-center w-full justify-between", children: children }), removable && (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: jsx(SvgX, { className: "size-4.5", "data-testid": "banner-remove-icon" }) }))] }));
648
+ return (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, jsx("div", { className: "flex items-center w-full justify-between", children: children }), removable && (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: jsx(SvgX, { className: "size-4.5", "data-testid": "banner-remove-icon" }) }))] }));
579
649
  });
580
650
  Banner.displayName = 'Banner';
581
651
 
@@ -631,20 +701,23 @@ const BodyText = forwardRef((props, ref) => {
631
701
  });
632
702
  BodyText.displayName = 'BodyText';
633
703
 
634
- function BottomNavigation({ items, activeId, onChange, fixed = true, showLabels = 'always', theme = 'light', activeIndicatorStyle = 'line', containerClass = '', itemClass = '', }) {
704
+ function BottomNavigation({ items, activeId, onChange, fixed = true, showLabels = 'always', theme = 'light', activeIndicatorStyle = 'line', containerClass = '', itemClass = '', linkAs = 'a', linkHrefProp = 'href', ariaLabel = 'Bottom Navigation', hidden = false, }) {
635
705
  const handleTabClick = (item) => {
636
706
  var _a;
637
707
  if (item.disabled)
638
708
  return;
639
709
  (_a = item.onClick) === null || _a === void 0 ? void 0 : _a.call(item);
640
- onChange(item.id);
710
+ onChange === null || onChange === void 0 ? void 0 : onChange(item.id);
641
711
  };
712
+ // Removed from the tree, not just covered, so hidden tabs can't take focus.
713
+ if (hidden)
714
+ return null;
642
715
  const themeStyles = {
643
716
  light: 'bg-white dark:bg-gray-900 border-t border-[var(--ui-border)] text-gray-600 dark:text-gray-400',
644
717
  dark: 'bg-gray-900 text-gray-400 border-t border-gray-800',
645
718
  primary: 'bg-blue-600 text-blue-100 border-t border-blue-700',
646
719
  }[theme];
647
- return (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) => {
720
+ return (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) => {
648
721
  const isActive = item.id === activeId;
649
722
  const showLabelText = showLabels === 'always' || (showLabels === 'active' && isActive);
650
723
  const activeItemColor = {
@@ -667,11 +740,17 @@ function BottomNavigation({ items, activeId, onChange, fixed = true, showLabels
667
740
  info: 'bg-blue-500 text-white',
668
741
  success: 'bg-emerald-500 text-white',
669
742
  }[item.badgeVariant || 'danger'];
670
- return (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 && (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') })), jsxs("div", { className: buildClassName('relative inline-flex items-center justify-center transition-all duration-200', activeIndicatorStyle === 'pill'
743
+ // A disabled item stays a button: a link can't be disabled.
744
+ const isLink = Boolean(item.href) && !item.disabled;
745
+ const ItemElement = isLink ? linkAs : 'button';
746
+ const elementProps = isLink
747
+ ? { [linkHrefProp]: item.href }
748
+ : { type: 'button', disabled: item.disabled };
749
+ return (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 && (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') })), jsxs("div", { className: buildClassName('relative inline-flex items-center justify-center transition-all duration-200', activeIndicatorStyle === 'pill'
671
750
  ? buildClassName('px-4 py-1 rounded-full', isActive
672
751
  ? pillActiveStyles
673
752
  : 'group-hover:bg-gray-100/50 dark:group-hover:bg-gray-800/40')
674
- : 'px-1 py-0.5'), children: [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 && (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 && (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 && (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));
753
+ : 'px-1 py-0.5'), children: [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 && (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 && (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 && (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));
675
754
  }) }));
676
755
  }
677
756
  BottomNavigation.displayName = 'BottomNavigation';
@@ -2315,77 +2394,165 @@ function PinInput({ length = 4, value: externalValue, onChange, onComplete, mask
2315
2394
  }
2316
2395
  PinInput.displayName = 'PinInput';
2317
2396
 
2318
- function PullArrowIcon({ progress }) {
2319
- const rotation = Math.min(180, progress * 180);
2320
- return (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: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 14l-7 7m0 0l-7-7m7 7V3" }) }));
2397
+ // Resistance applied to the finger's travel, so the pull feels elastic.
2398
+ const RESISTANCE = 0.45;
2399
+ // Finger travel (px) before deciding whether a gesture is a pull or a sideways swipe.
2400
+ const AXIS_LOCK_SLOP = 8;
2401
+ // Overlay badge size and the gap it rests at below the container's top edge.
2402
+ const BADGE_SIZE = 40;
2403
+ const BADGE_TOP_GAP = 10;
2404
+ function PullArrowIcon({ progress, className }) {
2405
+ return (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: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 14l-7 7m0 0l-7-7m7 7V3" }) }));
2321
2406
  }
2322
- function PullToRefresh({ onRefresh, pullThreshold = 70, maxPullDistance = 120, disabled = false, refreshing: externalRefreshing, pullingContent, refreshingContent, children, className = '', contentClass = '', }) {
2407
+ 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 = '', }) {
2323
2408
  const containerRef = useRef(null);
2324
- const startYRef = useRef(0);
2325
- const isPullingRef = useRef(false);
2326
2409
  const [pullDistance, setPullDistance] = useState(0);
2410
+ const [isDragging, setIsDragging] = useState(false);
2327
2411
  const [internalRefreshing, setInternalRefreshing] = useState(false);
2412
+ const [announcement, setAnnouncement] = useState('');
2328
2413
  const isRefreshing = externalRefreshing !== undefined ? externalRefreshing : internalRefreshing;
2329
- const handleTouchStart = useCallback((e) => {
2330
- if (disabled || isRefreshing)
2331
- return;
2332
- const container = containerRef.current;
2333
- if (container && container.scrollTop === 0) {
2334
- startYRef.current = e.touches[0].clientY;
2335
- isPullingRef.current = true;
2336
- }
2337
- }, [disabled, isRefreshing]);
2338
- const handleTouchMove = useCallback((e) => {
2339
- if (!isPullingRef.current || disabled || isRefreshing)
2340
- return;
2414
+ // The touch listeners are attached once, natively (see below), so they read
2415
+ // the latest props and state through this ref instead of stale closures.
2416
+ const latest = useRef({
2417
+ disabled,
2418
+ isRefreshing,
2419
+ pullThreshold,
2420
+ maxPullDistance,
2421
+ onRefresh,
2422
+ onError,
2423
+ refreshedLabel,
2424
+ });
2425
+ useLayoutEffect(() => {
2426
+ latest.current = {
2427
+ disabled,
2428
+ isRefreshing,
2429
+ pullThreshold,
2430
+ maxPullDistance,
2431
+ onRefresh,
2432
+ onError,
2433
+ refreshedLabel,
2434
+ };
2435
+ });
2436
+ useEffect(() => {
2341
2437
  const container = containerRef.current;
2342
- if (!container || container.scrollTop > 0)
2438
+ if (!container)
2343
2439
  return;
2344
- const currentY = e.touches[0].clientY;
2345
- const rawDistance = currentY - startYRef.current;
2346
- if (rawDistance > 0) {
2347
- // Rubber-band resistance formula
2348
- const dampened = Math.min(maxPullDistance, rawDistance * 0.45);
2349
- setPullDistance(dampened);
2350
- if (e.cancelable) {
2351
- e.preventDefault();
2352
- }
2353
- }
2354
- else {
2355
- setPullDistance(0);
2356
- }
2357
- }, [disabled, isRefreshing, maxPullDistance]);
2358
- const handleTouchEnd = useCallback(async () => {
2359
- if (!isPullingRef.current || disabled || isRefreshing)
2360
- return;
2361
- isPullingRef.current = false;
2362
- if (pullDistance >= pullThreshold) {
2363
- setPullDistance(pullThreshold);
2440
+ let mounted = true;
2441
+ let pulling = false;
2442
+ let axis = 'pending';
2443
+ let startX = 0;
2444
+ let startY = 0;
2445
+ let distance = 0;
2446
+ const setDistance = (next) => {
2447
+ distance = next;
2448
+ setPullDistance(next);
2449
+ };
2450
+ // Ends a gesture without refreshing: a cancelled touch, a sideways swipe,
2451
+ // or content that scrolled away from the top.
2452
+ const reset = () => {
2453
+ pulling = false;
2454
+ setIsDragging(false);
2455
+ setDistance(0);
2456
+ };
2457
+ const refresh = async () => {
2458
+ const { pullThreshold: threshold, onRefresh: run, onError: report } = latest.current;
2459
+ setDistance(threshold);
2364
2460
  setInternalRefreshing(true);
2365
2461
  try {
2366
- await onRefresh();
2462
+ await run();
2463
+ if (mounted)
2464
+ setAnnouncement(latest.current.refreshedLabel);
2465
+ }
2466
+ catch (error) {
2467
+ report === null || report === void 0 ? void 0 : report(error);
2367
2468
  }
2368
2469
  finally {
2369
- setInternalRefreshing(false);
2370
- setPullDistance(0);
2470
+ if (mounted) {
2471
+ setInternalRefreshing(false);
2472
+ setDistance(0);
2473
+ }
2371
2474
  }
2372
- }
2373
- else {
2374
- setPullDistance(0);
2375
- }
2376
- }, [disabled, isRefreshing, onRefresh, pullDistance, pullThreshold]);
2475
+ };
2476
+ const handleTouchStart = (e) => {
2477
+ var _a, _b;
2478
+ const { disabled: isDisabled, isRefreshing: busy } = latest.current;
2479
+ if (isDisabled || busy || container.scrollTop > 0 || e.touches.length !== 1)
2480
+ return;
2481
+ startX = (_a = e.touches[0].clientX) !== null && _a !== void 0 ? _a : 0;
2482
+ startY = (_b = e.touches[0].clientY) !== null && _b !== void 0 ? _b : 0;
2483
+ axis = 'pending';
2484
+ pulling = true;
2485
+ setAnnouncement('');
2486
+ setIsDragging(true);
2487
+ setDistance(0);
2488
+ };
2489
+ const handleTouchMove = (e) => {
2490
+ var _a, _b, _c, _d;
2491
+ if (!pulling)
2492
+ return;
2493
+ if (latest.current.disabled || latest.current.isRefreshing || container.scrollTop > 0) {
2494
+ reset();
2495
+ return;
2496
+ }
2497
+ const dx = ((_b = (_a = e.touches[0]) === null || _a === void 0 ? void 0 : _a.clientX) !== null && _b !== void 0 ? _b : 0) - startX;
2498
+ const dy = ((_d = (_c = e.touches[0]) === null || _c === void 0 ? void 0 : _c.clientY) !== null && _d !== void 0 ? _d : 0) - startY;
2499
+ if (axis === 'pending') {
2500
+ if (Math.abs(dx) < AXIS_LOCK_SLOP && Math.abs(dy) < AXIS_LOCK_SLOP)
2501
+ return;
2502
+ axis = dy > 0 && Math.abs(dy) > Math.abs(dx) ? 'vertical' : 'horizontal';
2503
+ // A sideways swipe or an upward scroll belongs to the content, not to us.
2504
+ if (axis === 'horizontal') {
2505
+ reset();
2506
+ return;
2507
+ }
2508
+ }
2509
+ const next = Math.min(latest.current.maxPullDistance, Math.max(0, dy) * RESISTANCE);
2510
+ setDistance(next);
2511
+ // Needs the non-passive listener: stops the browser's own pull-to-refresh/overscroll.
2512
+ if (next > 0 && e.cancelable)
2513
+ e.preventDefault();
2514
+ };
2515
+ const handleTouchEnd = () => {
2516
+ if (!pulling)
2517
+ return;
2518
+ pulling = false;
2519
+ setIsDragging(false);
2520
+ if (distance >= latest.current.pullThreshold)
2521
+ void refresh();
2522
+ else
2523
+ setDistance(0);
2524
+ };
2525
+ // React registers touch listeners as passive, where preventDefault() is
2526
+ // ignored, so these are attached natively with touchmove non-passive.
2527
+ container.addEventListener('touchstart', handleTouchStart, { passive: true });
2528
+ container.addEventListener('touchmove', handleTouchMove, { passive: false });
2529
+ container.addEventListener('touchend', handleTouchEnd);
2530
+ container.addEventListener('touchcancel', reset);
2531
+ return () => {
2532
+ mounted = false;
2533
+ container.removeEventListener('touchstart', handleTouchStart);
2534
+ container.removeEventListener('touchmove', handleTouchMove);
2535
+ container.removeEventListener('touchend', handleTouchEnd);
2536
+ container.removeEventListener('touchcancel', reset);
2537
+ };
2538
+ }, []);
2377
2539
  const progress = Math.min(1, pullDistance / pullThreshold);
2378
2540
  const isReady = pullDistance >= pullThreshold;
2379
- return (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: [jsx("div", { className: "left-0 right-0 top-0 flex items-center justify-center pointer-events-none transition-all duration-150 z-20", style: {
2380
- height: isRefreshing ? `${pullThreshold}px` : `${pullDistance}px`,
2381
- opacity: pullDistance > 0 || isRefreshing ? 1 : 0,
2382
- }, "data-testid": "pull-to-refresh-indicator", children: 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 || (jsxs(Fragment, { children: [jsx(Loader, { size: "sm" }), jsx("span", { children: "Refreshing..." })] }))) : pullingContent ? (pullingContent) : (jsxs(Fragment, { children: [jsx(PullArrowIcon, { progress: progress }), jsx("span", { children: isReady ? 'Release to refresh' : 'Pull down to refresh' })] })) }) }), jsx("div", { className: buildClassName('transition-transform duration-150', contentClass), style: {
2383
- transform: isRefreshing
2384
- ? `translateY(${pullThreshold}px)`
2385
- : pullDistance > 0
2386
- ? `translateY(${pullDistance}px)`
2387
- : 'translateY(0px)',
2388
- }, "data-testid": "pull-to-refresh-content", children: children })] }));
2541
+ const offset = isRefreshing ? pullThreshold : pullDistance;
2542
+ // Follow the finger exactly while dragging; animate only the settle afterwards.
2543
+ const motionClass = isDragging
2544
+ ? 'transition-none'
2545
+ : 'transition-transform duration-200 ease-out motion-reduce:transition-none';
2546
+ return (jsxs("div", { ref: containerRef, className: buildClassName('relative overflow-y-auto overscroll-none', className), "data-testid": "pull-to-refresh-container", children: [jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: isRefreshing ? refreshingLabel : announcement }), variant === 'overlay' ? (jsx("div", { className: buildClassName('pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center', motionClass), style: {
2547
+ transform: `translateY(${offset - BADGE_SIZE + BADGE_TOP_GAP}px)`,
2548
+ opacity: offset > 0 ? 1 : 0,
2549
+ }, "aria-hidden": "true", "data-testid": "pull-to-refresh-indicator", children: jsx("div", { className: "grid size-10 place-items-center rounded-full bg-[var(--ui-primary)] text-white shadow-lg", children: isRefreshing
2550
+ ? (refreshingContent !== null && refreshingContent !== void 0 ? refreshingContent : jsx(Loader, { size: "xs", className: "text-white" }))
2551
+ : (pullingContent !== null && pullingContent !== void 0 ? pullingContent : jsx(PullArrowIcon, { progress: progress, className: "size-4" })) }) })) : (
2552
+ // Absolutely positioned, so the content's own translate is the only shift.
2553
+ 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: 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
2554
+ ? (refreshingContent !== null && refreshingContent !== void 0 ? refreshingContent : (jsxs(Fragment, { children: [jsx(Loader, { size: "sm" }), jsx("span", { children: refreshingLabel })] })))
2555
+ : (pullingContent !== null && pullingContent !== void 0 ? pullingContent : (jsxs(Fragment, { children: [jsx(PullArrowIcon, { progress: progress, className: "size-4 text-[var(--ui-primary)] dark:text-blue-400" }), jsx("span", { children: isReady ? releaseLabel : pullLabel })] }))) }) })), jsx("div", { className: buildClassName(variant === 'push' && motionClass, contentClass), style: variant === 'push' ? { transform: `translateY(${offset}px)` } : undefined, "data-testid": "pull-to-refresh-content", children: children })] }));
2389
2556
  }
2390
2557
  PullToRefresh.displayName = 'PullToRefresh';
2391
2558
 
@@ -3298,12 +3465,23 @@ function ChevronLeftIcon$1() {
3298
3465
  function SearchIcon$1() {
3299
3466
  return (jsx("svg", { className: "size-4 text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" }) }));
3300
3467
  }
3468
+ function MoreIcon() {
3469
+ return (jsxs("svg", { className: "size-5", fill: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: [jsx("circle", { cx: "12", cy: "5", r: "1.75" }), jsx("circle", { cx: "12", cy: "12", r: "1.75" }), jsx("circle", { cx: "12", cy: "19", r: "1.75" })] }));
3470
+ }
3301
3471
  function ClearIcon() {
3302
3472
  return (jsx("svg", { className: "size-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }));
3303
3473
  }
3304
- function MobileHeader({ title, subtitle, titleAlign = 'center', onBack, backLabel = 'Back', leading, actions, searchable = false, searchValue: externalSearchValue, onSearchChange, searchPlaceholder = 'Search...', sticky = true, transparent = false, bottomSlot, className = '', }) {
3474
+ 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 = '', }) {
3305
3475
  const [isSearchActive, setIsSearchActive] = useState(false);
3306
3476
  const [internalSearchValue, setInternalSearchValue] = useState('');
3477
+ const [isOverflowOpen, setIsOverflowOpen] = useState(false);
3478
+ // The overflow button takes one of the visible slots.
3479
+ const allActions = actions !== null && actions !== void 0 ? actions : [];
3480
+ const shouldOverflow = maxVisibleActions !== undefined && allActions.length > maxVisibleActions;
3481
+ const visibleActions = shouldOverflow
3482
+ ? allActions.slice(0, Math.max(maxVisibleActions - 1, 0))
3483
+ : allActions;
3484
+ const overflowActions = shouldOverflow ? allActions.slice(visibleActions.length) : [];
3307
3485
  const searchValue = externalSearchValue !== undefined ? externalSearchValue : internalSearchValue;
3308
3486
  const handleSearchInput = (e) => {
3309
3487
  const val = e.target.value;
@@ -3318,14 +3496,20 @@ function MobileHeader({ title, subtitle, titleAlign = 'center', onBack, backLabe
3318
3496
  ? 'bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-[var(--ui-border-muted)]'
3319
3497
  : '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: [jsx("div", { className: "h-14 px-3.5 flex items-center justify-between gap-3 relative", children: searchable && isSearchActive ? (jsxs("div", { className: "flex-1 flex items-center gap-2", "data-testid": "mobile-header-search-container", children: [jsxs("div", { className: "relative flex-1 flex items-center", children: [jsx("span", { className: "absolute left-3 pointer-events-none", children: jsx(SearchIcon$1, {}) }), 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 && (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: jsx(ClearIcon, {}) }))] }), 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" })] })) : (jsxs(Fragment, { children: [jsx("div", { className: "flex items-center gap-1 min-w-[40px] shrink-0 z-10", children: onBack ? (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: [jsx(ChevronLeftIcon$1, {}), jsx("span", { className: "text-xs font-semibold hidden sm:inline", children: backLabel })] })) : (leading) }), jsxs("div", { className: buildClassName('flex-1 min-w-0 px-2', titleAlign === 'center'
3320
3498
  ? 'absolute inset-x-12 text-center pointer-events-none flex flex-col items-center justify-center'
3321
- : 'text-left'), children: [title && (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 && (jsx("div", { className: "text-[11px] text-gray-500 dark:text-gray-400 truncate pointer-events-auto", "data-testid": "mobile-header-subtitle", children: subtitle }))] }), jsxs("div", { className: "flex items-center gap-1 min-w-[40px] shrink-0 justify-end z-10", children: [searchable && (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: jsx(SearchIcon$1, {}) })), actions === null || actions === void 0 ? void 0 : actions.map((action) => {
3499
+ : 'text-left'), children: [title && (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 && (jsx("div", { className: "text-[11px] text-gray-500 dark:text-gray-400 truncate pointer-events-auto", "data-testid": "mobile-header-subtitle", children: subtitle }))] }), jsxs("div", { className: "flex items-center gap-1 min-w-[40px] shrink-0 justify-end z-10", children: [searchable && (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: jsx(SearchIcon$1, {}) })), visibleActions.map((action) => {
3322
3500
  const badgeColor = {
3323
3501
  danger: 'bg-red-500 text-white',
3324
3502
  info: 'bg-blue-500 text-white',
3325
3503
  success: 'bg-emerald-500 text-white',
3326
3504
  }[action.badgeVariant || 'danger'];
3327
3505
  return (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: [jsx("span", { className: "text-base", children: action.icon }), action.badge !== undefined && action.badge !== null && (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));
3328
- })] })] })) }), bottomSlot && (jsx("div", { className: "px-3 py-2 border-t border-[var(--ui-border-muted)]", "data-testid": "mobile-header-bottom-slot", children: bottomSlot }))] }));
3506
+ }), overflowActions.length > 0 && (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: jsx(MoreIcon, {}) })), trailing] })] })) }), overflowActions.length > 0 && (jsx(ActionSheet, { isOpen: isOverflowOpen, onClose: () => setIsOverflowOpen(false), actions: overflowActions.map((action) => ({
3507
+ id: action.id,
3508
+ label: action.label,
3509
+ icon: action.icon,
3510
+ disabled: action.disabled,
3511
+ onClick: action.onClick,
3512
+ })) })), bottomSlot && (jsx("div", { className: "px-3 py-2 border-t border-[var(--ui-border-muted)]", "data-testid": "mobile-header-bottom-slot", children: bottomSlot }))] }));
3329
3513
  }
3330
3514
  MobileHeader.displayName = 'MobileHeader';
3331
3515
 
@@ -3828,9 +4012,18 @@ const sizeClasses$1 = {
3828
4012
  },
3829
4013
  };
3830
4014
  const SelectBox = forwardRef((props, forwardedRef) => {
3831
- 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;
4015
+ 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;
4016
+ // Uncontrolled when `selected` is not passed: keep the selection internally.
4017
+ const isControlled = selectedProp !== undefined;
4018
+ const [internalSelected, setInternalSelected] = useState(defaultSelected !== null && defaultSelected !== void 0 ? defaultSelected : null);
4019
+ const selected = isControlled ? selectedProp : internalSelected;
4020
+ const onChange = useCallback((value) => {
4021
+ if (!isControlled)
4022
+ setInternalSelected(value);
4023
+ onChangeProp(value);
4024
+ }, [isControlled, onChangeProp]);
3832
4025
  if (props.asBottomSheet || props.mobileMode === 'sheet') {
3833
- return jsx(MobilePicker, { ...props });
4026
+ return jsx(MobilePicker, { ...props, selected: selected, onChange: onChange });
3834
4027
  }
3835
4028
  const hasError = Boolean(error);
3836
4029
  const [query, setQuery] = useState('');
@@ -4028,6 +4221,16 @@ const SelectBox = forwardRef((props, forwardedRef) => {
4028
4221
  }
4029
4222
  }, [isAllSelected, onChange, allOptions]);
4030
4223
  const hasLeftGroup = leftGroup != null && leftGroup !== undefined;
4224
+ // Reserve room for the chevron (and the clear button when shown) so long values
4225
+ // don't run underneath them.
4226
+ const showClearButton = allowClear && !multiple && !isEmpty(normalizedSelected);
4227
+ const rightPaddingClass = showClearButton
4228
+ ? 'pe-16'
4229
+ : size === 'lg'
4230
+ ? 'pe-11'
4231
+ : size === 'sm'
4232
+ ? 'pe-9'
4233
+ : 'pe-10';
4031
4234
  const hasFreeTextOption = Boolean(allowFreeText &&
4032
4235
  query.trim() &&
4033
4236
  !allOptions.some((opt) => { var _a; return ((_a = getDisplayValue(opt)) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === query.trim().toLowerCase(); }));
@@ -4040,7 +4243,7 @@ const SelectBox = forwardRef((props, forwardedRef) => {
4040
4243
  const isOptSelected = selectedValueSet.has(optVal);
4041
4244
  return (jsx(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))) : (jsxs("div", { className: "flex items-center justify-between gap-2 text-gray-900 dark:text-white group-data-[selected]:text-blue-600", children: [jsx("span", { className: "text-sm/6", children: getDisplayValue(option) }), jsx(SvgCheck, { className: "invisible size-5 dark:stroke-gray-300 group-data-[selected]:visible" })] })) }, optVal));
4042
4245
  };
4043
- return (jsxs("div", { className: buildClassName('group space-y-2', hasError && 'has-error', containerClass), ref: containerRef, children: [label && (jsxs("div", { className: buildClassName('flex items-center justify-between', labelWrapperClass), children: [jsx(Label, { className: labelClass, htmlFor: id, "aria-required": required, children: label }), labelHint && jsx(TextContent, { xsmall: true, children: labelHint })] })), jsxs(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 ? (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 && (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) &&
4246
+ return (jsxs("div", { className: buildClassName('group space-y-2', hasError && 'has-error', containerClass), ref: containerRef, children: [label && (jsxs("div", { className: buildClassName('flex items-center justify-between', labelWrapperClass), children: [jsx(Label, { className: labelClass, htmlFor: id, "aria-required": required, children: label }), labelHint && jsx(TextContent, { xsmall: true, children: labelHint })] })), jsxs(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 ? (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 && (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) &&
4044
4247
  (normalizedSelected === null || normalizedSelected === void 0 ? void 0 : normalizedSelected.length) > 0 &&
4045
4248
  normalizedSelected.map((option, idx) => (jsx(Badge, { theme: "secondary", size: currentSize.badgeSize, removable: !disabled, onRemove: () => removeSelection(idx), children: getDisplayValue(option) }, `${getValue(option)}-${idx}`))), jsx(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) => {
4046
4249
  const val = e.target.value;
@@ -4080,7 +4283,9 @@ const SelectBox = forwardRef((props, forwardedRef) => {
4080
4283
  }
4081
4284
  }, onFocus: () => {
4082
4285
  setQuery('');
4083
- }, 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) && (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: jsx(SvgX, { className: "size-4 stroke-gray-500 dark:stroke-gray-300" }) })), jsx(ComboboxButton, { className: "group absolute inset-y-0 right-0 px-2.5 flex items-center justify-center", "data-testid": "combobox-button", children: jsx(SvgChevronDown, { className: chevronIconClass }) })] })), jsxs(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 && (jsxs("div", { className: "pb-1 mb-1 border-b border-[var(--ui-border-muted)] px-3 pt-1 flex items-center justify-between", children: [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' }), jsxs("span", { className: "text-[11px] font-medium text-gray-400", children: [Array.isArray(normalizedSelected) ? normalizedSelected.length : 0, " of", ' ', allOptions.length] })] })), optionsLoading ? (jsx(BodyText, { "data-testid": "options-loading", className: "flex items-center justify-center py-2 px-3 select-none", children: jsx(Loader, { size: "sm" }) })) : shouldShowTypeToSearch ? (jsx(BodyText, { className: "flex items-center justify-between py-2 px-3 select-none", children: "Type to search..." })) : (jsxs("div", { children: [allowFreeText &&
4286
+ }, 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 && (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: jsx(SvgX, { className: "size-4 stroke-gray-500 dark:stroke-gray-300" }) })), jsx(ComboboxButton, { className: "group absolute inset-y-0 right-0 px-2.5 flex items-center justify-center", "data-testid": "combobox-button", children: jsx(SvgChevronDown, { className: chevronIconClass }) })] })), jsxs(ComboboxOptions, { anchor: { to: 'bottom start', gap: 5, padding: 8 }, as: "div", modal: modalDropdown,
4287
+ // Matches the input width by default; min-width lets the panel grow wider than a narrow input.
4288
+ 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 && (jsxs("div", { className: "pb-1 mb-1 border-b border-[var(--ui-border-muted)] px-3 pt-1 flex items-center justify-between", children: [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' }), jsxs("span", { className: "text-[11px] font-medium text-gray-400", children: [Array.isArray(normalizedSelected) ? normalizedSelected.length : 0, " of", ' ', allOptions.length] })] })), optionsLoading ? (jsx(BodyText, { "data-testid": "options-loading", className: "flex items-center justify-center py-2 px-3 select-none", children: jsx(Loader, { size: "sm" }) })) : shouldShowTypeToSearch ? (jsx(BodyText, { className: "flex items-center justify-between py-2 px-3 select-none", children: "Type to search..." })) : (jsxs("div", { children: [allowFreeText &&
4084
4289
  query.trim() &&
4085
4290
  !allOptions.some((opt) => { var _a; return ((_a = getDisplayValue(opt)) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === query.trim().toLowerCase(); }) && (jsx(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: jsxs("span", { children: ["Use \"", query.trim(), "\""] }) }, `freetext-${query.trim()}`)), filteredGroups
4086
4291
  ? filteredGroups.map((grp, gIdx) => (jsxs("div", { children: [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)))
@@ -4280,14 +4485,14 @@ StatusPill.displayName = 'StatusPill';
4280
4485
 
4281
4486
  const ChevronUpIcon = ({ className }) => (jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: className, "aria-hidden": "true", children: jsx("polyline", { points: "18 15 12 9 6 15" }) }));
4282
4487
  const StickyActionBar = forwardRef((props, ref) => {
4283
- const { summaryContent, actionsContent, drawerContent, drawerTitle = 'Summary Details', position = 'fixed', containerMaxWidth = 'max-w-5xl', className, ...restProps } = props;
4488
+ const { summaryContent, actionsContent, drawerContent, drawerTitle = 'Summary Details', position = 'fixed', containerMaxWidth = 'max-w-5xl', safeArea = true, bottomOffset, className, style, ...restProps } = props;
4284
4489
  const [isDrawerOpen, setIsDrawerOpen] = useState(false);
4285
4490
  const positionClass = position === 'fixed'
4286
4491
  ? 'fixed bottom-0 left-0 right-0 z-30'
4287
4492
  : position === 'sticky'
4288
4493
  ? 'sticky bottom-0 z-30'
4289
4494
  : 'relative z-10';
4290
- return (jsxs(Fragment, { children: [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: jsxs("div", { className: buildClassName('mx-auto flex items-center justify-between gap-3', containerMaxWidth), children: [summaryContent && (jsx("div", { className: "flex items-center gap-2", children: drawerContent ? (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: [jsx("div", { children: summaryContent }), jsx(ChevronUpIcon, { className: "size-4 text-[var(--ui-primary)] dark:text-blue-400 shrink-0" })] })) : (jsx("div", { children: summaryContent })) })), actionsContent && (jsx("div", { className: "flex items-center gap-2 shrink-0 ml-auto", "data-testid": "actions-stack", children: actionsContent }))] }) }), drawerContent && (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 }))] }));
4495
+ return (jsxs(Fragment, { children: [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: jsxs("div", { className: buildClassName('mx-auto flex items-center justify-between gap-3', containerMaxWidth), children: [summaryContent && (jsx("div", { className: "flex items-center gap-2", children: drawerContent ? (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: [jsx("div", { children: summaryContent }), jsx(ChevronUpIcon, { className: "size-4 text-[var(--ui-primary)] dark:text-blue-400 shrink-0" })] })) : (jsx("div", { children: summaryContent })) })), actionsContent && (jsx("div", { className: "flex items-center gap-2 shrink-0 ml-auto", "data-testid": "actions-stack", children: actionsContent }))] }) }), drawerContent && (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 }))] }));
4291
4496
  });
4292
4497
  StickyActionBar.displayName = 'StickyActionBar';
4293
4498
 
@@ -4550,7 +4755,7 @@ const Toast = ({ id, title, message, type, variant = 'accent', action, icon, sho
4550
4755
  const renderedIcon = icon !== null && icon !== void 0 ? icon : (variant === 'filled' ? filledIconMap[type] : iconMap[type]);
4551
4756
  return (jsx(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: 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: [jsx("div", { className: "shrink-0 mt-0.5", children: variant === 'filled' ? (renderedIcon) : (jsx(TextContent, { className: buildClassName(textColorMap[type]), children: renderedIcon })) }), jsxs("div", { className: "flex-1 flex flex-col gap-0.5", children: [title && (jsx("div", { className: buildClassName('font-semibold text-sm leading-snug', variant === 'filled' ? 'text-white' : 'text-gray-900 dark:text-gray-100'), children: title })), 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 && (jsx("div", { className: "mt-1.5", children: jsx("button", { type: "button", onClick: action.onClick, className: buildClassName('text-xs font-semibold underline underline-offset-2 transition-opacity focus:outline-none', variant === 'filled'
4552
4757
  ? 'text-white hover:opacity-85'
4553
- : 'text-[var(--ui-primary)] hover:opacity-80'), children: action.label }) }))] }), 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: jsx(SvgX, { className: "size-4" }) }), showProgressBar && duration > 0 && (jsx("div", { className: "absolute bottom-0 left-0 right-0 h-1 bg-black/10 dark:bg-white/10 overflow-hidden", children: 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)}%` } }) }))] }) }));
4758
+ : 'text-[var(--ui-primary)] hover:opacity-80'), children: action.label }) }))] }), 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: jsx(SvgX, { className: "size-4" }) }), showProgressBar && duration > 0 && (jsx("div", { className: "absolute bottom-0 left-0 right-0 h-1 bg-black/10 dark:bg-white/10 overflow-hidden", children: 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)}%` } }) }))] }) }));
4554
4759
  };
4555
4760
  Toast.displayName = 'Toast';
4556
4761
 
@@ -4880,5 +5085,5 @@ const useIsTabletOrMobile = () => {
4880
5085
  return isMobile || isTablet;
4881
5086
  };
4882
5087
 
4883
- export { Accordion, ActionSheet, Alert, Badge, Banner, BodyText, BottomNavigation, Breadcrumb, Button, ButtonGroup, Card, CardContent, CardDescription, CardFooter, CardHeader, CardMedia, CardTitle, Checkbox, CheckboxGroup, DOTS, DataList, DataTable, DetailedInformation, Dot, Drawer, Dropdown, EmptyState, EmptyTableRow, Flex, FlexItem, FloatingActionButton, Grid, GridCol, HeadingText, Input, Label, Loader, MobileHeader, MobilePicker, MobileStepper, PASSWORD_PLACEHOLDER, PASSWORD_TYPE, Pagination, PasswordInput, PinInput, PullToRefresh, QuantityStepper, Radio, RadioGroup, RadioSwitch, RangeInput, RangeSlider, SegmentedControl, SelectBox, Skeleton, SortIconMap, StatusPill, StickyActionBar, SwipeableRow, SwipeableTabs, Table, TableBody, TableCell, TableFoot, TableHead, TableHeaderCell, TableRow, Tabs, TextContent, Textarea, Toast, ToastProvider, VirtualizedDataTable, WheelPicker, buildClassName, buildInputClass, evaluatePasswordStrength, get, getSortIcon, isArray, isEmpty, nextSortDirection, updateSortQuery, useDebounce, useDebounceFn, useIsMobile, useIsTablet, useIsTabletOrMobile, usePagination, useRowSelection, useToast };
5088
+ export { Accordion, ActionSheet, Alert, BREAKPOINT_QUERIES, Badge, Banner, BodyText, BottomNavigation, Breadcrumb, Button, ButtonGroup, Card, CardContent, CardDescription, CardFooter, CardHeader, CardMedia, CardTitle, Checkbox, CheckboxGroup, DOTS, DataList, DataTable, DetailedInformation, Dot, Drawer, Dropdown, EmptyState, EmptyTableRow, Flex, FlexItem, FloatingActionButton, Grid, GridCol, HeadingText, Input, Label, Loader, MobileHeader, MobilePicker, MobileStepper, PASSWORD_PLACEHOLDER, PASSWORD_TYPE, Pagination, PasswordInput, PinInput, PullToRefresh, QuantityStepper, Radio, RadioGroup, RadioSwitch, RangeInput, RangeSlider, SegmentedControl, SelectBox, Skeleton, SortIconMap, StatusPill, StickyActionBar, SwipeableRow, SwipeableTabs, Table, TableBody, TableCell, TableFoot, TableHead, TableHeaderCell, TableRow, Tabs, TextContent, Textarea, Toast, ToastProvider, VirtualizedDataTable, WheelPicker, buildClassName, buildInputClass, evaluatePasswordStrength, get, getSortIcon, isArray, isEmpty, nextSortDirection, updateSortQuery, useBreakpoint, useDebounce, useDebounceFn, useIsMobile, useIsTablet, useIsTabletOrMobile, useMediaQuery, usePagination, useResponsiveValue, useRowSelection, useToast };
4884
5089
  //# sourceMappingURL=react-tailwind.es.js.map