@dreamtree-org/twreact-ui 1.3.11 → 2.0.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 (164) hide show
  1. package/README.md +40 -6
  2. package/ai-skills/dreamtree-ui.md +47 -9
  3. package/codemods/v2.cjs +198 -0
  4. package/dist/ai-schema.json +609 -33
  5. package/dist/components/blocks/AppShell.d.ts.map +1 -1
  6. package/dist/components/blocks/BottomActionBar.d.ts +6 -0
  7. package/dist/components/blocks/BottomActionBar.d.ts.map +1 -0
  8. package/dist/components/blocks/ConfirmDialog.d.ts.map +1 -1
  9. package/dist/components/blocks/PageHeader.d.ts.map +1 -1
  10. package/dist/components/core/Checkbox.d.ts.map +1 -1
  11. package/dist/components/core/DatePicker.d.ts.map +1 -1
  12. package/dist/components/core/DateRangePicker.d.ts.map +1 -1
  13. package/dist/components/core/Form.d.ts.map +1 -1
  14. package/dist/components/core/Input.d.ts.map +1 -1
  15. package/dist/components/core/Popover.d.ts.map +1 -1
  16. package/dist/components/core/ProgressBar.d.ts.map +1 -1
  17. package/dist/components/core/Rate.d.ts.map +1 -1
  18. package/dist/components/core/SearchField.d.ts +9 -0
  19. package/dist/components/core/SearchField.d.ts.map +1 -0
  20. package/dist/components/core/SegmentedControl.d.ts +8 -0
  21. package/dist/components/core/SegmentedControl.d.ts.map +1 -0
  22. package/dist/components/core/Select.d.ts.map +1 -1
  23. package/dist/components/core/SwipeableListItem.d.ts +4 -0
  24. package/dist/components/core/SwipeableListItem.d.ts.map +1 -0
  25. package/dist/components/core/SwipeableListItemGesture.d.ts +58 -0
  26. package/dist/components/core/SwipeableListItemGesture.d.ts.map +1 -0
  27. package/dist/components/core/Switch.d.ts.map +1 -1
  28. package/dist/components/core/Table/ActionMenu.d.ts +10 -3
  29. package/dist/components/core/Table/ActionMenu.d.ts.map +1 -1
  30. package/dist/components/core/Table/MobileCard.d.ts +10 -1
  31. package/dist/components/core/Table/MobileCard.d.ts.map +1 -1
  32. package/dist/components/core/Table/MobileFilters.d.ts +8 -2
  33. package/dist/components/core/Table/MobileFilters.d.ts.map +1 -1
  34. package/dist/components/core/Table/Table.d.ts.map +1 -1
  35. package/dist/components/core/Table/TableRow.d.ts +2 -1
  36. package/dist/components/core/Table/TableRow.d.ts.map +1 -1
  37. package/dist/components/core/Tabs.d.ts +3 -3
  38. package/dist/components/core/Tabs.d.ts.map +1 -1
  39. package/dist/components/core/ThreeDotPopover.d.ts +10 -22
  40. package/dist/components/core/ThreeDotPopover.d.ts.map +1 -1
  41. package/dist/components/core/Tooltip.d.ts +2 -1
  42. package/dist/components/core/Tooltip.d.ts.map +1 -1
  43. package/dist/components/core/internal/Calendar.d.ts +77 -0
  44. package/dist/components/core/internal/Calendar.d.ts.map +1 -0
  45. package/dist/components/core/menu/ActionMenuSurface.d.ts +42 -0
  46. package/dist/components/core/menu/ActionMenuSurface.d.ts.map +1 -0
  47. package/dist/components/core/menu/ActionSheetMenu.d.ts +17 -0
  48. package/dist/components/core/menu/ActionSheetMenu.d.ts.map +1 -0
  49. package/dist/components/core/menu/MenuPopover.d.ts +21 -0
  50. package/dist/components/core/menu/MenuPopover.d.ts.map +1 -0
  51. package/dist/components/core/menu/menuItems.d.ts +21 -0
  52. package/dist/components/core/menu/menuItems.d.ts.map +1 -0
  53. package/dist/components/core/menu/menuKeyboard.d.ts +16 -0
  54. package/dist/components/core/menu/menuKeyboard.d.ts.map +1 -0
  55. package/dist/components/feedback/ActionSheet.d.ts +4 -0
  56. package/dist/components/feedback/ActionSheet.d.ts.map +1 -0
  57. package/dist/components/feedback/Alert.d.ts.map +1 -1
  58. package/dist/components/feedback/Dialog.d.ts.map +1 -1
  59. package/dist/components/feedback/Drawer.d.ts.map +1 -1
  60. package/dist/components/feedback/Sheet.d.ts +4 -0
  61. package/dist/components/feedback/Sheet.d.ts.map +1 -0
  62. package/dist/components/feedback/Toast.d.ts +4 -3
  63. package/dist/components/feedback/Toast.d.ts.map +1 -1
  64. package/dist/components/feedback/ToastItem.d.ts +47 -0
  65. package/dist/components/feedback/ToastItem.d.ts.map +1 -0
  66. package/dist/components/feedback/ToastStack.d.ts +20 -0
  67. package/dist/components/feedback/ToastStack.d.ts.map +1 -0
  68. package/dist/components/feedback/ToastSwipe.d.ts +22 -0
  69. package/dist/components/feedback/ToastSwipe.d.ts.map +1 -0
  70. package/dist/components/feedback/Tour.d.ts.map +1 -1
  71. package/dist/components/feedback/sheetCore.d.ts +30 -0
  72. package/dist/components/feedback/sheetCore.d.ts.map +1 -0
  73. package/dist/components/navigation/Navbar.d.ts.map +1 -1
  74. package/dist/components/navigation/Sidebar.d.ts +6 -0
  75. package/dist/components/navigation/Sidebar.d.ts.map +1 -1
  76. package/dist/components/rich-text/RichTextEditor.d.ts.map +1 -1
  77. package/dist/components/rich-text/RichTextToolbar.d.ts +6 -2
  78. package/dist/components/rich-text/RichTextToolbar.d.ts.map +1 -1
  79. package/dist/components/utility/PullToRefresh.d.ts +5 -0
  80. package/dist/components/utility/PullToRefresh.d.ts.map +1 -0
  81. package/dist/esm/components/blocks/AppShell.js +97 -90
  82. package/dist/esm/components/blocks/BottomActionBar.js +132 -0
  83. package/dist/esm/components/blocks/ConfirmDialog.js +40 -4
  84. package/dist/esm/components/blocks/PageHeader.js +16 -3
  85. package/dist/esm/components/core/Checkbox.js +13 -24
  86. package/dist/esm/components/core/DatePicker.js +117 -304
  87. package/dist/esm/components/core/DateRangePicker.js +260 -308
  88. package/dist/esm/components/core/Form.js +11 -2
  89. package/dist/esm/components/core/Input.js +13 -18
  90. package/dist/esm/components/core/Popover.js +59 -48
  91. package/dist/esm/components/core/ProgressBar.js +36 -64
  92. package/dist/esm/components/core/Rate.js +5 -10
  93. package/dist/esm/components/core/SearchField.js +311 -0
  94. package/dist/esm/components/core/SegmentedControl.js +189 -0
  95. package/dist/esm/components/core/Select.js +185 -190
  96. package/dist/esm/components/core/SwipeableListItem.js +286 -0
  97. package/dist/esm/components/core/SwipeableListItemGesture.js +219 -0
  98. package/dist/esm/components/core/Switch.js +11 -13
  99. package/dist/esm/components/core/Table/ActionMenu.js +45 -231
  100. package/dist/esm/components/core/Table/ColumnMenu.js +1 -1
  101. package/dist/esm/components/core/Table/MobileCard.js +66 -16
  102. package/dist/esm/components/core/Table/MobileFilters.js +181 -10
  103. package/dist/esm/components/core/Table/Table.js +149 -102
  104. package/dist/esm/components/core/Table/TableRow.js +10 -2
  105. package/dist/esm/components/core/Tabs.js +7 -7
  106. package/dist/esm/components/core/ThreeDotPopover.js +93 -99
  107. package/dist/esm/components/core/Tooltip.js +138 -145
  108. package/dist/esm/components/core/internal/Calendar.js +381 -0
  109. package/dist/esm/components/core/menu/ActionMenuSurface.js +110 -0
  110. package/dist/esm/components/core/menu/ActionSheetMenu.js +140 -0
  111. package/dist/esm/components/core/menu/MenuPopover.js +177 -0
  112. package/dist/esm/components/core/menu/menuItems.js +61 -0
  113. package/dist/esm/components/core/menu/menuKeyboard.js +54 -0
  114. package/dist/esm/components/feedback/ActionSheet.js +55 -0
  115. package/dist/esm/components/feedback/Alert.js +63 -53
  116. package/dist/esm/components/feedback/Dialog.js +141 -53
  117. package/dist/esm/components/feedback/Drawer.js +130 -132
  118. package/dist/esm/components/feedback/Sheet.js +125 -0
  119. package/dist/esm/components/feedback/Toast.js +21 -291
  120. package/dist/esm/components/feedback/ToastItem.js +348 -0
  121. package/dist/esm/components/feedback/ToastStack.js +130 -0
  122. package/dist/esm/components/feedback/ToastSwipe.js +52 -0
  123. package/dist/esm/components/feedback/Tour.js +107 -30
  124. package/dist/esm/components/feedback/sheetCore.js +494 -0
  125. package/dist/esm/components/navigation/Dropdown.js +1 -1
  126. package/dist/esm/components/navigation/Navbar.js +172 -37
  127. package/dist/esm/components/navigation/Sidebar.js +263 -111
  128. package/dist/esm/components/rich-text/RichTextEditor.js +9 -3
  129. package/dist/esm/components/rich-text/RichTextToolbar.js +29 -9
  130. package/dist/esm/components/utility/Portal.js +31 -0
  131. package/dist/esm/components/utility/PullToRefresh.js +229 -0
  132. package/dist/esm/hooks/useFloatingPosition.js +82 -0
  133. package/dist/esm/hooks/useHaptics.js +65 -0
  134. package/dist/esm/hooks/usePresence.js +58 -0
  135. package/dist/esm/hooks/usePresentation.js +21 -0
  136. package/dist/esm/hooks/useSheetDrag.js +220 -0
  137. package/dist/esm/index.js +13 -2
  138. package/dist/esm/utils/position.js +10 -63
  139. package/dist/hooks/useFloatingPosition.d.ts +8 -4
  140. package/dist/hooks/useFloatingPosition.d.ts.map +1 -1
  141. package/dist/hooks/useHaptics.d.ts +19 -0
  142. package/dist/hooks/useHaptics.d.ts.map +1 -0
  143. package/dist/hooks/useSheetDrag.d.ts +31 -0
  144. package/dist/hooks/useSheetDrag.d.ts.map +1 -0
  145. package/dist/index.css +1 -1
  146. package/dist/index.d.ts +9 -1
  147. package/dist/index.esm.css +1 -1
  148. package/dist/index.esm.js +8851 -3198
  149. package/dist/index.js +8856 -3195
  150. package/dist/rich-text/index.js +41 -11
  151. package/dist/types/blocks.d.ts +32 -2
  152. package/dist/types/blocks.d.ts.map +1 -1
  153. package/dist/types/index.d.ts +353 -19
  154. package/dist/types/index.d.ts.map +1 -1
  155. package/dist/types/rich-text.d.ts +5 -0
  156. package/dist/types/rich-text.d.ts.map +1 -1
  157. package/dist/utils/position.d.ts +0 -15
  158. package/dist/utils/position.d.ts.map +1 -1
  159. package/doc/README.md +15 -0
  160. package/mcp/catalog.snapshot.json +1398 -464
  161. package/package.json +7 -2
  162. package/dist/esm/hooks/useSwipeToClose.js +0 -66
  163. package/dist/hooks/useSwipeToClose.d.ts +0 -24
  164. package/dist/hooks/useSwipeToClose.d.ts.map +0 -1
