@dreamtree-org/twreact-ui 1.3.10 → 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 (230) hide show
  1. package/README.md +70 -6
  2. package/ai-skills/dreamtree-ui.md +58 -15
  3. package/codemods/v2.cjs +198 -0
  4. package/dist/ai-schema.json +745 -53
  5. package/dist/charts/index.js +195 -66
  6. package/dist/components/blocks/AppShell.d.ts.map +1 -1
  7. package/dist/components/blocks/BottomActionBar.d.ts +6 -0
  8. package/dist/components/blocks/BottomActionBar.d.ts.map +1 -0
  9. package/dist/components/blocks/ConfirmDialog.d.ts.map +1 -1
  10. package/dist/components/blocks/PageHeader.d.ts.map +1 -1
  11. package/dist/components/charts/CartesianChart.d.ts.map +1 -1
  12. package/dist/components/charts/ChartFrame.d.ts.map +1 -1
  13. package/dist/components/charts/useChartLayout.d.ts +25 -0
  14. package/dist/components/charts/useChartLayout.d.ts.map +1 -0
  15. package/dist/components/core/Button.d.ts.map +1 -1
  16. package/dist/components/core/Checkbox.d.ts.map +1 -1
  17. package/dist/components/core/ColorPicker.d.ts.map +1 -1
  18. package/dist/components/core/DatePicker.d.ts.map +1 -1
  19. package/dist/components/core/DateRangePicker.d.ts.map +1 -1
  20. package/dist/components/core/Form.d.ts.map +1 -1
  21. package/dist/components/core/Input.d.ts.map +1 -1
  22. package/dist/components/core/Loader.d.ts.map +1 -1
  23. package/dist/components/core/LocationPicker.d.ts.map +1 -1
  24. package/dist/components/core/Popover.d.ts.map +1 -1
  25. package/dist/components/core/ProgressBar.d.ts.map +1 -1
  26. package/dist/components/core/Radio.d.ts.map +1 -1
  27. package/dist/components/core/Rate.d.ts.map +1 -1
  28. package/dist/components/core/RoundedTag.d.ts.map +1 -1
  29. package/dist/components/core/SearchField.d.ts +9 -0
  30. package/dist/components/core/SearchField.d.ts.map +1 -0
  31. package/dist/components/core/SegmentedControl.d.ts +8 -0
  32. package/dist/components/core/SegmentedControl.d.ts.map +1 -0
  33. package/dist/components/core/Select.d.ts.map +1 -1
  34. package/dist/components/core/Skeleton.d.ts.map +1 -1
  35. package/dist/components/core/Slider.d.ts.map +1 -1
  36. package/dist/components/core/SpeechToText.d.ts.map +1 -1
  37. package/dist/components/core/SwipeableListItem.d.ts +4 -0
  38. package/dist/components/core/SwipeableListItem.d.ts.map +1 -0
  39. package/dist/components/core/SwipeableListItemGesture.d.ts +58 -0
  40. package/dist/components/core/SwipeableListItemGesture.d.ts.map +1 -0
  41. package/dist/components/core/Switch.d.ts.map +1 -1
  42. package/dist/components/core/Table/ActionMenu.d.ts +10 -3
  43. package/dist/components/core/Table/ActionMenu.d.ts.map +1 -1
  44. package/dist/components/core/Table/MobileCard.d.ts +10 -1
  45. package/dist/components/core/Table/MobileCard.d.ts.map +1 -1
  46. package/dist/components/core/Table/MobileFilters.d.ts +8 -2
  47. package/dist/components/core/Table/MobileFilters.d.ts.map +1 -1
  48. package/dist/components/core/Table/Table.d.ts.map +1 -1
  49. package/dist/components/core/Table/TableRow.d.ts +2 -1
  50. package/dist/components/core/Table/TableRow.d.ts.map +1 -1
  51. package/dist/components/core/Tabs.d.ts +5 -8
  52. package/dist/components/core/Tabs.d.ts.map +1 -1
  53. package/dist/components/core/Textarea.d.ts.map +1 -1
  54. package/dist/components/core/ThreeDotPopover.d.ts +10 -22
  55. package/dist/components/core/ThreeDotPopover.d.ts.map +1 -1
  56. package/dist/components/core/Tooltip.d.ts +2 -1
  57. package/dist/components/core/Tooltip.d.ts.map +1 -1
  58. package/dist/components/core/internal/Calendar.d.ts +77 -0
  59. package/dist/components/core/internal/Calendar.d.ts.map +1 -0
  60. package/dist/components/core/menu/ActionMenuSurface.d.ts +42 -0
  61. package/dist/components/core/menu/ActionMenuSurface.d.ts.map +1 -0
  62. package/dist/components/core/menu/ActionSheetMenu.d.ts +17 -0
  63. package/dist/components/core/menu/ActionSheetMenu.d.ts.map +1 -0
  64. package/dist/components/core/menu/MenuPopover.d.ts +21 -0
  65. package/dist/components/core/menu/MenuPopover.d.ts.map +1 -0
  66. package/dist/components/core/menu/menuItems.d.ts +21 -0
  67. package/dist/components/core/menu/menuItems.d.ts.map +1 -0
  68. package/dist/components/core/menu/menuKeyboard.d.ts +16 -0
  69. package/dist/components/core/menu/menuKeyboard.d.ts.map +1 -0
  70. package/dist/components/feedback/ActionSheet.d.ts +4 -0
  71. package/dist/components/feedback/ActionSheet.d.ts.map +1 -0
  72. package/dist/components/feedback/Alert.d.ts.map +1 -1
  73. package/dist/components/feedback/Dialog.d.ts.map +1 -1
  74. package/dist/components/feedback/Drawer.d.ts.map +1 -1
  75. package/dist/components/feedback/Sheet.d.ts +4 -0
  76. package/dist/components/feedback/Sheet.d.ts.map +1 -0
  77. package/dist/components/feedback/Toast.d.ts +4 -3
  78. package/dist/components/feedback/Toast.d.ts.map +1 -1
  79. package/dist/components/feedback/ToastItem.d.ts +47 -0
  80. package/dist/components/feedback/ToastItem.d.ts.map +1 -0
  81. package/dist/components/feedback/ToastStack.d.ts +20 -0
  82. package/dist/components/feedback/ToastStack.d.ts.map +1 -0
  83. package/dist/components/feedback/ToastSwipe.d.ts +22 -0
  84. package/dist/components/feedback/ToastSwipe.d.ts.map +1 -0
  85. package/dist/components/feedback/Tour.d.ts.map +1 -1
  86. package/dist/components/feedback/sheetCore.d.ts +30 -0
  87. package/dist/components/feedback/sheetCore.d.ts.map +1 -0
  88. package/dist/components/navigation/Breadcrumbs.d.ts.map +1 -1
  89. package/dist/components/navigation/FootNav.d.ts.map +1 -1
  90. package/dist/components/navigation/Navbar.d.ts.map +1 -1
  91. package/dist/components/navigation/Sidebar.d.ts +6 -0
  92. package/dist/components/navigation/Sidebar.d.ts.map +1 -1
  93. package/dist/components/rich-text/RichTextEditor.d.ts.map +1 -1
  94. package/dist/components/rich-text/RichTextToolbar.d.ts +6 -2
  95. package/dist/components/rich-text/RichTextToolbar.d.ts.map +1 -1
  96. package/dist/components/utility/Avatar.d.ts +8 -0
  97. package/dist/components/utility/Avatar.d.ts.map +1 -1
  98. package/dist/components/utility/AvatarGroup.d.ts +4 -0
  99. package/dist/components/utility/AvatarGroup.d.ts.map +1 -0
  100. package/dist/components/utility/Badge.d.ts.map +1 -1
  101. package/dist/components/utility/Card.d.ts.map +1 -1
  102. package/dist/components/utility/Carousel.d.ts.map +1 -1
  103. package/dist/components/utility/FileUpload.d.ts.map +1 -1
  104. package/dist/components/utility/Pagination.d.ts.map +1 -1
  105. package/dist/components/utility/PullToRefresh.d.ts +5 -0
  106. package/dist/components/utility/PullToRefresh.d.ts.map +1 -0
  107. package/dist/components/utility/Sortable.d.ts.map +1 -1
  108. package/dist/components/utility/Stepper.d.ts.map +1 -1
  109. package/dist/esm/components/blocks/AppShell.js +97 -90
  110. package/dist/esm/components/blocks/BottomActionBar.js +132 -0
  111. package/dist/esm/components/blocks/ConfirmDialog.js +40 -4
  112. package/dist/esm/components/blocks/EmptyState.js +1 -1
  113. package/dist/esm/components/blocks/ErrorState.js +1 -1
  114. package/dist/esm/components/blocks/LoadingState.js +4 -4
  115. package/dist/esm/components/blocks/PageHeader.js +16 -3
  116. package/dist/esm/components/blocks/StatCard.js +6 -6
  117. package/dist/esm/components/blocks/StatusPill.js +5 -8
  118. package/dist/esm/components/charts/CartesianChart.js +85 -62
  119. package/dist/esm/components/charts/ChartFrame.js +12 -5
  120. package/dist/esm/components/charts/useChartLayout.js +105 -0
  121. package/dist/esm/components/core/Button.js +49 -25
  122. package/dist/esm/components/core/Checkbox.js +72 -57
  123. package/dist/esm/components/core/ColorPicker.js +31 -42
  124. package/dist/esm/components/core/DatePicker.js +117 -304
  125. package/dist/esm/components/core/DateRangePicker.js +260 -308
  126. package/dist/esm/components/core/Form.js +11 -2
  127. package/dist/esm/components/core/Input.js +100 -38
  128. package/dist/esm/components/core/Loader.js +30 -44
  129. package/dist/esm/components/core/LocationPicker.js +82 -16
  130. package/dist/esm/components/core/Popover.js +59 -48
  131. package/dist/esm/components/core/ProgressBar.js +40 -72
  132. package/dist/esm/components/core/Radio.js +10 -12
  133. package/dist/esm/components/core/Rate.js +77 -14
  134. package/dist/esm/components/core/RoundedTag.js +5 -5
  135. package/dist/esm/components/core/SearchField.js +311 -0
  136. package/dist/esm/components/core/SegmentedControl.js +189 -0
  137. package/dist/esm/components/core/Select.js +185 -190
  138. package/dist/esm/components/core/Skeleton.js +13 -14
  139. package/dist/esm/components/core/Slider.js +24 -6
  140. package/dist/esm/components/core/SpeechToText.js +68 -16
  141. package/dist/esm/components/core/SwipeableListItem.js +286 -0
  142. package/dist/esm/components/core/SwipeableListItemGesture.js +219 -0
  143. package/dist/esm/components/core/Switch.js +98 -28
  144. package/dist/esm/components/core/Table/ActionMenu.js +45 -231
  145. package/dist/esm/components/core/Table/ColumnMenu.js +1 -1
  146. package/dist/esm/components/core/Table/MobileCard.js +66 -16
  147. package/dist/esm/components/core/Table/MobileFilters.js +181 -10
  148. package/dist/esm/components/core/Table/Table.js +149 -102
  149. package/dist/esm/components/core/Table/TableRow.js +10 -2
  150. package/dist/esm/components/core/Tabs.js +186 -39
  151. package/dist/esm/components/core/Textarea.js +10 -2
  152. package/dist/esm/components/core/ThreeDotPopover.js +93 -99
  153. package/dist/esm/components/core/Tooltip.js +138 -145
  154. package/dist/esm/components/core/internal/Calendar.js +381 -0
  155. package/dist/esm/components/core/menu/ActionMenuSurface.js +110 -0
  156. package/dist/esm/components/core/menu/ActionSheetMenu.js +140 -0
  157. package/dist/esm/components/core/menu/MenuPopover.js +177 -0
  158. package/dist/esm/components/core/menu/menuItems.js +61 -0
  159. package/dist/esm/components/core/menu/menuKeyboard.js +54 -0
  160. package/dist/esm/components/feedback/ActionSheet.js +55 -0
  161. package/dist/esm/components/feedback/Alert.js +63 -53
  162. package/dist/esm/components/feedback/Dialog.js +141 -53
  163. package/dist/esm/components/feedback/Drawer.js +130 -132
  164. package/dist/esm/components/feedback/Sheet.js +125 -0
  165. package/dist/esm/components/feedback/Toast.js +21 -291
  166. package/dist/esm/components/feedback/ToastItem.js +348 -0
  167. package/dist/esm/components/feedback/ToastStack.js +130 -0
  168. package/dist/esm/components/feedback/ToastSwipe.js +52 -0
  169. package/dist/esm/components/feedback/Tour.js +107 -30
  170. package/dist/esm/components/feedback/sheetCore.js +494 -0
  171. package/dist/esm/components/navigation/Breadcrumbs.js +97 -35
  172. package/dist/esm/components/navigation/Dropdown.js +1 -1
  173. package/dist/esm/components/navigation/FootNav.js +102 -13
  174. package/dist/esm/components/navigation/Navbar.js +172 -37
  175. package/dist/esm/components/navigation/Sidebar.js +263 -111
  176. package/dist/esm/components/rich-text/RichTextEditor.js +9 -3
  177. package/dist/esm/components/rich-text/RichTextToolbar.js +29 -9
  178. package/dist/esm/components/utility/Avatar.js +14 -12
  179. package/dist/esm/components/utility/AvatarGroup.js +56 -0
  180. package/dist/esm/components/utility/Badge.js +43 -23
  181. package/dist/esm/components/utility/Card.js +32 -8
  182. package/dist/esm/components/utility/Carousel.js +139 -29
  183. package/dist/esm/components/utility/FileUpload.js +106 -31
  184. package/dist/esm/components/utility/Pagination.js +31 -13
  185. package/dist/esm/components/utility/Portal.js +31 -0
  186. package/dist/esm/components/utility/PullToRefresh.js +229 -0
  187. package/dist/esm/components/utility/Sortable.js +6 -2
  188. package/dist/esm/components/utility/Stepper.js +110 -47
  189. package/dist/esm/hooks/useFloatingPosition.js +82 -0
  190. package/dist/esm/hooks/useHaptics.js +65 -0
  191. package/dist/esm/hooks/usePresence.js +58 -0
  192. package/dist/esm/hooks/usePresentation.js +21 -0
  193. package/dist/esm/hooks/useSheetDrag.js +220 -0
  194. package/dist/esm/hooks/useSliderValues.js +8 -2
  195. package/dist/esm/hooks/useSortable.js +107 -5
  196. package/dist/esm/index.js +14 -2
  197. package/dist/esm/utils/position.js +10 -63
  198. package/dist/esm/utils/touchTarget.js +4 -1
  199. package/dist/hooks/useFloatingPosition.d.ts +8 -4
  200. package/dist/hooks/useFloatingPosition.d.ts.map +1 -1
  201. package/dist/hooks/useHaptics.d.ts +19 -0
  202. package/dist/hooks/useHaptics.d.ts.map +1 -0
  203. package/dist/hooks/useSheetDrag.d.ts +31 -0
  204. package/dist/hooks/useSheetDrag.d.ts.map +1 -0
  205. package/dist/hooks/useSliderValues.d.ts +6 -2
  206. package/dist/hooks/useSliderValues.d.ts.map +1 -1
  207. package/dist/hooks/useSortable.d.ts +11 -1
  208. package/dist/hooks/useSortable.d.ts.map +1 -1
  209. package/dist/index.css +1 -1
  210. package/dist/index.d.ts +11 -2
  211. package/dist/index.esm.css +1 -1
  212. package/dist/index.esm.js +10645 -3842
  213. package/dist/index.js +10657 -3845
  214. package/dist/rich-text/index.js +41 -11
  215. package/dist/types/blocks.d.ts +32 -2
  216. package/dist/types/blocks.d.ts.map +1 -1
  217. package/dist/types/charts.d.ts +3 -1
  218. package/dist/types/charts.d.ts.map +1 -1
  219. package/dist/types/index.d.ts +409 -26
  220. package/dist/types/index.d.ts.map +1 -1
  221. package/dist/types/rich-text.d.ts +5 -0
  222. package/dist/types/rich-text.d.ts.map +1 -1
  223. package/dist/utils/position.d.ts +0 -15
  224. package/dist/utils/position.d.ts.map +1 -1
  225. package/doc/README.md +16 -0
  226. package/mcp/catalog.snapshot.json +1540 -407
  227. package/package.json +7 -2
  228. package/dist/esm/hooks/useSwipeToClose.js +0 -66
  229. package/dist/hooks/useSwipeToClose.d.ts +0 -24
  230. package/dist/hooks/useSwipeToClose.d.ts.map +0 -1
