@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,37 +1,28 @@
1
1
  'use client';
2
2
  import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
3
  import { jsx, jsxs } from 'react/jsx-runtime';
4
- import React, { forwardRef, useRef, useCallback, useState, useEffect } from 'react';
4
+ import React, { forwardRef, useRef, useState, useCallback, useId } from 'react';
5
5
  import { MoreVertical } from 'lucide-react';
6
6
  import { cn } from '../../utils/cn.js';
7
+ import { TOUCH_TARGET } from '../../utils/touchTarget.js';
8
+ import ActionMenuSurface from './menu/ActionMenuSurface.js';
9
+ import { renderIcon } from './menu/menuItems.js';
7
10
 
8
- var _excluded = ["items", "trigger", "className", "menuClass", "menuItemClass", "closeOnSelect", "ariaLabel", "wrapperClassName"];
11
+ var _excluded = ["items", "trigger", "className", "menuClass", "menuItemClass", "closeOnSelect", "ariaLabel", "wrapperClassName", "presentation", "cancelLabel"];
9
12
  /**
10
- * ThreeDotPopover (Simple)
13
+ * ThreeDotPopover
11
14
  *
12
- * A minimal three-dot popover for your UI library.
15
+ * A kebab (three-dot) button that opens an action menu.
13
16
  *
14
- * Features (simple):
15
- * - Minimal, single-file React component (no animation libs, no portals).
16
- * - Click trigger toggles menu (default three-dot icon or custom trigger node).
17
- * - Close on outside click and Escape key.
18
- * - Menu items: { key, label, icon?, onClick?, disabled?, destructive? }.
17
+ * - Desktop (`presentation="popover"`): a portaled floating `role="menu"`
18
+ * that flips/shifts to stay on screen, with roving focus (ArrowUp/Down,
19
+ * Home/End, typeahead). Escape closes and returns focus to the trigger.
20
+ * - Phones (`presentation="sheet"`): an iOS-style action sheet with a
21
+ * separate Cancel button.
22
+ * - `presentation="auto"` (default) picks the sheet on small touch screens.
19
23
  *
20
- * Props:
21
- * - items: Array of menu item objects (see above)
22
- * - trigger: optional React node to use as trigger (falls back to three-dot button)
23
- * - className: additional class for trigger wrapper
24
- * - menuClass: additional class for menu container
25
- * - menuItemClass: additional class for every menu item
26
- * - closeOnSelect: boolean (default true)
27
- *
28
- * Example:
29
- * const items = [
30
- * { key: 'edit', label: 'Edit', icon: Edit2, onClick: () => {} },
31
- * { key: 'view', label: 'View', icon: Eye, onClick: () => {} },
32
- * { key: 'delete', label: 'Delete', destructive: true, onClick: () => {} },
33
- * ];
34
- * <ThreeDotPopoverSimple items={items} />
24
+ * Items: { key, label, icon?, onClick?, disabled?, destructive?, tone? }.
25
+ * `tone: 'destructive'` and `destructive: true` are equivalent.
35
26
  */