@@ -1,13 +1,20 @@
1
1
  'use client';
2
2
  import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2, toConsumableArray as _toConsumableArray, typeof as _typeof, defineProperty as _defineProperty } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
- import React, { useState, useRef, useImperativeHandle, useId, useMemo, useEffect, useLayoutEffect } from 'react';
4
+ import React, { useState, useRef, useCallback, useImperativeHandle, useId, useMemo, useEffect } from 'react';
5
5
  import { createPortal } from 'react-dom';
6
6
  import { cn } from '../../utils/cn.js';
7
+ import { setRefs } from '../../utils/position.js';
8
+ import { TOUCH_TARGET } from '../../utils/touchTarget.js';
7
9
  import { useLatestRef } from '../../hooks/useLatestRef.js';
10
+ import { useMediaQuery } from '../../hooks/useMediaQuery.js';
11
+ import { usePresentation } from '../../hooks/usePresentation.js';
12
+ import { useFloatingPosition } from '../../hooks/useFloatingPosition.js';
13
+ import Sheet from '../feedback/Sheet.js';
14
+ import Button from './Button.js';
8
15
  import { ChevronDown, X, Check, Search, Plus } from 'lucide-react';
9
16
 
10
- var _excluded = ["options", "value", "onChange", "placeholder", "label", "error", "helperText", "disabled", "required", "multiSelect", "searchable", "grouped", "onMenuItemRender", "className", "allowClear", "creatable", "onCreateOption", "onSearch", "loading", "selectAllOption", "closeOnSelect", "maxTagCount", "renderGroupLabel", "name", "size"];
17
+ var _excluded = ["options", "value", "onChange", "placeholder", "label", "error", "helperText", "disabled", "required", "multiSelect", "searchable", "grouped", "onMenuItemRender", "className", "allowClear", "creatable", "onCreateOption", "onSearch", "loading", "selectAllOption", "closeOnSelect", "maxTagCount", "renderGroupLabel", "name", "size", "presentation"];
11
18
  /**
12
19
  * Enhanced Select component (debounce removed)
13
20
  * - Supports single and multi select
@@ -19,8 +26,8 @@ var _excluded = ["options", "value", "onChange", "placeholder", "label", "error"
19
26
  * - ARIA & keyboard accessibility
20
27
  * - Custom render per option via onMenuItemRender
21
28
  * - Max tag count with overflow indicator
22
- * - Auto-flip dropdown if not enough space below; use above instead
23
- * - Adjusts dropdown max-height to available space
29
+ * - Popover: anchored with useFloatingPosition (flip / shift / size)
30
+ * - Sheet: a bottom Sheet on small touch screens (`presentation`, ADR 0001)
24
31
  */