package/README.md CHANGED
@@ -68,6 +68,22 @@ npm install @dreamtree-org/twreact-ui
68
68
  yarn add @dreamtree-org/twreact-ui
69
69
  ```
70
70
 
71
+ ### Upgrading from 1.x
72
+
73
+ 2.0.0 removes the 1.x deprecated colour props and renames the icon props to the shared `leftIcon` / `rightIcon`. A codemod handles most of it:
74
+
75
+ ```bash
76
+ npm install @dreamtree-org/twreact-ui@^2.0.0
77
+ npx jscodeshift -t node_modules/@dreamtree-org/twreact-ui/codemods/v2.cjs src/
78
+ ```
79
+
80
+ - **Input:** `icon` + `iconPosition` became `leftIcon` / `rightIcon`. A controlled Input's clear button now calls only `onClear`, so clear `value` there.
81
+ - **`Tabs.Tab`:** `iconLeft` / `iconRight` became `leftIcon` / `rightIcon`.
82
+ - **ProgressBar:** the HEX `color` / `bgColor` / `valueColor` props are gone; use `variant`.
83
+ - **Checkbox / Switch / Rate:** `borderColor`, `activeColor` / `inactiveColor` and the `blue` / `indigo` / `purple` presets are gone.
84
+
85
+ [`doc/MIGRATION-2.0.md`](doc/MIGRATION-2.0.md) has before/after code for each change and lists what the codemod leaves to you.
86
+
71
87
  Import the stylesheet once (app entry or root layout):
72
88
 
73
89
  ```js
