@dreamtree-org/twreact-ui 1.3.11 → 2.0.1

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
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
@@ -737,19 +752,31 @@ Interaction primitives that products used to compose by hand. All are `forwardRe
737
752
  | Component | Group | Use for |
738
753
  |---|---|---|
739
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 |
740
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"` |
741
757
  | [`Textarea`](doc/Textarea.md) | Core | Multi-line input mirroring `Input`: `label`, `error`, `helperText`, `required`, `showCount`/`maxLength`, `autoResize`/`maxRows` |
742
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*` |
743
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` |
744
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 |
745
767
 
746
768
  ```jsx
747
- 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';
748
770
 
749
771
  <Drawer isOpen={open} onClose={() => setOpen(false)} title="Edit task" footer={<Button onClick={save}>Save</Button>}>
750
772
  <Textarea label="Description" autoResize showCount maxLength={500} />
751
773
  </Drawer>
752
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
+
753
780
  <Popover trigger={<Button variant="outline">Filters</Button>} title="Filters">
754
781
  <Slider label="Budget" defaultValue={[2000, 8000]} min={0} max={10000} step={500} showValue />
755
782
  </Popover>
@@ -860,6 +887,9 @@ fetchApiWith('updateUser', [7, { name: 'Ada' }]);
860
887
 
861
888
  See [doc/useApi.md](doc/useApi.md).
862
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
+
863
893
  ## Theming
864
894
 
865
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.
@@ -904,7 +934,7 @@ The palettes are `primary`, `secondary`, `success`, `warning`, `error`, `info` a
904
934
 
905
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`).
906
936
 
907
- **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).
908
938
 
909
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.
910
940
 
@@ -923,6 +953,10 @@ The stylesheet ships a phone baseline: no tap highlight, no iOS zoom when a fiel
923
953
  - **FileUpload:** shows "Choose file" / "Take photo" buttons on touch (`capture`).
924
954
  - **Charts:** take `aspectRatio` and pin the tooltip on tap.
925
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
+
926
960
  ## Dark Mode
927
961
 
928
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`, `AvatarGroup` (overlapping stack + `max` "+N"), `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
@@ -178,24 +189,36 @@ Components use a **shared variant/size/focus vocabulary**:
178
189
  (the color token swaps for destructive / etc.).
179
190
  - **Dark mode:** Tailwind `darkMode: 'class'`. Every color choice has a `dark:` counterpart.
180
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
+
181
200
  When suggesting a component or new variant value, use this exact vocabulary.
182
201
 
183
202
  ## Page blocks and design tokens
184
203
 
185
204
  **Blocks** are page-level patterns extracted from the products, so screens stop re-inventing them:
186
205
 
187
- - `AppShell` — sidebar + top bar + mobile drawer; pass `nav` items and `linkAs={Link}` in Next.js.
188
- - `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.
189
- - `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.
190
210
  - `StatCard` — KPI tile (`label`, `value`, `icon`, `tone`, `change`).
191
211
  - `StatusPill` — map domain states to `{ label, tone }` once; unknown states are humanized, never raw.
192
212
  - `EmptyState`, `ErrorState`, `LoadingState` — every data card needs all three.
193
- - `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.
194
214
 
195
215
  **Interaction primitives** — reach for these before hand-rolling the pattern:
196
216
 
197
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`.
198
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.
199
222
  - `Button` as a link — `<Button href="tel:…">` or `<Button as={Link} to="/x">`; never copy Button's class string onto an `<a>`.
200
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.
201
224
  - `Badge` — `dot` (live/status dot), `icon` (leading icon slot), `max={99}` with a numeric child renders `99+`.
@@ -203,10 +226,14 @@ When suggesting a component or new variant value, use this exact vocabulary.
203
226
  - `Select size="lg"` — 44px trigger for phone forms (`md` already grows to 44px on touch screens).
204
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.
205
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.
206
231
  - `Popover` — click-triggered panel with controls (`trigger={<Button>…</Button>}`, children or `({ close }) => …`); `Tooltip` is for read-only hover text only.
207
232
  - `Textarea` — same API as `Input` plus `autoResize`/`maxRows`; never a raw `<textarea>`.
208
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).
209
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')`.
210
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.
211
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.
212
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.
@@ -283,6 +310,9 @@ with a `group` key (`{ value, label, group: 'Code' }`) — no need to pre-group.
283
310
 
284
311
  **Navbar** takes a `center` slot (search field / primary action) and shows
285
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`.
286
316
 
287
317
  **Tooltip** around a component (e.g. `<Button>`) in a row/grid: use
288
318
  `customTrigger={<Button>…</Button>}` so there is one tab stop, not two. A lone
@@ -312,6 +342,12 @@ function SomeButton() {
312
342
  }
313
343
  ```
314
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
+
315
351
  ### Theme
316
352
 
317
353
  ```jsx
@@ -391,7 +427,9 @@ import { cn } from '@dreamtree-org/twreact-ui';
391
427
  matching primitive.
392
428
  2. **Use the shared vocabulary.** `variant="primary"`, `size="md"`,
393
429
  `leftIcon={...}`, `fullWidth`, `clearable` — these are exact names.
394
- 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`).
395
433
  3. **Wrap in `<ThemeProvider>`.** Examples that use `useTheme`, or rely
396
434
  on dark-mode classes, MUST show the provider.
397
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";