25
32
  var isPlainObject = function isPlainObject(v) {
26
33
  return v !== null && _typeof(v) === "object";
@@ -109,6 +116,13 @@ var highlight = function highlight() {
109
116
  // Trigger sizes. min-h (not h) so multi-select tags can wrap. `md` keeps its
110
117
  // desktop look but grows to the 44px touch target on coarse pointers; `lg` is
111
118
  // 44px everywhere.
119
+ // Popover menu height cap (px); it also shrinks to the room left on its side.
120
+ var MENU_MAX_HEIGHT = 250;
121
+ // Option rows: compact in the popover, 48px touch rows in the sheet.
122
+ var ROW_CLASSES = {
123
+ popover: "px-3 py-2 text-sm",
124
+ sheet: "min-h-12 px-4 py-3 text-base"
125
+ };
112
126
  var SIZE_CLASSES = {
113
127
  sm: "min-h-8 px-2.5 py-1 text-xs",
114
128
  md: "min-h-[38px] px-3 py-2 text-sm [@media(pointer:coarse)]:min-h-11",
@@ -152,6 +166,8 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
152
166
  name = _ref.name,
153
167
  _ref$size = _ref.size,
154
168
  size = _ref$size === void 0 ? "md" : _ref$size,
169
+ _ref$presentation = _ref.presentation,
170
+ presentation = _ref$presentation === void 0 ? "auto" : _ref$presentation,
155
171
  props = _objectWithoutProperties(_ref, _excluded);
156
172
  var _useState = useState(false),
157
173
  _useState2 = _slicedToArray(_useState, 2),
@@ -174,20 +190,33 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
174
190
  var searchRef = useRef(null);
175
191
  var menuRef = useRef(null);
176
192
  var listboxRef = useRef(null);
177
- // New: dropdown placement & max height
178
- var _useState9 = useState("bottom"),
179
- _useState0 = _slicedToArray(_useState9, 2),
180
- placement = _useState0[0],
181
- setPlacement = _useState0[1]; // "bottom" | "top"
182
- var _useState1 = useState(null),
183
- _useState10 = _slicedToArray(_useState1, 2),
184
- menuMaxHeight = _useState10[0],
185
- setMenuMaxHeight = _useState10[1]; // px or null
186
- // Fixed-position coordinates for the portaled menu (anchored to the trigger).
187
- var _useState11 = useState(null),
188
- _useState12 = _slicedToArray(_useState11, 2),
189
- menuCoords = _useState12[0],
190
- setMenuCoords = _useState12[1]; // { left, top, width } in viewport px
193
+ // 'sheet' on small touch screens (or when forced), else 'popover'.
194
+ var isSheet = usePresentation(presentation) === "sheet";
195
+ // Focusing the search box pops the on-screen keyboard over the options,
196
+ // so touch users get the listbox focused instead and tap search to type.
197
+ var isCoarse = useMediaQuery("(pointer: coarse)");
198
+ var autoFocusSearch = searchable && !isSheet && !isCoarse;
199
+ // Popover mode: the portaled menu is anchored to the trigger wrapper.
200
+ var _useFloatingPosition = useFloatingPosition({
201
+ placement: "bottom-start",
202
+ offset: 4,
203
+ matchWidth: "exact",
204
+ maxHeight: MENU_MAX_HEIGHT,
205
+ strategy: "fixed",
206
+ open: isOpen && !isSheet
207
+ }),
208
+ floatingRefs = _useFloatingPosition.refs,
209
+ floatingStyles = _useFloatingPosition.floatingStyles,
210
+ floatingPlacement = _useFloatingPosition.placement,
211
+ isPositioned = _useFloatingPosition.isPositioned;
212
+ var setReference = floatingRefs.setReference,
213
+ setFloating = floatingRefs.setFloating;
214
+ var wrapperRef = useCallback(function (node) {
215
+ return setRefs(node, selectRef, setReference);
216
+ }, [setReference]);
217
+ var popoverRef = useCallback(function (node) {
218
+ return setRefs(node, menuRef, setFloating);
219
+ }, [setFloating]);
191
220
  useImperativeHandle(forwardedRef, function () {
192
221
  return selectRef.current;
193
222
  }, []);
@@ -278,10 +307,22 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
278
307
  };
279
308
  var activeDescendant = isOpen && focusedIndex >= 0 && focusedIndex < navOptions.length ? optionId(focusedIndex) : undefined;
280
309
  // A new search term or a collapsed group changes what the rows are, so
281
- // the active row no longer means the same option: start over.
282
- useEffect(function () {
310
+ // the active row no longer means the same option: start over. Adjusted
311
+ // during render (previous-value pattern), not in an effect.
312
+ var _useState9 = useState({
313
+ searchTerm: searchTerm,
314
+ collapsedGroups: collapsedGroups
315
+ }),
316
+ _useState0 = _slicedToArray(_useState9, 2),
317
+ rowsKey = _useState0[0],
318
+ setRowsKey = _useState0[1];
319
+ if (rowsKey.searchTerm !== searchTerm || rowsKey.collapsedGroups !== collapsedGroups) {
320
+ setRowsKey({
321
+ searchTerm: searchTerm,
322
+ collapsedGroups: collapsedGroups
323
+ });
283
324
  setFocusedIndex(-1);
284
- }, [searchTerm, collapsedGroups]);
325
+ }
285
326
  // Keep the active option visible while arrowing through a long list.
286
327
  useEffect(function () {
287
328
  var _el$scrollIntoView;
@@ -291,101 +332,22 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
291
332
  block: "nearest"
292
333
  });
293
334
  }, [activeDescendant]);