@@ -235,7 +251,6 @@ Enhanced text input with validation states, icons, and clear functionality.
235
251
 
236
252
  **Props:**
237
253
  - `type`: 'text' | 'password' | 'email' | 'search' | 'number' | 'tel' | 'url'
238
- - `variant`: 'default' | 'filled' | 'outlined'
239
254
  - `size`: 'sm' | 'md' | 'lg' (default: 'md')
240
255
  - `label`: string
241
256
  - `placeholder`: string
@@ -245,12 +260,12 @@ Enhanced text input with validation states, icons, and clear functionality.
245
260
  - `disabled`: boolean
246
261
  - `required`: boolean
247
262
  - `clearable`: boolean
248
- - `icon`: ReactNode
249
- - `iconPosition`: 'left' | 'right' (default: 'left')
263
+ - `leftIcon`: ReactNode — icon before the value (2.0: was `icon`)
264
+ - `rightIcon`: ReactNode — icon after the value (2.0: was `icon` + `iconPosition="right"`)
250
265
  - `showCount`: boolean
251
266
  - `maxLength`: number
252
267
  - `readOnly`: boolean
253
- - `onClear`: () => void
268
+ - `onClear`: () => void — called by the clear button. A **controlled** Input clears its own `value` here (2.0: no synthetic `onChange`)
254
269
 
255
270
  ```jsx
256
271
  <Input
@@ -669,6 +684,22 @@ User profile images with fallbacks and status indicators.
669
684
  />
670
685
  ```
671
686
 
687
+ #### AvatarGroup
688
+ Overlapping stack of avatars with a "+N" overflow tile.
689
+
690
+ **Props:**
691
+ - `max`: number. Avatars past `max` collapse into a "+N" tile
692
+ - `size`: Avatar size passed to every child that doesn't set its own (default: 'md')
693
+
694
+ ```jsx
695
+ <AvatarGroup max={3} size="sm">
696
+ <Avatar name="Ana" />
697
+ <Avatar name="Ben" />
698
+ <Avatar name="Cy" />
699
+ <Avatar name="Dee" />
700
+ </AvatarGroup>
701
+ ```
702
+
672
703
  #### Badge
673
704
  Status indicators and labels with different variants.
674
705
 
@@ -677,6 +708,9 @@ Status indicators and labels with different variants.
677
708
  - `variant`: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | 'outline' (default: 'default')
678
709
  - `size`: 'sm' | 'md' | 'lg' (default: 'md')
679
710
  - `rounded`: boolean (default: true)
711
+ - `dot`: boolean. A status dot before the label (default: false)
712
+ - `icon`: ReactNode. A leading icon
713
+ - `max`: number. A number child above `max` shows `${max}+`
680
714
 
681
715
  ```jsx
682
716
  <Badge variant="success" size="sm">
@@ -718,19 +752,31 @@ Interaction primitives that products used to compose by hand. All are `forwardRe
718
752
  | Component | Group | Use for |
719
753
  |---|---|---|
720
754
  | [`Drawer`](doc/Drawer.md) | Feedback | Side/edge sheet (`placement` right/left/top/bottom, `size` sm–full) — record peeks and editors. `placement="bottom"` is a phone bottom sheet (grab `handle`, `swipeToClose`, rounded top, safe-area padding). Same `isOpen`/`onClose` contract as `Dialog`; focus trap, Escape/backdrop close, focus restore, scroll lock; `Drawer.Header/Body/Footer` |
755
+ | [`Sheet`](doc/Sheet.md) | Feedback | Phone-native sheet (`placement` bottom/top/left/right): `snapPoints` (viewport fractions, `activeSnap`/`defaultSnap`/`onSnapChange`), drag-to-dismiss with rubber band and flick, keyboard-aware via `visualViewport`, opt-in `scaleBackground`. `open` (or `isOpen`) + `onClose(reason)`; focus trap, Escape/backdrop close, focus restore, scroll lock; `Sheet.Header/Body/Footer`. Use for phone sheets; keep `Drawer` for record peeks |
721
756
  | [`Popover`](doc/Popover.md) | Core | Anchored, click-triggered panel with interactive content (unlike `Tooltip`). Controlled/uncontrolled `open`, `placement` with flip, Escape/outside-click close, `aria-expanded`/`aria-controls`/`aria-haspopup="dialog"` |
722
757
  | [`Textarea`](doc/Textarea.md) | Core | Multi-line input mirroring `Input`: `label`, `error`, `helperText`, `required`, `showCount`/`maxLength`, `autoResize`/`maxRows` |
723
758
  | [`Slider`](doc/Slider.md) | Core | Single value or `[low, high]` range; `min`/`max`/`step`, arrows/PageUp/PageDown/Home/End, `role="slider"` with full `aria-value*` |
724
759
  | [`Sortable`](doc/Sortable.md) | Utility | Drag-to-reorder list (pointer events, no extra dependency) + keyboard reordering (Space, arrows, Space/Escape) with live announcements; `items`, `onReorder`, `renderItem`, `getKey` |
725
760
  | [`Tour`](doc/Tour.md) | Feedback | Step-based onboarding: spotlight on each `target` (selector or ref), Back/Next/Skip/Finish, `open`/`onClose`/`onFinish`, scrolls targets into view |
