@dreamtree-org/twreact-ui 1.3.11 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -6
- package/ai-skills/dreamtree-ui.md +47 -9
- package/codemods/v2.cjs +198 -0
- package/dist/ai-schema.json +609 -33
- package/dist/components/blocks/AppShell.d.ts.map +1 -1
- package/dist/components/blocks/BottomActionBar.d.ts +6 -0
- package/dist/components/blocks/BottomActionBar.d.ts.map +1 -0
- package/dist/components/blocks/ConfirmDialog.d.ts.map +1 -1
- package/dist/components/blocks/PageHeader.d.ts.map +1 -1
- package/dist/components/core/Checkbox.d.ts.map +1 -1
- package/dist/components/core/DatePicker.d.ts.map +1 -1
- package/dist/components/core/DateRangePicker.d.ts.map +1 -1
- package/dist/components/core/Form.d.ts.map +1 -1
- package/dist/components/core/Input.d.ts.map +1 -1
- package/dist/components/core/Popover.d.ts.map +1 -1
- package/dist/components/core/ProgressBar.d.ts.map +1 -1
- package/dist/components/core/Rate.d.ts.map +1 -1
- package/dist/components/core/SearchField.d.ts +9 -0
- package/dist/components/core/SearchField.d.ts.map +1 -0
- package/dist/components/core/SegmentedControl.d.ts +8 -0
- package/dist/components/core/SegmentedControl.d.ts.map +1 -0
- package/dist/components/core/Select.d.ts.map +1 -1
- package/dist/components/core/SwipeableListItem.d.ts +4 -0
- package/dist/components/core/SwipeableListItem.d.ts.map +1 -0
- package/dist/components/core/SwipeableListItemGesture.d.ts +58 -0
- package/dist/components/core/SwipeableListItemGesture.d.ts.map +1 -0
- package/dist/components/core/Switch.d.ts.map +1 -1
- package/dist/components/core/Table/ActionMenu.d.ts +10 -3
- package/dist/components/core/Table/ActionMenu.d.ts.map +1 -1
- package/dist/components/core/Table/MobileCard.d.ts +10 -1
- package/dist/components/core/Table/MobileCard.d.ts.map +1 -1
- package/dist/components/core/Table/MobileFilters.d.ts +8 -2
- package/dist/components/core/Table/MobileFilters.d.ts.map +1 -1
- package/dist/components/core/Table/Table.d.ts.map +1 -1
- package/dist/components/core/Table/TableRow.d.ts +2 -1
- package/dist/components/core/Table/TableRow.d.ts.map +1 -1
- package/dist/components/core/Tabs.d.ts +3 -3
- package/dist/components/core/Tabs.d.ts.map +1 -1
- package/dist/components/core/ThreeDotPopover.d.ts +10 -22
- package/dist/components/core/ThreeDotPopover.d.ts.map +1 -1
- package/dist/components/core/Tooltip.d.ts +2 -1
- package/dist/components/core/Tooltip.d.ts.map +1 -1
- package/dist/components/core/internal/Calendar.d.ts +77 -0
- package/dist/components/core/internal/Calendar.d.ts.map +1 -0
- package/dist/components/core/menu/ActionMenuSurface.d.ts +42 -0
- package/dist/components/core/menu/ActionMenuSurface.d.ts.map +1 -0
- package/dist/components/core/menu/ActionSheetMenu.d.ts +17 -0
- package/dist/components/core/menu/ActionSheetMenu.d.ts.map +1 -0
- package/dist/components/core/menu/MenuPopover.d.ts +21 -0
- package/dist/components/core/menu/MenuPopover.d.ts.map +1 -0
- package/dist/components/core/menu/menuItems.d.ts +21 -0
- package/dist/components/core/menu/menuItems.d.ts.map +1 -0
- package/dist/components/core/menu/menuKeyboard.d.ts +16 -0
- package/dist/components/core/menu/menuKeyboard.d.ts.map +1 -0
- package/dist/components/feedback/ActionSheet.d.ts +4 -0
- package/dist/components/feedback/ActionSheet.d.ts.map +1 -0
- package/dist/components/feedback/Alert.d.ts.map +1 -1
- package/dist/components/feedback/Dialog.d.ts.map +1 -1
- package/dist/components/feedback/Drawer.d.ts.map +1 -1
- package/dist/components/feedback/Sheet.d.ts +4 -0
- package/dist/components/feedback/Sheet.d.ts.map +1 -0
- package/dist/components/feedback/Toast.d.ts +4 -3
- package/dist/components/feedback/Toast.d.ts.map +1 -1
- package/dist/components/feedback/ToastItem.d.ts +47 -0
- package/dist/components/feedback/ToastItem.d.ts.map +1 -0
- package/dist/components/feedback/ToastStack.d.ts +20 -0
- package/dist/components/feedback/ToastStack.d.ts.map +1 -0
- package/dist/components/feedback/ToastSwipe.d.ts +22 -0
- package/dist/components/feedback/ToastSwipe.d.ts.map +1 -0
- package/dist/components/feedback/Tour.d.ts.map +1 -1
- package/dist/components/feedback/sheetCore.d.ts +30 -0
- package/dist/components/feedback/sheetCore.d.ts.map +1 -0
- package/dist/components/navigation/Navbar.d.ts.map +1 -1
- package/dist/components/navigation/Sidebar.d.ts +6 -0
- package/dist/components/navigation/Sidebar.d.ts.map +1 -1
- package/dist/components/rich-text/RichTextEditor.d.ts.map +1 -1
- package/dist/components/rich-text/RichTextToolbar.d.ts +6 -2
- package/dist/components/rich-text/RichTextToolbar.d.ts.map +1 -1
- package/dist/components/utility/PullToRefresh.d.ts +5 -0
- package/dist/components/utility/PullToRefresh.d.ts.map +1 -0
- package/dist/esm/components/blocks/AppShell.js +97 -90
- package/dist/esm/components/blocks/BottomActionBar.js +132 -0
- package/dist/esm/components/blocks/ConfirmDialog.js +40 -4
- package/dist/esm/components/blocks/PageHeader.js +16 -3
- package/dist/esm/components/core/Checkbox.js +13 -24
- package/dist/esm/components/core/DatePicker.js +117 -304
- package/dist/esm/components/core/DateRangePicker.js +260 -308
- package/dist/esm/components/core/Form.js +11 -2
- package/dist/esm/components/core/Input.js +13 -18
- package/dist/esm/components/core/Popover.js +59 -48
- package/dist/esm/components/core/ProgressBar.js +36 -64
- package/dist/esm/components/core/Rate.js +5 -10
- package/dist/esm/components/core/SearchField.js +311 -0
- package/dist/esm/components/core/SegmentedControl.js +189 -0
- package/dist/esm/components/core/Select.js +185 -190
- package/dist/esm/components/core/SwipeableListItem.js +286 -0
- package/dist/esm/components/core/SwipeableListItemGesture.js +219 -0
- package/dist/esm/components/core/Switch.js +11 -13
- package/dist/esm/components/core/Table/ActionMenu.js +45 -231
- package/dist/esm/components/core/Table/ColumnMenu.js +1 -1
- package/dist/esm/components/core/Table/MobileCard.js +66 -16
- package/dist/esm/components/core/Table/MobileFilters.js +181 -10
- package/dist/esm/components/core/Table/Table.js +149 -102
- package/dist/esm/components/core/Table/TableRow.js +10 -2
- package/dist/esm/components/core/Tabs.js +7 -7
- package/dist/esm/components/core/ThreeDotPopover.js +93 -99
- package/dist/esm/components/core/Tooltip.js +138 -145
- package/dist/esm/components/core/internal/Calendar.js +381 -0
- package/dist/esm/components/core/menu/ActionMenuSurface.js +110 -0
- package/dist/esm/components/core/menu/ActionSheetMenu.js +140 -0
- package/dist/esm/components/core/menu/MenuPopover.js +177 -0
- package/dist/esm/components/core/menu/menuItems.js +61 -0
- package/dist/esm/components/core/menu/menuKeyboard.js +54 -0
- package/dist/esm/components/feedback/ActionSheet.js +55 -0
- package/dist/esm/components/feedback/Alert.js +63 -53
- package/dist/esm/components/feedback/Dialog.js +141 -53
- package/dist/esm/components/feedback/Drawer.js +130 -132
- package/dist/esm/components/feedback/Sheet.js +125 -0
- package/dist/esm/components/feedback/Toast.js +21 -291
- package/dist/esm/components/feedback/ToastItem.js +348 -0
- package/dist/esm/components/feedback/ToastStack.js +130 -0
- package/dist/esm/components/feedback/ToastSwipe.js +52 -0
- package/dist/esm/components/feedback/Tour.js +107 -30
- package/dist/esm/components/feedback/sheetCore.js +494 -0
- package/dist/esm/components/navigation/Dropdown.js +1 -1
- package/dist/esm/components/navigation/Navbar.js +172 -37
- package/dist/esm/components/navigation/Sidebar.js +263 -111
- package/dist/esm/components/rich-text/RichTextEditor.js +9 -3
- package/dist/esm/components/rich-text/RichTextToolbar.js +29 -9
- package/dist/esm/components/utility/Portal.js +31 -0
- package/dist/esm/components/utility/PullToRefresh.js +229 -0
- package/dist/esm/hooks/useFloatingPosition.js +82 -0
- package/dist/esm/hooks/useHaptics.js +65 -0
- package/dist/esm/hooks/usePresence.js +58 -0
- package/dist/esm/hooks/usePresentation.js +21 -0
- package/dist/esm/hooks/useSheetDrag.js +220 -0
- package/dist/esm/index.js +13 -2
- package/dist/esm/utils/position.js +10 -63
- package/dist/hooks/useFloatingPosition.d.ts +8 -4
- package/dist/hooks/useFloatingPosition.d.ts.map +1 -1
- package/dist/hooks/useHaptics.d.ts +19 -0
- package/dist/hooks/useHaptics.d.ts.map +1 -0
- package/dist/hooks/useSheetDrag.d.ts +31 -0
- package/dist/hooks/useSheetDrag.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +8851 -3198
- package/dist/index.js +8856 -3195
- package/dist/rich-text/index.js +41 -11
- package/dist/types/blocks.d.ts +32 -2
- package/dist/types/blocks.d.ts.map +1 -1
- package/dist/types/index.d.ts +353 -19
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/rich-text.d.ts +5 -0
- package/dist/types/rich-text.d.ts.map +1 -1
- package/dist/utils/position.d.ts +0 -15
- package/dist/utils/position.d.ts.map +1 -1
- package/doc/README.md +15 -0
- package/mcp/catalog.snapshot.json +1398 -464
- package/package.json +7 -2
- package/dist/esm/hooks/useSwipeToClose.js +0 -66
- package/dist/hooks/useSwipeToClose.d.ts +0 -24
- 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
|
-
- `
|
|
249
|
-
- `
|
|
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
|
-
**
|
|
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
|
|
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
|
package/codemods/v2.cjs
ADDED
|
@@ -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";
|