294
- // ===== Placement/space calculation =====
295
- var calculatePlacement = function calculatePlacement() {
296
- var MARGIN = 8; // px margin from viewport edges
297
- var DEFAULT_MENU_MAX = 240; // fallback max (approx 15rem)
298
- if (!selectRef.current) return;
299
- var rect = selectRef.current.getBoundingClientRect();
300
- var viewportH = window.innerHeight;
301
- var spaceBelow = viewportH - rect.bottom - MARGIN;
302
- var spaceAbove = rect.top - MARGIN;
303
- // desired approximate menu height (we can try to measure menu if already rendered)
304
- var desiredMenuHeight = DEFAULT_MENU_MAX;
305
- if (menuRef.current) {
306
- // when menu exists, use its scrollHeight as desired
307
- desiredMenuHeight = Math.min(menuRef.current.scrollHeight, DEFAULT_MENU_MAX);
308
- }
309
- // decide placement: prefer bottom unless not enough space and top has more space
310
- var nextPlacement;
311
- var nextMaxHeight;
312
- if (spaceBelow >= desiredMenuHeight || spaceBelow >= spaceAbove) {
313
- nextPlacement = "bottom";
314
- nextMaxHeight = Math.min(desiredMenuHeight, Math.max(80, spaceBelow));
315
- } else {
316
- nextPlacement = "top";
317
- nextMaxHeight = Math.min(desiredMenuHeight, Math.max(80, spaceAbove));
318
- }
319
- setPlacement(nextPlacement);
320
- setMenuMaxHeight(nextMaxHeight);
321
- // The menu is portaled to document.body with position:fixed, so it floats
322
- // above any ancestor with overflow:hidden|auto|scroll. Anchor it to the
323
- // trigger via the viewport-relative rect (no scroll offset for fixed).
324
- // maxHeight here mirrors the inline style applied to the menu (+10 fudge).
325
- var renderedMaxHeight = nextMaxHeight + 10;
326
- var next = {
327
- left: rect.left,
328
- width: rect.width
329
- };
330
- if (nextPlacement === "bottom") {
331
- next.top = rect.bottom + 4; // ~mt-1
332
- } else {
333
- // place above the trigger; bottom of menu sits just above the trigger top
334
- next.top = Math.max(MARGIN, rect.top - 4 - renderedMaxHeight);
335
- }
336
- setMenuCoords(next);
337
- };
338
- // Anchor the portaled menu BEFORE the browser paints, so it never flashes
339
- // at (0,0). The menu is already in the DOM (createPortal runs during render),
340
- // we just need its fixed coords set synchronously.
341
- useLayoutEffect(function () {
342
- if (isOpen) calculatePlacement();
343
- // eslint-disable-next-line react-hooks/exhaustive-deps
344
- }, [isOpen, filtered.length]);
335
+ // Popover mode: move focus into the popup once it is in the DOM
336
+ // (WAI-ARIA collapsible listbox) — the search box when searchable on a
337
+ // fine pointer, otherwise the listbox itself. Both carry
338
+ // aria-activedescendant and the same key handling as the trigger. The
339
+ // sheet moves focus itself (to the listbox, via `initialFocus`).
345
340
  useEffect(function () {
346
- if (!isOpen) {
347
- // drop stale coords so the next open re-anchors from scratch
348
- setMenuCoords(null);
349
- return;
350
- }
351
- // initial calc on open; wait a tick to allow DOM to render
341
+ if (!isOpen || isSheet) return undefined;
352
342
  var raf = requestAnimationFrame(function () {
353
343
  var _searchRef$current, _listboxRef$current;
354
- calculatePlacement();
355
- // Move focus into the popup (WAI-ARIA collapsible listbox): the search
356
- // box when searchable, otherwise the listbox itself. Both carry
357
- // aria-activedescendant and the same key handling as the trigger.
358
344
  var inMenu = menuRef.current && menuRef.current.contains(document.activeElement);
359
- if (searchable) (_searchRef$current = searchRef.current) === null || _searchRef$current === void 0 || _searchRef$current.focus();else if (!inMenu) (_listboxRef$current = listboxRef.current) === null || _listboxRef$current === void 0 || _listboxRef$current.focus();
360
- });
361
- // Reposition on resize/scroll while open (throttled by rAF). Use
362
- // capture-phase scroll so we also catch any scrolling ancestor — the
363
- // menu is portaled with position:fixed, so its coords must track the
364
- // trigger as the page (or a scroll container) moves.
365
- var rafId = null;
366
- var onWindowChange = function onWindowChange() {
367
- if (rafId) cancelAnimationFrame(rafId);
368
- rafId = requestAnimationFrame(function () {
369
- return calculatePlacement();
370
- });
371
- };
372
- window.addEventListener("resize", onWindowChange, {
373
- passive: true
374
- });
375
- window.addEventListener("scroll", onWindowChange, {
376
- passive: true,
377
- capture: true
345
+ if (autoFocusSearch) (_searchRef$current = searchRef.current) === null || _searchRef$current === void 0 || _searchRef$current.focus();else if (!inMenu) (_listboxRef$current = listboxRef.current) === null || _listboxRef$current === void 0 || _listboxRef$current.focus();
378
346
  });
379
347
  return function () {
380
- cancelAnimationFrame(raf);
381
- if (rafId) cancelAnimationFrame(rafId);
382
- window.removeEventListener("resize", onWindowChange);
383
- window.removeEventListener("scroll", onWindowChange, {
384
- capture: true
385
- });
348
+ return cancelAnimationFrame(raf);
386
349
  };
387
- // eslint-disable-next-line react-hooks/exhaustive-deps
388
- }, [isOpen, searchable, filtered.length]);
350
+ }, [isOpen, isSheet, autoFocusSearch]);
389
351
  // ===== handlers =====