761
+ | [`ActionSheet`](doc/ActionSheet.md) | Feedback | iOS action sheet on `Sheet`: `actions=[{ key?, label, icon?, tone?: 'default' | 'destructive', disabled?, onSelect }]`, optional `title`/`message`, a separate Cancel button, `onClose(reason)` |
762
+ | [`SegmentedControl`](doc/SegmentedControl.md) | Core | Radiogroup view toggle (list/board, day/week): `options`, `value`/`defaultValue` + `onChange`, `size`, `fullWidth`, `name` for forms; arrow keys move the selection. Use `Tabs variant="segmented"` when the choice switches panels |
763
+ | [`SwipeableListItem`](doc/SwipeableListItem.md) | Core | Mail-style swipe-to-reveal row: `leadingActions`/`trailingActions`, `fullSwipe` commits the outer action, controlled `open`; a "More actions" button reaches every action without gestures. Table cards take `swipeActions` |
764
+ | [`SearchField`](doc/SearchField.md) | Core | iOS-style search input: clear button, phone Cancel, `onValueChange`/`onSearch`; `results`/`renderResults` show in a Sheet on phones and a popover elsewhere (`presentation`) |
765
+ | [`PullToRefresh`](doc/PullToRefresh.md) | Utility | Touch pull-to-refresh around a scroller: `onRefresh` returns a promise, `threshold`, `maxPull`, `disabled`, `scrollRef` |
766
+ | [`BottomActionBar`](doc/BottomActionBar.md) | Block | Sticky bottom bar for a form's primary action: `pb-safe`, rides above the on-screen keyboard (`keyboardAware`), `stackOnMobile`; add `<BottomActionBar.Spacer />` at the end of the page |
726
767
 
727
768
  ```jsx
728
- import { Drawer, Popover, Textarea, Slider, Sortable, Tour, Button } from '@dreamtree-org/twreact-ui';
769
+ import { Drawer, Sheet, Popover, Textarea, Slider, Sortable, Tour, Button } from '@dreamtree-org/twreact-ui';
729
770
 
730
771
  <Drawer isOpen={open} onClose={() => setOpen(false)} title="Edit task" footer={<Button onClick={save}>Save</Button>}>
731
772
  <Textarea label="Description" autoResize showCount maxLength={500} />
732
773
  </Drawer>
733
774
 
775
+ <Sheet open={filtersOpen} onClose={() => setFiltersOpen(false)} title="Filters" snapPoints={[0.5, 1]}>
776
+ <Sheet.Body>…</Sheet.Body>
777
+ <Sheet.Footer><Button onClick={apply}>Apply</Button></Sheet.Footer>
778
+ </Sheet>
779
+
734
780
  <Popover trigger={<Button variant="outline">Filters</Button>} title="Filters">
735
781
  <Slider label="Budget" defaultValue={[2000, 8000]} min={0} max={10000} step={500} showValue />
736
782
  </Popover>
@@ -841,6 +887,9 @@ fetchApiWith('updateUser', [7, { name: 'Ada' }]);
841
887
 
842
888
  See [doc/useApi.md](doc/useApi.md).
843
889
 
890
+ #### useHaptics
891
+ Thin `navigator.vibrate` wrapper. Returns `{ trigger, supported, reducedMotion }`; `trigger('selection' | 'success' | 'warning' | 'error' | ms | number[])` does nothing where vibration is unsupported (iOS Safari) or reduced motion is on. `Switch`, `Checkbox`, `ConfirmDialog` and `Sheet` take an opt-in `haptics` prop that uses it.
892
+
844
893
  ## Theming
845
894
 
846
895
  Every colour in the compiled CSS is a CSS variable (`--dt-primary-600`, `--dt-surface`, …) generated from `tokens/tokens.json` (W3C Design Tokens format). The defaults equal the classic palette, so nothing changes until you override a variable.
@@ -885,7 +934,7 @@ The palettes are `primary`, `secondary`, `success`, `warning`, `error`, `info` a
885
934
 
886
935
  **Design rules:** [`DESIGN.md`](DESIGN.md) is the rule set for product screens and AI agents: tokens only, blocks first, loading, empty and error states, 390 px, no native dialogs. It is generated from the tokens (`npm run build:tokens`).
887
936
 
888
- **Colour props are overrides, not defaults.** Without colour props, `ProgressBar`, `Checkbox`, `Rate` and `Skeleton` use token classes, so a brand file recolours them. Their HEX props (`color`, `bgColor`, `borderColor`, `activeColor`, `inactiveColor`) still work but are deprecated.
937
+ **Colours come from tokens.** `ProgressBar`, `Checkbox`, `Switch`, `Rate` and `Skeleton` use token classes, so a brand file recolours them. 2.0.0 removed their raw-colour escape hatches: ProgressBar's HEX `color` / `bgColor` / `valueColor` (use `variant`), Checkbox's `borderColor`, Rate's `activeColor` / `inactiveColor`, and the `blue` / `indigo` / `purple` presets on Checkbox and Switch. See [`doc/MIGRATION-2.0.md`](doc/MIGRATION-2.0.md).
889
938
 
890
939
  **SSR:** `Tabs`, `Tooltip` and `Switch` generate ids with `React.useId()`, so server-rendered pages (Next.js) hydrate without mismatches. The `/charts` components do the same for their SVG ids, and they render token colours and reduced-motion state the way the server did until hydration finishes.
891
940
 
@@ -893,6 +942,21 @@ The palettes are `primary`, `secondary`, `success`, `warning`, `error`, `info` a
893
942
 
894
943
  The stylesheet ships a phone baseline: no tap highlight, no iOS zoom when a field is focused (text fields are 16px on touch screens; add `dt-keep-font-size` to opt one out), `color-scheme` that follows the theme, and no rubber-band overscroll on `body` (add `dt-overscroll-auto` to `<html>` to keep pull-to-refresh). Pad edge bars with `pt-safe` / `pb-safe` / `px-safe` / `inset-safe` — they need `<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">` — and size full-height layouts with `min-h-dvh` instead of `100vh`. Touch targets grow to 44px on coarse pointers.
895
944
 
945
+ **Touch-friendly components (1.6–1.8).**
946
+ - **Controls:** `Input`/`Textarea` take `size`, `Input` adds `floatingLabel` and `otp`, and sets mobile keyboard hints for `tel`/`email`/`url`/`search`.
947
+ - **Buttons:** square `size="icon"` sizes, press feedback, and a loading state that keeps the button's width.
948
+ - **FootNav:** a native tab bar with safe area, links (`href` + `linkAs`), `hideOnScroll` and `FootNav.Spacer`.
949
+ - **Tabs:** scroll with snap, plus a `segmented` variant and `swipeable`.
950
+ - **Navigation:** `Pagination` goes compact on phones, `Stepper` can stack (`orientation="auto"`), and `Breadcrumbs` collapses (`maxItems`).
951
+ - **Carousel:** a native scroll-snap swiper.
952
+ - **Sortable:** drags after a long-press on touch.
953
+ - **FileUpload:** shows "Choose file" / "Take photo" buttons on touch (`capture`).
954
+ - **Charts:** take `aspectRatio` and pin the tooltip on tap.
955
+
956
+ **Adaptive overlays.** On phones (coarse pointer, under 640px), `Select`, `DatePicker`, `DateRangePicker`, `ThreeDotPopover`, the Table row menu, `Dialog`, `ConfirmDialog`, `Popover` and `Tour` open as a bottom `Sheet` (or an action sheet, or a docked card) by default. Each takes `presentation="auto" | "popover" | "sheet"`; use `"popover"` to keep the desktop behaviour. `Tooltip` opens on long-press on touch screens.
957
+
958
+ **Native feel and PWA guide.** [`doc/MobileNativeFeel.md`](doc/MobileNativeFeel.md) walks through the rest of the setup for phones and installed apps: the viewport meta, per-theme `theme-color` kept in sync with `ThemeProvider`, a `display: "standalone"` web manifest, `dvh` layouts, the `AppShell` + `FootNav` + `Sheet` recipe, when to override `presentation`, safe areas, opt-in haptics, and a real-device testing checklist for iOS Safari and Android Chrome.
959
+
896
960
  ## Dark Mode