36
27
  var ThreeDotPopoverSimple = /*#__PURE__*/forwardRef(function ThreeDotPopoverSimple(_ref, ref) {
37
28
  var _ref$items = _ref.items,
@@ -50,108 +41,111 @@ var ThreeDotPopoverSimple = /*#__PURE__*/forwardRef(function ThreeDotPopoverSimp
50
41
  ariaLabel = _ref$ariaLabel === void 0 ? "More options" : _ref$ariaLabel,
51
42
  _ref$wrapperClassName = _ref.wrapperClassName,
52
43
  wrapperClassName = _ref$wrapperClassName === void 0 ? "" : _ref$wrapperClassName,
44
+ _ref$presentation = _ref.presentation,
45
+ presentation = _ref$presentation === void 0 ? "auto" : _ref$presentation,
46
+ _ref$cancelLabel = _ref.cancelLabel,
47
+ cancelLabel = _ref$cancelLabel === void 0 ? "Cancel" : _ref$cancelLabel,
53
48
  rest = _objectWithoutProperties(_ref, _excluded);
54
49
  var triggerRef = useRef(null);
55
- // Callback ref for a consumer-supplied trigger: handing a function (not the
56
- // ref object) to cloneElement keeps ref access out of render.
50
+ var _useState = useState(null),
51
+ _useState2 = _slicedToArray(_useState, 2),
52
+ anchorEl = _useState2[0],
53
+ setAnchorEl = _useState2[1];
54
+ // Callback ref for the trigger (default or consumer-supplied): handing a
55
+ // function (not the ref object) to cloneElement keeps ref access out of render.
57
56
  var setTriggerNode = useCallback(function (node) {
58
57
  triggerRef.current = node;
58
+ setAnchorEl(node);
59
59
  }, []);
60
- var menuRef = useRef(null);
61
- var _useState = useState(false),
62
- _useState2 = _slicedToArray(_useState, 2),
63
- open = _useState2[0],
64
- setOpen = _useState2[1];
65
- useEffect(function () {
66
- function onDocClick(e) {
67
- if (!open) return;
68
- if (triggerRef.current && triggerRef.current.contains(e.target)) return;
69
- if (menuRef.current && menuRef.current.contains(e.target)) return;
70
- setOpen(false);
71
- }
72
- function onKey(e) {
73
- if (!open) return;
74
- if (e.key === "Escape") setOpen(false);
75
- }
76
- document.addEventListener("mousedown", onDocClick);
77
- document.addEventListener("keydown", onKey);
78
- return function () {
79
- document.removeEventListener("mousedown", onDocClick);
80
- document.removeEventListener("keydown", onKey);
81
- };
82
- }, [open]);
60
+ var _useState3 = useState(false),
61
+ _useState4 = _slicedToArray(_useState3, 2),
62
+ open = _useState4[0],
63
+ setOpen = _useState4[1];
64
+ var _useState5 = useState("first"),
65
+ _useState6 = _slicedToArray(_useState5, 2),
66
+ initialFocus = _useState6[0],
67
+ setInitialFocus = _useState6[1];
68
+ var menuId = "three-dot-menu-".concat(useId());
83
69
  function toggleOpen(e) {
84
70
  e && e.stopPropagation();
71
+ setInitialFocus("first");
85
72
  setOpen(function (v) {
86
73
  return !v;
87
74
  });
88
75
  }
89
- function handleItemClick(item) {
90
- if (item.disabled) return;
91
- item.onClick && item.onClick(item);
92
- if (closeOnSelect) setOpen(false);
93
- // natural focus behavior: return focus to trigger
94
- triggerRef.current && triggerRef.current.focus();
76
+ function onTriggerKeyDown(e) {
77
+ if (open || e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
78
+ e.preventDefault();
79
+ setInitialFocus(e.key === "ArrowUp" ? "last" : "first");
80
+ setOpen(true);
95
81
  }
96
- var DefaultTrigger = jsx("button", {
97
- type: "button",
98
- "aria-label": ariaLabel,
82
+ var menuItems = (Array.isArray(items) ? items : []).map(function (it, i) {
83
+ var _it$key, _renderIcon;
84
+ return {
85
+ key: (_it$key = it.key) !== null && _it$key !== void 0 ? _it$key : i,
86
+ label: it.label,
87
+ icon: (_renderIcon = renderIcon(it.icon, "w-5 h-5")) !== null && _renderIcon !== void 0 ? _renderIcon : jsx("span", {
88
+ className: "w-5 h-5"
89
+ }),
90
+ disabled: Boolean(it.disabled),
91
+ tone: it.tone,
92
+ destructive: it.destructive,
93
+ onSelect: function onSelect() {
94
+ return it.onClick && it.onClick(it);
95
+ }
96
+ };
97
+ });
98
+ var triggerA11y = {
99
+ "aria-haspopup": "menu",
99
100
  "aria-expanded": open,
100
- ref: triggerRef,
101
+ "aria-controls": open ? menuId : undefined
102
+ };
103
+ var DefaultTrigger = jsx("button", _objectSpread2(_objectSpread2({
104
+ type: "button",
105
+ "aria-label": ariaLabel
106
+ }, triggerA11y), {}, {
107
+ ref: setTriggerNode,
101
108
  onClick: toggleOpen,
102
- className: cn("inline-flex items-center justify-center rounded-md focus:outline-none", className),
103
- children: jsx(MoreVertical, {})
104
- });
109
+ onKeyDown: onTriggerKeyDown,
110
+ className: cn("inline-flex items-center justify-center rounded-md text-muted hover:text-foreground", "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2", TOUCH_TARGET, className),
111
+ children: jsx(MoreVertical, {
112
+ "aria-hidden": "true"
113
+ })
114
+ }));
105
115
  var triggerNode = trigger ?
106
116
  /*#__PURE__*/
107
117
  // eslint-disable-next-line react-hooks/refs -- setTriggerNode is a callback ref, run by React at commit
108
- React.cloneElement(trigger, {
118
+ React.cloneElement(trigger, _objectSpread2({
109
119
  ref: setTriggerNode,
110
120
  onClick: function onClick(e) {
111
121
  trigger.props && trigger.props.onClick && trigger.props.onClick(e);
112
122
  toggleOpen(e);
113
123
  },
114
- "aria-expanded": open,
115
- "aria-haspopup": "menu"
116
- }) : DefaultTrigger;
124
+ onKeyDown: function onKeyDown(e) {
125
+ trigger.props && trigger.props.onKeyDown && trigger.props.onKeyDown(e);
126
+ if (!e.defaultPrevented) onTriggerKeyDown(e);
127
+ }
128
+ }, triggerA11y)) : DefaultTrigger;
117
129
  return jsxs("div", _objectSpread2(_objectSpread2({
118
130
  ref: ref,
119
131
  className: cn("inline-block relative", wrapperClassName)
120
132
  }, rest), {}, {
121
- children: [triggerNode, jsx("div", {
122
- ref: menuRef,
123
- className: cn("absolute left-0 mt-2 min-w-[160px] rounded-lg bg-white border shadow-sm", menuClass),
124
- style: {
125
- display: open ? "block" : "none"
133
+ children: [triggerNode, jsx(ActionMenuSurface, {
134
+ open: open,
135
+ onClose: function onClose() {
136
+ return setOpen(false);
126
137
  },
127
- role: "menu",
128
- "aria-hidden": !open,
129
- children: jsx("ul", {
130
- className: "py-1",
131
- children: items.map(function (it, i) {
132
- var _it$key;
133
- var Icon = it.icon || null;
134
- return jsx("li", {
135
- children: jsxs("button", {
136
- type: "button",
137
- role: "menuitem",
138
- onClick: function onClick() {
139
- return handleItemClick(it);
140
- },
141
- disabled: it.disabled,
142
- className: "w-full text-left px-4 py-3 flex items-center gap-3 text-sm leading-5 ".concat(it.destructive ? "text-error-600" : "text-primary-900", " ").concat(it.disabled ? "opacity-50 cursor-not-allowed" : "hover:bg-gray-50", " ").concat(menuItemClass),
143
- children: [Icon ? jsx(Icon, {
144
- className: "w-5 h-5"
145
- }) : jsx("span", {
146
- className: "w-5 h-5"
147
- }), jsx("span", {
148
- className: "flex-1",
149
- children: it.label
150
- })]
151
- })
152
- }, (_it$key = it.key) !== null && _it$key !== void 0 ? _it$key : i);
153
- })
154
- })
138
+ anchorEl: anchorEl,
139
+ items: menuItems,
140
+ presentation: presentation,
141
+ placement: "bottom-start",
142
+ initialFocus: initialFocus,
143
+ closeOnSelect: closeOnSelect,
144
+ id: menuId,
145
+ label: typeof ariaLabel === "string" ? ariaLabel : undefined,
146
+ cancelLabel: cancelLabel,
147
+ menuClassName: menuClass,
148
+ itemClassName: menuItemClass
155
149
  })]
156
150
  }));
157
151
  });
@@ -1,28 +1,30 @@
1
1
  'use client';
2
- import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, createForOfIteratorHelper as _createForOfIteratorHelper, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
2
+ import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
  import React, { forwardRef, useRef, useState, useId, useCallback, useEffect, useMemo } 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 { useFloatingPosition } from '../../hooks/useFloatingPosition.js';
9
+ import { useLatestRef } from '../../hooks/useLatestRef.js';
7
10
 
8
- var _excluded = ["position", "trigger", "content", "customTrigger", "children", "open", "defaultOpen", "onChange", "delay", "offset", "textClass", "bgClass", "className", "portal", "id"];
11
+ var _excluded = ["position", "trigger", "content", "customTrigger", "children", "open", "defaultOpen", "onChange", "delay", "offset", "textClass", "bgClass", "className", "portal", "id", "style"];
9
12
  // Grace period (ms) before a hover tooltip closes after the pointer leaves the
10
13
  // trigger, so the pointer can cross the `offset` gap onto the tooltip itself
11
14
  // (WCAG 1.4.13 "hoverable").
12
15
  var HOVER_CLOSE_GRACE = 100;
13
- /**
14
- * Nearest ancestor that establishes the containing block for an absolutely
15
- * positioned element. Uses `offsetParent` where the browser provides it and
16
- * falls back to walking computed styles (jsdom has no layout).
17
- */
18
- var getContainingBlock = function getContainingBlock(el) {
19
- if (el.offsetParent) return el.offsetParent;
20
- for (var p = el.parentElement; p && p !== document.body; p = p.parentElement) {
21
- var cs = window.getComputedStyle(p);
22
- if (cs.position && cs.position !== "static") return p;
23
- if (cs.transform && cs.transform !== "none") return p;
24
- }
25
- return null;
16
+ // Touch (pointerType "touch"): a hover/focus tooltip opens on a long-press
17
+ // instead of on the emulated mouseenter/focus that follows every tap, and
18
+ // hides on its own shortly after the finger lifts so it never sticks open.
19
+ var LONG_PRESS_MS = 500;
20
+ var TOUCH_HIDE_MS = 1500;
21
+ // Emulated mouse/focus events fire right after a tap; ignore them this long.
22
+ var TOUCH_COMPAT_MS = 800;
23
+ // A finger that moves further than this (px) is scrolling, not pressing.
24
+ var LONG_PRESS_SLOP = 10;
25
+ // True right after a touch: the mouseenter/focus that follows is emulated.
26
+ var isTouchEcho = function isTouchEcho(touch) {
27
+ return Date.now() - touch.lastAt < TOUCH_COMPAT_MS;
26
28
  };
27
29
  var NATIVE_FOCUSABLE_TAGS = new Set(["button", "input", "select", "textarea"]);
28
30
  /**
@@ -47,7 +49,8 @@ var isNativelyFocusable = function isNativelyFocusable(el) {
47
49
  };
48
50
  /**
49
51
  * Tooltip props
50
- * - position: 'auto' | 'top' | 'bottom' | 'left' | 'right' (default 'auto')
52
+ * - position: 'auto' | 'top' | 'bottom' | 'left' | 'right' (default 'auto';
53
+ * 'auto' prefers top and flips/shifts to stay in view)
51
54
  * - trigger: 'hover' | 'click' | 'focus' | Array of those (default 'hover')
52
55
  * - content: ReactNode | ( { close } ) => ReactNode
53
56
  * - customTrigger: ReactElement (rendered in place of children as trigger)
@@ -88,40 +91,54 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
88
91
  _ref$portal = _ref.portal,
89
92
  portal = _ref$portal === void 0 ? true : _ref$portal,
90
93
  propsId = _ref.id,
94
+ style = _ref.style,
91
95
  rest = _objectWithoutProperties(_ref, _excluded);
92
96
  var triggerRef = useRef(null);
93
97
  var tooltipRef = useRef(null);
94
98
  var delayRef = useRef(null);
95
99
  var closeTimerRef = useRef(null);
100
+ // Touch long-press bookkeeping (timers, start point, timestamps).
101
+ var touchRef = useRef({
102
+ press: null,
103
+ hide: null,
104
+ pressing: false,
105
+ fired: false,
106
+ x: 0,
107
+ y: 0,
108
+ lastAt: -Infinity,
109
+ suppressClickUntil: 0
110
+ });
96
111
  var _useState = useState(Boolean(controlledOpen !== null && controlledOpen !== void 0 ? controlledOpen : defaultOpen)),
97
112
  _useState2 = _slicedToArray(_useState, 2),
98
113
  open = _useState2[0],
99
114
  setOpen = _useState2[1];
100
- var _useState3 = useState("top"),
115
+ var _useState3 = useState(false),
101
116
  _useState4 = _slicedToArray(_useState3, 2),
102
- setPlacement = _useState4[1];
103
- var _useState5 = useState({
104
- left: 0,
105
- top: 0
106
- }),
107
- _useState6 = _slicedToArray(_useState5, 2),
108
- coords = _useState6[0],
109
- setCoords = _useState6[1];
110
- var _useState7 = useState(false),
111
- _useState8 = _slicedToArray(_useState7, 2),
112
- mounted = _useState8[0],
113
- setMounted = _useState8[1];
117
+ mounted = _useState4[0],
118
+ setMounted = _useState4[1];
114
119
  var reactId = useId();
115
120
  var id = propsId !== null && propsId !== void 0 ? propsId : "tooltip-".concat(reactId);
121
+ var openRef = useLatestRef(open);
122
+ var _useFloatingPosition = useFloatingPosition({
123
+ placement: position === "auto" ? "top" : position,
124
+ offset: offset,
125
+ open: open
126
+ }),
127
+ refs = _useFloatingPosition.refs,
128
+ floatingStyles = _useFloatingPosition.floatingStyles,
129
+ placement = _useFloatingPosition.placement;
130
+ var setReference = refs.setReference,
131
+ setFloating = refs.setFloating;
116
132
  var setTriggerRef = useCallback(function (node) {
117
133
  triggerRef.current = node;
134
+ setReference(node);
118
135
  if (typeof forwardedRef === "function") forwardedRef(node);else if (forwardedRef) forwardedRef.current = node;
119
- }, [forwardedRef]);
136
+ }, [forwardedRef, setReference]);
120
137
  // sync controlled (adjust-during-render pattern)
121
- var _useState9 = useState(controlledOpen),
122
- _useState0 = _slicedToArray(_useState9, 2),
123
- prevControlledOpen = _useState0[0],
124
- setPrevControlledOpen = _useState0[1];
138
+ var _useState5 = useState(controlledOpen),
139
+ _useState6 = _slicedToArray(_useState5, 2),
140
+ prevControlledOpen = _useState6[0],
141
+ setPrevControlledOpen = _useState6[1];
125
142
  if (controlledOpen !== prevControlledOpen) {
126
143
  setPrevControlledOpen(controlledOpen);
127
144
  if (typeof controlledOpen === "boolean") setOpen(controlledOpen);
@@ -132,106 +149,6 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
132
149
  var notify = useCallback(function (val) {
133
150
  if (typeof onChange === "function") onChange(val);
134
151
  }, [onChange]);
135
- var computePosition = useCallback(function () {
136
- var t = triggerRef.current;
137
- var el = tooltipRef.current;
138
- if (!t || !el) return;
139
- var tr = t.getBoundingClientRect();
140
- var ttW = el.offsetWidth;
141
- var ttH = el.offsetHeight;
142
- var winW = window.innerWidth;
143
- var winH = window.innerHeight;
144
- var candidates = ["top", "bottom", "left", "right"];
145
- var chosen = position === "auto" ? "top" : position;
146
- if (position === "auto") {
147
- var _iterator = _createForOfIteratorHelper(candidates),
148
- _step;
149
- try {
150
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
151
- var p = _step.value;
152
- if (p === "top" && tr.top >= ttH + offset) {
153
- chosen = "top";
154
- break;
155
- }
156
- if (p === "bottom" && winH - tr.bottom >= ttH + offset) {
157
- chosen = "bottom";
158
- break;
159
- }
160
- if (p === "right" && winW - tr.right >= ttW + offset) {
161
- chosen = "right";
162
- break;
163
- }
164
- if (p === "left" && tr.left >= ttW + offset) {
165
- chosen = "left";
166
- break;
167
- }
168
- }
169
- } catch (err) {
170
- _iterator.e(err);
171
- } finally {
172
- _iterator.f();
173
- }
174
- }
175
- var left = 0;
176
- var top = 0;
177
- switch (chosen) {
178
- case "top":
179
- left = tr.left + tr.width / 2 - ttW / 2;
180
- top = tr.top - ttH - offset;
181
- break;
182
- case "bottom":
183
- left = tr.left + tr.width / 2 - ttW / 2;
184
- top = tr.bottom + offset;
185
- break;
186
- case "left":
187
- left = tr.left - ttW - offset;
188
- top = tr.top + tr.height / 2 - ttH / 2;
189
- break;
190
- case "right":
191
- left = tr.right + offset;
192
- top = tr.top + tr.height / 2 - ttH / 2;
193
- break;
194
- }
195
- var pad = 8;
196
- left = Math.max(pad, Math.min(left, winW - ttW - pad));
197
- top = Math.max(pad, Math.min(top, winH - ttH - pad));
198
- // `left`/`top` are viewport coords; convert to the containing block of the
199
- // absolutely positioned tooltip. Portaled into <body> (or inline with no
200
- // positioned ancestor) that is the document, so add page scroll; inline
201
- // inside a positioned ancestor, offset by that ancestor instead.
202
- var cb = getContainingBlock(el);
203
- var originX = -window.scrollX;
204
- var originY = -window.scrollY;
205
- if (cb && cb !== document.body && cb !== document.documentElement) {
206
- var cr = cb.getBoundingClientRect();
207
- originX = cr.left + (cb.clientLeft || 0) - cb.scrollLeft;
208
- originY = cr.top + (cb.clientTop || 0) - cb.scrollTop;
209
- }
210
- setPlacement(chosen);
211
- setCoords({
212
- left: Math.round(left - originX),
213
- top: Math.round(top - originY)
214
- });
215
- }, [offset, position]);
216
- useEffect(function () {
217
- if (!open) return;
218
- computePosition();
219
- var ro = new ResizeObserver(function () {
220
- return computePosition();
221
- });
222
- if (triggerRef.current) ro.observe(triggerRef.current);
223
- if (tooltipRef.current) ro.observe(tooltipRef.current);
224
- var onScroll = function onScroll() {
225
- return computePosition();
226
- };
227
- window.addEventListener("scroll", onScroll, true);
228
- window.addEventListener("resize", computePosition);
229
- return function () {
230
- ro.disconnect();
231
- window.removeEventListener("scroll", onScroll, true);
232
- window.removeEventListener("resize", computePosition);
233
- };
234
- }, [open, computePosition]);
235
152
  // open/close helpers
236
153
  var cancelScheduledClose = useCallback(function () {
237
154
  clearTimeout(closeTimerRef.current);
@@ -253,6 +170,7 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
253
170
  var closeTooltip = useCallback(function () {
254
171
  clearTimeout(delayRef.current);
255
172
  clearTimeout(closeTimerRef.current);
173
+ clearTimeout(touchRef.current.hide);
256
174
  setOpen(false);
257
175
  notify(false);
258
176
  }, [notify]);
@@ -263,9 +181,12 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
263
181
  closeTimerRef.current = setTimeout(closeTooltip, HOVER_CLOSE_GRACE);
264
182
  }, [closeTooltip]);
265
183
  useEffect(function () {
184
+ var touch = touchRef.current;
266
185
  return function () {
267
186
  clearTimeout(delayRef.current);
268
187
  clearTimeout(closeTimerRef.current);
188
+ clearTimeout(touch.press);
189
+ clearTimeout(touch.hide);
269
190
  };
270
191
  }, []);
271
192
  // triggers normalization
@@ -280,7 +201,8 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
280
201
  // onMouseLeave props (see tooltipNode) — it doesn't exist yet here.
281
202
  if (triggers.includes("hover")) {
282
203
  var onMouseEnter = function onMouseEnter() {
283
- return openTooltip();
204
+ if (isTouchEcho(touchRef.current)) return;
205
+ openTooltip();
284
206
  };
285
207
  var onMouseLeave = function onMouseLeave() {
286
208
  return scheduleClose();
@@ -329,11 +251,13 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
329
251
  if (!t) return;
330
252
  if (opensOnFocus) {
331
253
  var onFocusIn = function onFocusIn() {
332
- return openTooltip(true);
254
+ if (isTouchEcho(touchRef.current)) return;
255
+ openTooltip(true);
333
256
  };
334
257
  var onFocusOut = function onFocusOut(e) {
335
258
  var tip = tooltipRef.current;
336
259
  if (e.relatedTarget && tip && tip.contains(e.relatedTarget)) return;
260
+ if (touchRef.current.fired) return; // a long-press tip hides on its own timer
337
261
  closeTooltip();
338
262
  };
339
263
  t.addEventListener("focusin", onFocusIn);
@@ -344,14 +268,84 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
344
268
  };
345
269
  }
346
270
  }, [opensOnFocus, openTooltip, closeTooltip]);
271
+ // Touch: long-press opens a hover/focus tooltip; a plain tap is left alone
272
+ // so the trigger's own onClick still runs. Click tooltips toggle on tap.
273
+ var longPress = opensOnFocus || triggers.includes("hover");
274
+ useEffect(function () {
275
+ var t = triggerRef.current;
276
+ if (!t || !longPress) return;
277
+ var touch = touchRef.current;
278
+ var onPointerDown = function onPointerDown(e) {
279
+ if (e.pointerType !== "touch") return;
280
+ clearTimeout(touch.press);
281
+ clearTimeout(touch.hide);
282
+ touch.lastAt = Date.now();
283
+ touch.pressing = true;
284
+ touch.fired = false;
285
+ touch.x = e.clientX;
286
+ touch.y = e.clientY;
287
+ touch.press = setTimeout(function () {
288
+ touch.fired = true;
289
+ openTooltip(true);
290
+ }, LONG_PRESS_MS);
291
+ };
292
+ var onPointerMove = function onPointerMove(e) {
293
+ if (!touch.pressing || e.pointerType !== "touch") return;
294
+ if (Math.hypot(e.clientX - touch.x, e.clientY - touch.y) > LONG_PRESS_SLOP) {
295
+ clearTimeout(touch.press);
296
+ }
297
+ };
298
+ var onPointerEnd = function onPointerEnd(e) {
299
+ if (!touch.pressing || e.pointerType !== "touch") return;
300
+ touch.pressing = false;
301
+ touch.lastAt = Date.now();
302
+ clearTimeout(touch.press);
303
+ if (touch.fired) {
304
+ // The press already did its job: swallow the click it may produce.
305
+ touch.suppressClickUntil = Date.now() + TOUCH_COMPAT_MS;
306
+ touch.hide = setTimeout(function () {
307
+ touch.fired = false;
308
+ closeTooltip();
309
+ }, TOUCH_HIDE_MS);
310
+ } else if (openRef.current) {
311
+ closeTooltip(); // a plain tap dismisses an open tip (and still clicks)
312
+ }
313
+ };
314
+ var onClickCapture = function onClickCapture(e) {
315
+ if (Date.now() < touch.suppressClickUntil) {
316
+ touch.suppressClickUntil = 0;
317
+ e.preventDefault();
318
+ e.stopPropagation();
319
+ }
320
+ };
321
+ // Stop the OS long-press menu / callout from covering the tooltip.
322
+ var onContextMenu = function onContextMenu(e) {
323
+ if (touch.pressing || touch.fired) e.preventDefault();
324
+ };
325
+ t.addEventListener("pointerdown", onPointerDown);
326
+ t.addEventListener("pointermove", onPointerMove);
327
+ t.addEventListener("pointerup", onPointerEnd);
328
+ t.addEventListener("pointercancel", onPointerEnd);
329
+ t.addEventListener("click", onClickCapture, true);
330
+ t.addEventListener("contextmenu", onContextMenu);
331
+ return function () {
332
+ t.removeEventListener("pointerdown", onPointerDown);
333
+ t.removeEventListener("pointermove", onPointerMove);
334
+ t.removeEventListener("pointerup", onPointerEnd);
335
+ t.removeEventListener("pointercancel", onPointerEnd);
336
+ t.removeEventListener("click", onClickCapture, true);
337
+ t.removeEventListener("contextmenu", onContextMenu);
338
+ };
339
+ }, [longPress, openTooltip, closeTooltip, openRef]);
347
340
  // Dismiss on a pointer/tap outside. Touch has no mouseleave, so without this
348
- // a tapped-open hover tooltip would never close.
341
+ // a tapped-open tooltip would never close.
349
342
  useEffect(function () {
350
343
  if (!open) return;
351
344
  var onPointerDown = function onPointerDown(e) {
352
345
  var t = triggerRef.current;
353
346
  var tip = tooltipRef.current;
354
347
  if (t && t.contains(e.target) || tip && tip.contains(e.target)) return;
348
+ touchRef.current.fired = false;
355
349
  closeTooltip();
356
350
  };
357
351
  document.addEventListener("pointerdown", onPointerDown);
@@ -419,16 +413,15 @@ var Tooltip = /*#__PURE__*/forwardRef(function Tooltip(_ref, forwardedRef) {
419
413
  } : {};
420
414
  // Tooltip box element
421
415
  var tooltipNode = open ? jsx("div", _objectSpread2(_objectSpread2(_objectSpread2({
422
- ref: tooltipRef,
416
+ ref: function ref(node) {
417
+ return setRefs(node, tooltipRef, setFloating);
418
+ },
423
419
  id: id,
424
- role: "tooltip"
420
+ role: "tooltip",
421
+ "data-placement": placement
425
422
  }, rest), tipHoverProps), {}, {
426
- className: cn("absolute z-50 transform-gpu transition-opacity duration-150 motion-reduce:transition-none", className),
427
- style: {
428
- left: coords.left,
429
- top: coords.top,
430
- position: "absolute"
431
- },
423
+ className: cn("z-tooltip transform-gpu transition-opacity duration-150 motion-reduce:transition-none", className),
424
+ style: _objectSpread2(_objectSpread2({}, floatingStyles), style),
432
425
  children: jsx("div", {
433
426
  className: cn("max-w-xs rounded-md border border-line px-3 py-2 text-sm shadow-lg shadow-black/5", bgClass, textClass),
434
427
  children: renderContent()