390
352
  // Close the popup. When focus is inside it (the portaled menu unmounts),
391
353
  // hand focus back to the trigger so keyboard users are not dropped on
@@ -445,7 +407,7 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
445
407
  setIsOpen(true);
446
408
  setTimeout(function () {
447
409
  var _searchRef$current2;
448
- return searchable ? (_searchRef$current2 = searchRef.current) === null || _searchRef$current2 === void 0 ? void 0 : _searchRef$current2.focus() : null;
410
+ return autoFocusSearch ? (_searchRef$current2 = searchRef.current) === null || _searchRef$current2 === void 0 ? void 0 : _searchRef$current2.focus() : null;
449
411
  }, 0);
450
412
  }
451
413
  return;
@@ -509,8 +471,9 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
509
471
  break;
510
472
  case "Tab":
511
473
  // Leaving the (portaled) popup: close it and continue the tab order
512
- // from the trigger rather than from the end of <body>.
513
- closeMenu();
474
+ // from the trigger rather than from the end of <body>. The sheet is
475
+ // modal and traps Tab (search -> options -> Done) instead.
476
+ if (!isSheet) closeMenu();
514
477
  break;
515
478
  }
516
479
  };
@@ -545,14 +508,18 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
545
508
  children: [jsx("span", {
546
509
  className: "truncate max-w-[8rem]",
547
510
  children: opt.label
548
- }), jsx("div", {
511
+ }), jsx("span", {
549
512
  "aria-label": "Remove ".concat(opt.label),
513
+ // 44px hit area on coarse pointers; the negative margins
514
+ // keep the pill its compact size.
515
+ className: cn("inline-flex items-center justify-center cursor-pointer", TOUCH_TARGET, "[@media(pointer:coarse)]:-my-4 [@media(pointer:coarse)]:-mr-3"),
550
516
  onClick: function onClick(e) {
551
517
  e.stopPropagation();
552
518
  handleSelect(opt);
553
519
  },
554
520
  children: jsx(X, {
555
- className: "h-3 w-3"
521
+ className: "h-3 w-3",
522
+ "aria-hidden": "true"
556
523
  })
557
524
  })]