897
961
 
898
962
  Enable dark mode with the built-in theme provider:
@@ -40,6 +40,13 @@ npm install @tiptap/react @tiptap/pm @tiptap/starter-kit
40
40
 
41
41
  Apps that don't import these subpaths install and bundle nothing extra.
42
42
 
43
+ **Upgrading from 1.x to 2.0:** run the bundled codemod, then fix what it reports
44
+ (see `doc/MIGRATION-2.0.md`, also served as `dreamtree://docs/MIGRATION-2.0`):
45
+
46
+ ```bash
47
+ npx jscodeshift -t node_modules/@dreamtree-org/twreact-ui/codemods/v2.cjs src/
48
+ ```
49
+
43
50
  ## Wiring (do not invent alternatives)
44
51
 
45
52
  ```jsx
@@ -106,6 +113,9 @@ wire it up and **query it instead of guessing prop names**:
106
113
  - `search_components(query)` — keyword search by capability.
107
114
 
108
115
  Resources: `dreamtree://skill` (this guide) and `dreamtree://docs/<Component>`.
116
+ Guide: `dreamtree://docs/MobileNativeFeel` (`doc/MobileNativeFeel.md`) — making an app
117
+ feel native on phones / as a PWA (viewport + `theme-color` meta, manifest, `dvh`, safe
118
+ areas, AppShell + FootNav + Sheet, `presentation` overrides, device testing checklist).
109
119
  Prompt: `compose_ui`. The catalog is a snapshot baked into the package, so it is
110
120
  self-contained and always matches the installed library version. The lists below
111
121
  are the fallback when the MCP server isn't connected.
@@ -118,11 +128,11 @@ The library exports four kinds of things from `@dreamtree-org/twreact-ui`:
118
128
 
119
129
  | Group | Components |
120
130
  | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
121
- | **Core** | `Input`, `Button`, `Select`, `Table`, `Form`, `Accordion`, `Checkbox`, `ColorPicker`, `DatePicker`, `DateRangePicker`, `Loader`, `LocationPicker`, `PriceRangePicker`, `ProgressBar`, `Radio`, `Rate`, `RoundedTag`, `Skeleton`, `Switch`, `Tabs`, `ThreeDotPopover`, `Tooltip`, `SpeechToText`, `TextToSpeech`, `Textarea`, `Slider`, `Popover` |
131
+ | **Core** | `Input`, `Button`, `Select`, `Table`, `Form`, `Accordion`, `Checkbox`, `ColorPicker`, `DatePicker`, `DateRangePicker`, `Loader`, `LocationPicker`, `PriceRangePicker`, `ProgressBar`, `Radio`, `Rate`, `RoundedTag`, `Skeleton`, `Switch`, `Tabs`, `ThreeDotPopover`, `Tooltip`, `SpeechToText`, `TextToSpeech`, `Textarea`, `Slider`, `Popover`, `SegmentedControl`, `SwipeableListItem` (swipe-to-reveal row actions), `SearchField` |
122
132
  | **Navigation** | `Sidebar`, `Navbar`, `FootNav`, `Breadcrumbs` |
123
- | **Feedback** | `Dialog`, `Toast` (+ `ToastProvider`, `ToastContainer`, `useToast`), `Alert`, `Drawer` (side sheet; `isOpen`/`onClose` like Dialog), `Tour` (onboarding spotlight) |
124
- | **Utility** | `Badge`, `Avatar`, `Card`, `Pagination`, `Stepper`, `FileUpload`, `Condition`, `Carousel`, `Sortable` (drag + keyboard reorder) |
125
- | **Blocks** | `AppShell`, `PageHeader`, `StatCard`, `StatusPill`, `EmptyState`, `ErrorState`, `LoadingState`, `ConfirmDialog` — page-level patterns; build pages from these before composing layouts by hand |
133
+ | **Feedback** | `Dialog`, `Toast` (+ `ToastProvider`, `ToastContainer`, `useToast`), `Alert`, `Drawer` (side sheet; `isOpen`/`onClose` like Dialog), `Sheet` (+ `Sheet.Header/Body/Footer`; phone sheet with snap points), `ActionSheet` (iOS action sheet: `actions` + separate Cancel), `Tour` (onboarding spotlight) |
134
+ | **Utility** | `Badge`, `Avatar`, `AvatarGroup` (overlapping stack + `max` "+N"), `Card`, `Pagination`, `Stepper`, `FileUpload`, `Condition`, `Carousel`, `Sortable` (drag + keyboard reorder), `PullToRefresh` (touch pull-to-refresh around a scroller) |
135
+ | **Blocks** | `AppShell`, `PageHeader`, `StatCard`, `StatusPill`, `EmptyState`, `ErrorState`, `LoadingState`, `ConfirmDialog`, `BottomActionBar` (+ `BottomActionBar.Spacer`) — page-level patterns; build pages from these before composing layouts by hand |
126
136
  | **Charts** (`@dreamtree-org/twreact-ui/charts`, peer `recharts`) | `LineChart`, `BarChart`, `AreaChart`, `DonutChart`, `Sparkline` — NOT exported from the root |
127
137
  | **Rich text** (`@dreamtree-org/twreact-ui/rich-text`, peers `@tiptap/react @tiptap/pm @tiptap/starter-kit`) | `RichTextEditor` — NOT exported from the root |
128
138
 
@@ -133,6 +143,7 @@ The library exports four kinds of things from `@dreamtree-org/twreact-ui`:
133
143
  | `useTheme` | Read/write the current theme (`light | dark`); pairs with `<ThemeProvider>`. Throws if used outside the provider. |
134
144
  | `ThemeProvider` | Provider that persists theme to `localStorage` (`dreamtree-theme`) and toggles `data-theme` and the `.dark` class on `<html>`. |
135
145
  | `useApi` | Thin axios wrapper. Returns `{data, error, loading, lastRequest, fetchData, fetchApi, fetchApiWith}`. Accepts `BASE_URL`, `DEFAULT_HEADERS`, `apiMap`. |
146
+ | `useHaptics` | Opt-in `navigator.vibrate` wrapper. Returns `{trigger(pattern), supported, reducedMotion}`; patterns `selection` / `success` / `warning` / `error`. No-op on iOS Safari, the server and under reduced motion. |
136
147
  | `useMixins` | Bridge between component state, Redux store, and an in-memory cache. Power-user hook — prefer local React state for new code. |
137
148
 
138
149
  ### Store
@@ -172,36 +183,57 @@ Components use a **shared variant/size/focus vocabulary**:
172
183
  (component-specific extras like `Badge: info` and `Alert: neutral` are documented per-component).
173
184
  - **Sizes:** `xs | sm | md | lg | xl | 2xl` mapping to heights `h-7 / h-8 / h-10 / h-12 / h-14 / h-16`
174
185
  and padding `px-2 / px-3 / px-4 / px-6 / px-8 / px-10` (`Button` implements all six;
175
- other components MAY support a subset and fall back to `md`).
186
+ other components MAY support a subset and fall back to `md`). `Button` also has square
187
+ icon-only sizes `icon-sm | icon | icon-lg` (always with `aria-label`).
176
188
  - **Focus ring:** `focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500`
177
189
  (the color token swaps for destructive / etc.).
178
190
  - **Dark mode:** Tailwind `darkMode: 'class'`. Every color choice has a `dark:` counterpart.
179
191
 
192
+ - **Icon slots:** `leftIcon` / `rightIcon` (Button, Input, `Tabs.Tab`).
193
+ - **Colour is a token, never a HEX prop** (2.0.0): `ProgressBar variant="success"`, not
194
+ `color="#16a34a"`; Checkbox / Switch `color` takes `primary | secondary | success | error |
195
+ warning`. Checkbox `borderColor`, Rate `activeColor` / `inactiveColor` and the
196
+ `blue` / `indigo` / `purple` presets no longer exist.
197
+ - **Controlled clearable Input:** clear `value` in `onClear` — the clear button does not call
198
+ `onChange` (2.0.0).
199
+
180
200
  When suggesting a component or new variant value, use this exact vocabulary.
