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