558
525
  }, opt.value);
@@ -584,6 +551,7 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
584
551
  var renderGroupedOptions = function renderGroupedOptions() {
585
552
  // Running index into `navOptions` (render order, collapsed skipped).
586
553
  var navIndex = -1;
554
+ var rowClass = ROW_CLASSES[isSheet ? "sheet" : "popover"];
587
555
  return jsx("div", {
588
556
  className: "divide-y",
589
557
  children: filtered.map(function (group, gi) {
@@ -630,7 +598,7 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
630
598
  onClick: function onClick() {
631
599
  return handleSelect(option);
632
600
  },
633
- className: cn("flex items-center px-3 py-2 text-sm hover:bg-gray-100 cursor-pointer", {
601
+ className: cn("flex items-center hover:bg-gray-100 cursor-pointer", rowClass, {
634
602
  "opacity-50 cursor-not-allowed": option.disabled,
635
603
  "bg-primary-50 text-primary-600": focusedIndex === index
636
604
  }),
@@ -668,7 +636,7 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
668
636
  onClick: function onClick() {
669
637
  return handleSelect(option);
670
638
  },
671
- className: cn("flex items-center px-3 py-2 text-sm hover:bg-gray-100 cursor-pointer", {
639
+ className: cn("flex items-center hover:bg-gray-100 cursor-pointer", ROW_CLASSES[isSheet ? "sheet" : "popover"], {
672
640
  "opacity-50 cursor-not-allowed": option.disabled,
673
641
  "bg-primary-50 text-primary-600": focusedIndex === index,
674
642
  "bg-primary-100": multiSelect && selectedValues.includes(option.value)
@@ -699,6 +667,67 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
699
667
  })]
700
668
  });
701
669
  };
670
+ var renderSearch = function renderSearch() {
671
+ return jsxs("div", {
672
+ className: "relative",
673
+ children: [selectAllOption && multiSelect && jsx("div", {
674
+ className: " text-sm flex items-center gap-2 border-b absolute left-1 top-1/2 h-4 w-4 -translate-y-1/2",
675
+ children: function () {
676
+ var allSelected = selectedValues.length === flatOptions.filter(function (o) {
677
+ return !o.disabled;
678
+ }).length;
679
+ return jsx("button", {
680
+ type: "button",
681
+ onClick: handleSelectAll,
682
+ "aria-label": allSelected ? "Deselect all" : "Select all",
683
+ "aria-pressed": allSelected,
684
+ className: "text-sm rounded focus:outline-none focus:ring-2 focus:ring-primary-500",
685
+ children: allSelected ? jsx(Check, {
686
+ className: "h-4 w-4 text-primary-600",
687
+ "aria-hidden": "true"
688
+ }) : jsx("div", {
689
+ className: "h-4 w-4 rounded border border-gray-300"
690
+ })
691
+ });
692
+ }()
693
+ }), jsx("input", {
694
+ ref: searchRef,
695
+ type: "text",
696
+ className: cn("w-full rounded-md border-0 bg-transparent pl-8 pr-3 py-1 focus:outline-none focus:ring-0",
697
+ // 16px in the sheet so iOS does not zoom the page on focus.
698
+ isSheet ? "min-h-11 text-base" : "text-sm", {
699
+ "pl-2": !(selectAllOption && multiSelect)
700
+ }),
701
+ placeholder: "Search...",
702
+ value: searchTerm,
703
+ onChange: function onChange(e) {
704
+ return setSearchTerm(e.target.value);
705
+ },
706
+ onKeyDown: handleKeyDown,
707
+ "aria-label": "Search options",
708
+ "aria-controls": listboxId,
709
+ "aria-activedescendant": activeDescendant,
710
+ "aria-autocomplete": "list"
711
+ }), jsx(Search, {
712
+ className: "absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-primary-400"
713
+ })]
714
+ });
715
+ };
716
+ var renderListbox = function renderListbox() {
717
+ return jsx("div", {
718
+ ref: listboxRef,
719
+ className: "overflow-auto focus:outline-none",
720
+ role: "listbox",
721
+ id: listboxId,
722
+ tabIndex: -1,
723
+ "aria-labelledby": label ? labelId : undefined,
724
+ "aria-label": label ? undefined : "Options",
725
+ "aria-multiselectable": multiSelect,
726
+ "aria-activedescendant": activeDescendant,
727
+ onKeyDown: handleKeyDown,
728
+ children: grouped ? renderGroupedOptions() : renderFlatOptions()
729
+ });
730
+ };
702
731
  return jsxs("div", {
703
732
  className: "w-full",
704
733
  children: [label && jsxs("label", {
@@ -710,13 +739,13 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
710
739
  children: "*"
711
740
  })]
712
741
  }), jsxs("div", {
713
- ref: selectRef,
742
+ ref: wrapperRef,
714
743
  className: "relative",
715
744
  children: [jsxs("button", _objectSpread2(_objectSpread2({
716
745
  ref: triggerRef,
717
746
  type: "button",
718
747
  id: baseId,
719
- "aria-haspopup": "listbox",
748
+ "aria-haspopup": isSheet ? "dialog" : "listbox",
720
749
  "aria-expanded": isOpen,
721
750
  "aria-controls": isOpen ? listboxId : undefined,
722
751
  "aria-labelledby": label ? "".concat(labelId, " ").concat(valueId) : valueId,
@@ -734,7 +763,7 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
734
763
  });
735
764
  setTimeout(function () {
736
765
  var _searchRef$current3;
737
- return searchable ? (_searchRef$current3 = searchRef.current) === null || _searchRef$current3 === void 0 ? void 0 : _searchRef$current3.focus() : null;
766
+ return autoFocusSearch ? (_searchRef$current3 = searchRef.current) === null || _searchRef$current3 === void 0 ? void 0 : _searchRef$current3.focus() : null;
738
767
  }, 0);
739
768
  }
740
769
  },
@@ -755,7 +784,7 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
755
784
  type: "button",
756
785
  onClick: handleClear,
757
786
  "aria-label": "Clear selection",
758
- className: "absolute right-8 top-1/2 -translate-y-1/2 rounded p-1 text-primary-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500",
787
+ className: cn("absolute right-8 top-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded p-1 text-primary-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500", TOUCH_TARGET),
759
788
  children: jsx(X, {
760
789
  className: "h-4 w-4",
761
790
  "aria-hidden": "true"
@@ -776,81 +805,47 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
776
805
  type: "hidden",
777
806
  name: name,
778
807
  value: selectedValues !== null && selectedValues !== void 0 ? selectedValues : ""
779
- })), isOpen && typeof document !== "undefined" && /*#__PURE__*/createPortal(jsxs("div", {
780
- ref: menuRef,
781
- className: cn("fixed z-50 rounded-md border border-gray-300 bg-white shadow-lg"),
808
+ })), isOpen && !isSheet && typeof document !== "undefined" && /*#__PURE__*/createPortal(jsxs("div", {
809
+ ref: popoverRef,
810
+ className: "fixed z-popover overflow-auto rounded-md border border-gray-300 bg-white shadow-lg",
782
811
  role: "dialog",
783
- "data-placement": placement,
812
+ "data-placement": floatingPlacement.split("-")[0],
784
813
  // Portaled to document.body with position:fixed so the menu
785
- // escapes any ancestor overflow:hidden|auto|scroll. Coordinates
786
- // are anchored to the trigger's viewport rect; width matches the
787
- // trigger; maxHeight is driven by available space.
788
- style: _objectSpread2({
789
- left: menuCoords ? "".concat(menuCoords.left, "px") : undefined,
790
- top: menuCoords ? "".concat(menuCoords.top, "px") : undefined,
791
- width: menuCoords ? "".concat(menuCoords.width, "px") : undefined
792
- }, menuMaxHeight ? {
793
- maxHeight: "".concat(menuMaxHeight + 10, "px"),
794
- overflow: "auto"
795
- } : {}),
814
+ // escapes any ancestor overflow:hidden|auto|scroll. Anchored
815
+ // by useFloatingPosition (flip / shift; width matches the
816
+ // trigger; max-height follows the room left). Transparent
817
+ // until the first position so it never flashes at (0,0).
818
+ style: _objectSpread2(_objectSpread2({}, floatingStyles), isPositioned ? {} : {
819
+ opacity: 0
820
+ }),
796
821
  children: [searchable && jsx("div", {
797
822
  className: "border-b border-gray-200 p-2",
798
- children: jsxs("div", {
799
- className: "relative",
800
- children: [selectAllOption && multiSelect && jsx("div", {
801
- className: " text-sm flex items-center gap-2 border-b absolute left-1 top-1/2 h-4 w-4 -translate-y-1/2",
802
- children: function () {
803
- var allSelected = selectedValues.length === flatOptions.filter(function (o) {
804
- return !o.disabled;
805
- }).length;
806
- return jsx("button", {
807
- type: "button",
808
- onClick: handleSelectAll,
809
- "aria-label": allSelected ? "Deselect all" : "Select all",
810
- "aria-pressed": allSelected,
811
- className: "text-sm rounded focus:outline-none focus:ring-2 focus:ring-primary-500",
812
- children: allSelected ? jsx(Check, {
813
- className: "h-4 w-4 text-primary-600",
814
- "aria-hidden": "true"
815
- }) : jsx("div", {
816
- className: "h-4 w-4 rounded border border-gray-300"
817
- })
818
- });
819
- }()
820
- }), jsx("input", {
821
- ref: searchRef,
822
- type: "text",
823
- className: cn("w-full rounded-md border-0 bg-transparent pl-8 pr-3 py-1 text-sm focus:outline-none focus:ring-0", {
824
- "pl-2": !(selectAllOption && multiSelect)
825
- }),
826
- placeholder: "Search...",
827
- value: searchTerm,
828
- onChange: function onChange(e) {
829
- return setSearchTerm(e.target.value);
830
- },
831
- onKeyDown: handleKeyDown,
832
- "aria-label": "Search options",
833
- "aria-controls": listboxId,
834
- "aria-activedescendant": activeDescendant,
835
- "aria-autocomplete": "list"
836
- }), jsx(Search, {
837
- className: "absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-primary-400"
838
- })]
839
- })
840
- }), jsx("div", {
841
- ref: listboxRef,
842
- className: "overflow-auto focus:outline-none",
843
- role: "listbox",
844
- id: listboxId,
845
- tabIndex: -1,
846
- "aria-labelledby": label ? labelId : undefined,
847
- "aria-label": label ? undefined : "Options",
848
- "aria-multiselectable": multiSelect,
849
- "aria-activedescendant": activeDescendant,
850
- onKeyDown: handleKeyDown,
851
- children: grouped ? renderGroupedOptions() : renderFlatOptions()
852
- })]
823
+ children: renderSearch()
824
+ }), renderListbox()]
853
825
  }), document.body)]
826
+ }), isSheet && jsxs(Sheet, {
827
+ ref: menuRef,
828
+ open: isOpen,
829
+ onClose: closeMenu,
830
+ placement: "bottom",
831
+ title: label || placeholder,
832
+ initialFocus: listboxRef,
833
+ "data-select-sheet": "",
834
+ children: [searchable && jsx(Sheet.Header, {
835
+ className: "py-2",
836
+ children: renderSearch()
837
+ }), jsx(Sheet.Body, {
838
+ className: "px-0 py-1",
839
+ children: renderListbox()
840
+ }), multiSelect && jsx(Sheet.Footer, {
841
+ children: jsx(Button, {
842
+ type: "button",
843
+ size: "lg",
844
+ className: "w-full",
845
+ onClick: closeMenu,
846
+ children: "Done"
847
+ })
848
+ })]
854
849
  }), error ? jsx("p", {
855
850
  id: errorId,
856
851
  className: "mt-1 text-sm text-error-500",