181
201
 
182
202
  ## Page blocks and design tokens
183
203
 
184
204
  **Blocks** are page-level patterns extracted from the products, so screens stop re-inventing them:
185
205
 
186
- - `AppShell` — sidebar + top bar + mobile drawer; pass `nav` items and `linkAs={Link}` in Next.js.
187
- - `Sidebar` — every item renders as an `<a>`; give items an `href` (and `linkAs={Link}` in Next.js) so Ctrl/Cmd-click opens a new tab. Parents with `href` navigate + expand; the chevron toggles. Mark the current page `active: true` and its group opens on load (control groups with `expandedIds`/`defaultExpandedIds`/`onExpandedChange`). Top-level `group: 'Name'` adds section headings; `badge` (+ `badgeLabel: '3 unread'`) shows counts; `footer` pins links above the user block; `stickyOffset={64}` when a top bar sits above it. The mobile drawer (`isMobileOpen`/`setIsMobileOpen`) is inert when shut and closes on Escape.
188
- - `PageHeader` — the page's only `h1`, subtitle, one primary action, optional `back` link.
206
+ - `AppShell` — sidebar + top bar + mobile drawer (a left `Sheet`); pass `nav` items and `linkAs={Link}` in Next.js. `bottomNav={<FootNav … />}` adds a phone tab bar (below `md`, spacer included). Fills `min-h-dvh` and pads safe areas.
207
+ - `Sidebar` — every item renders as an `<a>`; give items an `href` (and `linkAs={Link}` in Next.js) so Ctrl/Cmd-click opens a new tab. Parents with `href` navigate + expand; the chevron toggles. Mark the current page `active: true` and its group opens on load (control groups with `expandedIds`/`defaultExpandedIds`/`onExpandedChange`). Top-level `group: 'Name'` adds section headings; `badge` (+ `badgeLabel: '3 unread'`) shows counts; `footer` pins links above the user block; `stickyOffset={64}` when a top bar sits above it. The mobile drawer (`isMobileOpen`/`setIsMobileOpen`) is inert when shut, closes on Escape, and opens/closes with an edge swipe (`swipeable`, default on). The content renders once (column on `md`+, drawer below), so don't render a second Sidebar for phones.
208
+ - `PageHeader` — the page's only `h1`, subtitle, one primary action, optional `back` link. `actionsPlacement="bottom"` pins the actions in a `BottomActionBar` on phones (inline on desktop).
209
+ - `BottomActionBar` — sticky bottom bar for a form's / checkout's primary action (`pb-safe`, `z-sticky`, rides above the on-screen keyboard via `visualViewport`). Children are the actions; `fullWidth` / `stackOnMobile` for phone layouts, `align`, `position="fixed" | "sticky"`. Put `<BottomActionBar.Spacer />` at the end of the page. Never hand-roll a `fixed bottom-0` action bar.
189
210
  - `StatCard` — KPI tile (`label`, `value`, `icon`, `tone`, `change`).
190
211
  - `StatusPill` — map domain states to `{ label, tone }` once; unknown states are humanized, never raw.
191
212
  - `EmptyState`, `ErrorState`, `LoadingState` — every data card needs all three.
192
- - `ConfirmDialog` — use instead of `window.confirm()`; title is the question, button is the verb.
213
+ - `ConfirmDialog` — use instead of `window.confirm()`; title is the question, button is the verb. Like `Dialog`, it takes `presentation` (`auto` default: bottom sheet on phones, centred modal elsewhere; `popover` = always centred; `sheet` = always a sheet). Write footer actions secondary-then-primary; sheet mode stacks them full width with the primary on top.
193
214
 
194
215
  **Interaction primitives** — reach for these before hand-rolling the pattern:
195
216
 
196
217
  - `Drawer` — side/edge sheet for record peeks and editors (`isOpen`, `onClose`, `title`, `placement`, `size`, `footer`); don't fake one with `Dialog size="lg"`. On phones use `placement="bottom"` (+ `swipeToClose`) as the bottom sheet: it already has a grab `handle`, rounded top and safe-area padding — don't add them with `className`.
197
- - `FootNav` — phone tab bar. A centre "+" is an item with `variant: 'action'` (raised primary circle); give it `'aria-haspopup': 'dialog'` and `'aria-expanded'` when it opens a sheet.
218
+ - `FootNav` — phone tab bar. A centre "+" is an item with `variant: 'action'` (raised primary circle); give it `'aria-haspopup': 'dialog'` and `'aria-expanded'` when it opens a sheet. Navigation tabs take `href`/`to` (+ `linkAs={Link}`) so they render as links with `aria-current="page"`; `hideOnScroll` slides it away on scroll down; put `<FootNav.Spacer />` after the page content instead of a hand-tuned `pb-20`. It already pads the safe area and caps badges at `99+` (`badgeMax`).
219
+ - `SwipeableListItem` — iOS Mail / Gmail row (`leadingActions` / `trailingActions`: `{ key, label, icon, tone, onSelect }`; `fullSwipe` commits the first action; `open`/`onOpenChange`; `as="li"`). Every action is also reachable through its "More actions" button (menu, action sheet on phones). `Table` uses it for mobile cards via `swipeActions={(row) => ({ leading, trailing })}`. Never hand-roll swipe gestures.
220
+ - `Sheet` — phone sheet (`open`, `onClose(reason)`, `placement` bottom/top/left/right, `snapPoints={[0.5, 1]}` + `activeSnap`/`onSnapChange`, `dismissible`, `title`) with drag-to-dismiss and keyboard-aware positioning; compose `Sheet.Header` / `Sheet.Body` (scrolls) / `Sheet.Footer`. Use it for phone sheets with snap points (filters, pickers, quick forms); keep `Drawer` for record peeks. Never hand-roll drag or `visualViewport` handling.
221
+ - `ActionSheet` — iOS `UIAlertController`-style action sheet on `Sheet` (`open`/`isOpen`, `onClose(reason)`, `title`, `message`, `actions={[{ key, label, icon, tone: 'destructive', onSelect, disabled }]}`, `cancelLabel` default "Cancel"; Cancel always renders separately). Selecting calls `onSelect` then `onClose('select')`; focus returns to the opener. Use it for "what to do with this?" choices on phones instead of a hand-built list in a `Sheet`; for a kebab menu use `ThreeDotPopover`, which already becomes an action sheet on phones.
198
222
  - `Button` as a link — `<Button href="tel:…">` or `<Button as={Link} to="/x">`; never copy Button's class string onto an `<a>`.
223
+ - Icon-only `Button` — `<Button size="icon" aria-label="Add"><Plus /></Button>` (`icon-sm`/`icon-lg` too); never a raw `<button>` with an icon. `loading` keeps the button's width.
224
+ - `Badge` — `dot` (live/status dot), `icon` (leading icon slot), `max={99}` with a numeric child renders `99+`.
225
+ - `AvatarGroup` — `<AvatarGroup max={3} size="sm">{people.map(p => <Avatar name={p.name} />)}</AvatarGroup>`; don't hand-roll `-space-x` stacks or a fake `<Avatar name="+5" />`.
199
226
  - `Select size="lg"` — 44px trigger for phone forms (`md` already grows to 44px on touch screens).
200
- - `Stepper` steps take `title` (or its alias `label`). `Radio` follows the primary token by default — don't pass `radioColor` to match the brand.
227
+ - `Input` — same `size` (`sm`/`md`/`lg`; `md` is 44px on touch screens); `floatingLabel` puts the label inside the field; `otp` for one-time codes. `type="tel|email|url|search"` already sets `inputMode`/`autoComplete` — don't add them by hand.
228
+ - `Stepper` steps take `title` (or its alias `label`); on phones use `orientation="auto"` (vertical below `sm`) or `variant="dots"` for compact progress. `Tabs variant="segmented"` is the iOS segmented control (2–4 short options); add `swipeable` for swiping between panels on touch. Many tabs scroll sideways on their own, so don't wrap `Tabs.List` in an `overflow-x-auto` div. `Breadcrumbs maxItems={3}` collapses deep trails into "…". `Pagination` collapses to "‹ Page 3 of 10 ›" below `sm` by itself. `Radio` follows the primary token by default — don't pass `radioColor` to match the brand.
229
+ - `SegmentedControl` — radiogroup view toggle (list / board, day / week): `options=[{ value, label, icon?, disabled? }]`, `value`/`defaultValue` + `onChange(value)`, `size` (`sm`/`md`/`lg`, 44px on touch screens), `fullWidth`, `name`, `aria-label`. Arrow keys move and select; the thumb slides. Use `Tabs variant="segmented"` instead when the choice switches panels.
230
+ - `SearchField` — iOS search input (`type="search"`, Search key, clear button, **Cancel** on phones while focused). `value`/`defaultValue` + `onValueChange(text)`, `onSearch(text)` on Enter, `onCancel`. Pass `results` or `renderResults={({ value, close }) => …}` for a results surface: a full-height Sheet on phones, a popover under the field elsewhere (`presentation` overrides). Never hand-roll a search input with an icon.
201
231
  - `Popover` — click-triggered panel with controls (`trigger={<Button>…</Button>}`, children or `({ close }) => …`); `Tooltip` is for read-only hover text only.
202
232
  - `Textarea` — same API as `Input` plus `autoResize`/`maxRows`; never a raw `<textarea>`.
203
- - `Slider` — `defaultValue={40}` for one thumb, `[low, high]` for a range; `onChangeEnd` to save after a drag.
233
+ - `Slider` — `defaultValue={40}` for one thumb, `[low, high]` for a range; `onChangeEnd` to save after a drag. A value bubble shows while dragging (`showBubble={false}` to hide).
204
234
  - `Sortable` — `items` + `onReorder(newItems)` + `renderItem`; keyboard reordering and announcements are built in. Pass `handle` and spread `handleProps` on a grip for touch screens.
235
+ - `PullToRefresh` — wrap a list: `<PullToRefresh onRefresh={() => refetch()} className="h-96">…</PullToRefresh>` (renders the scroller) or pass `scrollRef` to your own. `onRefresh` returns a promise; the spinner spins until it settles. `threshold` (64), `disabled`, `refreshingLabel`. Touch only — keep a refresh button for mouse/keyboard users.
236
+ - Haptics are opt-in: `haptics` on `Switch` / `Checkbox` (selection tick), `Sheet` (tick on a new snap point) and `ConfirmDialog` (warning on destructive open, success on confirm). Default `false`; never the only signal. For custom cues use `useHaptics().trigger('success')`.
205
237
  - Charts (`/charts`) — `<LineChart data={rows} index="month" categories={['revenue', 'costs']} />`; same shape for `BarChart` (`stacked`, `horizontal`) and `AreaChart` (`stacked`); `DonutChart` takes `index` (name key) + `category` (value key) + `centerLabel`; `Sparkline` takes `category` + `color`. Colours are tokens (`colors={['primary', 'success-600']}`), never hex. Always pass `title` (accessible name) and, for key charts, a `description` with the takeaway. `valueFormatter` formats tooltip/axis/table values.
206
238
  - `RichTextEditor` (`/rich-text`) — `label`/`error`/`helperText` like `Input`; `value` + `onChange(html)` (or `outputFormat="json"`); `toolbar={['bold', 'italic', 'link']}` to trim. Tell the user to **sanitize the HTML server-side**; never render it with `dangerouslySetInnerHTML` unsanitized.
207
239
  - `Tour` — onboarding coach marks: `steps=[{ target: '[data-tour="x"]', title, content, placement }]`, `open`, `onClose(reason)`; a missing target falls back to a centred card.
@@ -235,8 +267,8 @@ import { Button } from '@dreamtree-org/twreact-ui';
235
267
  <Button variant="outline" leftIcon={<Plus className="h-4 w-4" />}>Add item</Button>
236
268
  ```
237
269
 
238
- Props: `variant`, `size`, `disabled`, `loading`, `leftIcon`, `rightIcon`,
239
- `fullWidth`, `className`, plus all native `<button>` props.
270
+ Props: `variant`, `size` (incl. `icon-sm`/`icon`/`icon-lg`), `disabled`, `loading`, `leftIcon`, `rightIcon`,
271
+ `fullWidth`, `as`, `href`, `className`, plus all native `<button>` props.
240
272
 
241
273
  ### Input + Form
242
274
 
@@ -278,6 +310,9 @@ with a `group` key (`{ value, label, group: 'Code' }`) — no need to pre-group.
278
310
 
279
311
  **Navbar** takes a `center` slot (search field / primary action) and shows
280
312
  `user.name` initials when `user.avatar` is missing; it fits a 390px phone.
313
+ `sticky` makes it a translucent sticky bar with the top safe area; `largeTitle="Inbox"`
314
+ adds an iOS large title that collapses on scroll. With `searchable`, phones get a
315
+ Search button that opens a `SearchField` sheet wired to `onSearch`.
281
316
 
282
317
  **Tooltip** around a component (e.g. `<Button>`) in a row/grid: use
283
318
  `customTrigger={<Button>…</Button>}` so there is one tab stop, not two. A lone
@@ -307,6 +342,12 @@ function SomeButton() {
307
342
  }
308
343
  ```
309
344
 
345
+ Toasts swipe away horizontally, pause while hovered / focused / touched, and
346
+ past three collapse into a stack that expands on hover, focus or tap
347
+ (`expand` forces it open, `visibleToasts` changes the limit). On phones they
348
+ sit bottom-center, full width, above the FootNav — pass `mobileOffset={16}`
349
+ when the app has no FootNav, or `mobilePosition={false}` to keep `position`.
350
+
310
351
  ### Theme
311
352
 
312
353
  ```jsx
@@ -386,7 +427,9 @@ import { cn } from '@dreamtree-org/twreact-ui';
386
427
  matching primitive.
387
428
  2. **Use the shared vocabulary.** `variant="primary"`, `size="md"`,
388
429
  `leftIcon={...}`, `fullWidth`, `clearable` — these are exact names.
389
- Don't invent `color="blue"` or `iconLeft={...}`.
430
+ Don't invent `color="blue"` or `iconLeft={...}`. Icon slots are
431
+ `leftIcon` / `rightIcon` on `Button`, `Input` and `Tabs.Tab` (2.0 renamed
432
+ Input `icon`/`iconPosition` and Tab `iconLeft`/`iconRight`).
390
433
  3. **Wrap in `<ThemeProvider>`.** Examples that use `useTheme`, or rely
391
434
  on dark-mode classes, MUST show the provider.
392
435
  4. **`<StoreProvider>` is optional.** Only mention it when the user is
@@ -0,0 +1,198 @@
1
+ /**
2
+ * @dreamtree-org/twreact-ui 1.x -> 2.0.0 codemod (DTUI-67).
3
+ *
4
+ * npx jscodeshift -t node_modules/@dreamtree-org/twreact-ui/codemods/v2.cjs src/
5
+ *
6
+ * Only JSX on components imported from `@dreamtree-org/twreact-ui` is touched
7
+ * (named imports, aliases such as `{ Input as TextInput }`, and namespace
8
+ * imports such as `UI.Input`). See doc/MIGRATION-2.0.md for every change and
9
+ * for what this script leaves to you.
10
+ *
11
+ * Automated:
12
+ * - Input icon (+ iconPosition "left" / absent) -> leftIcon
13
+ * icon + iconPosition="right" -> rightIcon
14
+ * a literal iconPosition without icon -> removed
15
+ * - Tabs.Tab iconLeft -> leftIcon, iconRight -> rightIcon
16
+ * - Checkbox borderColor -> removed
17
+ * - ProgressBar color / bgColor / valueColor -> removed
18
+ * - Rate activeColor / inactiveColor -> removed
19
+ *
20
+ * Reported for a manual fix (the file and line are printed):
21
+ * - Input with a non-literal iconPosition
22
+ * - Input `clearable` + `value` without `onClear` (clear no longer calls onChange)
23
+ * - Checkbox / Switch color="blue" | "indigo" | "purple"
24
+ * - ProgressBar that had a colour prop (pick a `variant`)
25
+ *
26
+ * The default parser is `tsx`, which reads .js, .jsx, .ts and .tsx files.
27
+ * Pass `--parser=babel` (or another) to override.
28
+ */
29
+
30
+ const PACKAGE = "@dreamtree-org/twreact-ui";
31
+ const RAW_PRESETS = new Set(["blue", "indigo", "purple"]);
32
+
33
+ const REMOVED = {
34
+ Checkbox: ["borderColor"],
35
+ ProgressBar: ["color", "bgColor", "valueColor"],
36
+ Rate: ["activeColor", "inactiveColor"],
37
+ };
38
+
39
+ const RENAMED = {
40
+ "Tabs.Tab": { iconLeft: "leftIcon", iconRight: "rightIcon" },
41
+ };
42
+
43
+ /** Map local JSX names to the library component they refer to. */
44
+ function collectImports(j, root) {
45
+ const named = new Map(); // local name -> exported name
46
+ const namespaces = new Set(); // `import * as UI`
47
+ root
48
+ .find(j.ImportDeclaration, { source: { value: PACKAGE } })
49
+ .forEach((path) => {
50
+ (path.node.specifiers || []).forEach((spec) => {
51
+ if (spec.type === "ImportSpecifier") {
52
+ const imported = spec.imported.name || spec.imported.value;
53
+ named.set(spec.local ? spec.local.name : imported, imported);
54
+ } else if (spec.type === "ImportNamespaceSpecifier") {
55
+ namespaces.add(spec.local.name);
56
+ }
57
+ // A default import has no named component to match; skip it.
58
+ });
59
+ });
60
+ return { named, namespaces };
61
+ }
62
+
63
+ /** Resolve a JSX element name to "Input", "Tabs.Tab", … or null. */
64
+ function componentName(nameNode, imports) {
65
+ if (nameNode.type === "JSXIdentifier") {
66
+ return imports.named.get(nameNode.name) || null;
67
+ }
68
+ if (nameNode.type !== "JSXMemberExpression") return null;
69
+ const parts = [];
70
+ let node = nameNode;
71
+ while (node.type === "JSXMemberExpression") {
72
+ parts.unshift(node.property.name);
73
+ node = node.object;
74
+ }
75
+ if (node.type !== "JSXIdentifier") return null;
76
+ if (imports.namespaces.has(node.name)) {
77
+ // UI.Input / UI.Tabs.Tab
78
+ return parts.join(".") || null;
79
+ }
80
+ const head = imports.named.get(node.name);
81
+ return head ? [head, ...parts].join(".") : null;
82
+ }
83
+
84
+ function attrName(attr) {
85
+ return attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier"
86
+ ? attr.name.name
87
+ : null;
88
+ }
89
+
90
+ /** The string value of a literal attribute (`x="a"`, `x={'a'}`), else undefined. */
91
+ function literalValue(attr) {
92
+ const v = attr.value;
93
+ if (!v) return undefined;
94
+ if (v.type === "StringLiteral" || v.type === "Literal") {
95
+ return typeof v.value === "string" ? v.value : undefined;
96
+ }
97
+ if (v.type === "JSXExpressionContainer") {
98
+ const e = v.expression;
99
+ if ((e.type === "StringLiteral" || e.type === "Literal") && typeof e.value === "string") return e.value;
100
+ if (e.type === "TemplateLiteral" && e.expressions.length === 0) return e.quasis[0].value.cooked;
101
+ }
102
+ return undefined;
103
+ }
104
+
105
+ module.exports = function transformer(file, api) {
106
+ const j = api.jscodeshift;
107
+ const root = j(file.source);
108
+ const imports = collectImports(j, root);
109
+ if (imports.named.size === 0 && imports.namespaces.size === 0) return undefined;
110
+
111
+ let changed = false;
112
+ const report = (node, msg) => {
113
+ // jscodeshift prefixes each report with the file path.
114
+ const text = node.loc ? `line ${node.loc.start.line}: ${msg}` : msg;
115
+ if (typeof api.report === "function") api.report(text);
116
+ };
117
+
118
+ root.find(j.JSXOpeningElement).forEach((path) => {
119
+ const el = path.node;
120
+ const name = componentName(el.name, imports);
121
+ if (!name) return;
122
+ const attrs = el.attributes;
123
+ const find = (n) => attrs.find((a) => attrName(a) === n);
124
+ const remove = (attr) => {
125
+ attrs.splice(attrs.indexOf(attr), 1);
126
+ changed = true;
127
+ };
128
+ const rename = (attr, to) => {
129
+ attr.name = j.jsxIdentifier(to);
130
+ changed = true;
131
+ };
132
+
133
+ // --- removals --------------------------------------------------------
134
+ const removedProps = REMOVED[name] || [];
135
+ let hadColourProp = false;
136
+ removedProps.forEach((prop) => {
137
+ const attr = find(prop);
138
+ if (attr) {
139
+ remove(attr);
140
+ hadColourProp = true;
141
+ }
142
+ });
143
+ if (name === "ProgressBar" && hadColourProp) {
144
+ report(el, "ProgressBar: removed HEX colour props; set `variant` (primary|secondary|success|warning|destructive) if you need a non-primary tone.");
145
+ }
146
+
147
+ // --- renames ---------------------------------------------------------
148
+ const renames = RENAMED[name];
149
+ if (renames) {
150
+ Object.keys(renames).forEach((from) => {
151
+ const attr = find(from);
152
+ if (!attr) return;
153
+ if (find(renames[from])) {
154
+ report(el, `${name}: both \`${from}\` and \`${renames[from]}\` are set; remove \`${from}\` by hand.`);
155
+ return;
156
+ }
157
+ rename(attr, renames[from]);
158
+ });
159
+ }
160
+
161
+ // --- Input icon / iconPosition / clear ---------------------------------
162
+ if (name === "Input") {
163
+ const icon = find("icon");
164
+ const position = find("iconPosition");
165
+ const posValue = position ? literalValue(position) : "left";
166
+ if (position && posValue === undefined) {
167
+ report(el, "Input: `iconPosition` is not a literal; replace `icon` + `iconPosition` with `leftIcon` or `rightIcon` by hand.");
168
+ } else if (icon) {
169
+ const target = posValue === "right" ? "rightIcon" : "leftIcon";
170
+ if (find(target)) {
171
+ report(el, `Input: \`${target}\` is already set; merge \`icon\` by hand.`);
172
+ } else {
173
+ rename(icon, target);
174
+ if (position) remove(position);
175
+ }
176
+ } else if (position) {
177
+ remove(position); // iconPosition without icon never rendered anything
178
+ }
179
+
180
+ if (find("clearable") && find("value") && !find("onClear")) {
181
+ report(el, "Input: controlled + clearable without `onClear`. The clear button no longer calls onChange; add onClear={() => setValue('')}.");
182
+ }
183
+ }
184
+
185
+ // --- raw colour presets ---------------------------------------------
186
+ if (name === "Checkbox" || name === "Switch") {
187
+ const color = find("color");
188
+ const value = color && literalValue(color);
189
+ if (value && RAW_PRESETS.has(value)) {
190
+ report(el, `${name}: color="${value}" is no longer a preset (it now paints the CSS colour inline). Use a token: primary, secondary, success, error or warning.`);
191
+ }
192
+ }
193
+ });
194
+
195
+ return changed ? root.toSource({ quote: "double" }) : undefined;
196
+ };
197
+
198
+ module.exports.parser = "tsx";