@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
|
@@ -79,22 +79,22 @@
|
|
|
79
79
|
"description": "Show clear button when has value"
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
|
-
"prop": "
|
|
82
|
+
"prop": "leftIcon",
|
|
83
83
|
"type": "`ReactNode`",
|
|
84
84
|
"default": "",
|
|
85
|
-
"description": "
|
|
85
|
+
"description": "Decorative icon before the value (`type=\"search\"` shows its own search icon instead). Replaces `icon` in 2.0.0 ([migration](MIGRATION-2.0.md#input-icon--iconposition--lefticon--righticon))"
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
|
-
"prop": "
|
|
89
|
-
"type": "
|
|
90
|
-
"default": "
|
|
91
|
-
"description": "
|
|
88
|
+
"prop": "rightIcon",
|
|
89
|
+
"type": "`ReactNode`",
|
|
90
|
+
"default": "",
|
|
91
|
+
"description": "Decorative icon after the value, hidden while the password toggle or clear button shows. Replaces `icon` + `iconPosition=\"right\"` in 2.0.0. Both icons can be set together"
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
"prop": "onClear",
|
|
95
95
|
"type": "`function`",
|
|
96
96
|
"default": "",
|
|
97
|
-
"description": "Called when clear is
|
|
97
|
+
"description": "Called when the clear button is pressed. **Controlled** fields must clear their own `value` here: since 2.0.0 the clear no longer sends a synthetic `onChange({ target: { value: '' } })` ([migration](MIGRATION-2.0.md#input-clear-no-longer-fires-onchange)). Uncontrolled fields clear themselves. Focus returns to the input either way."
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
100
|
"prop": "showCount",
|
|
@@ -132,6 +132,24 @@
|
|
|
132
132
|
"default": "",
|
|
133
133
|
"description": "Change handler"
|
|
134
134
|
},
|
|
135
|
+
{
|
|
136
|
+
"prop": "size",
|
|
137
|
+
"type": "`\"sm\"` | `\"md\"` | `\"lg\"`",
|
|
138
|
+
"default": "\"md\"",
|
|
139
|
+
"description": "Field size. `sm` is 32px, `md` is 40px and grows to the 44px touch target on coarse pointers, `lg` is 44px everywhere. A number is passed through as the native `<input size>` attribute."
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"prop": "floatingLabel",
|
|
143
|
+
"type": "`boolean`",
|
|
144
|
+
"default": "false",
|
|
145
|
+
"description": "Render `label` inside the field; it floats above the value once the field is focused or filled. The field is taller (48px at `md`)."
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"prop": "otp",
|
|
149
|
+
"type": "`boolean`",
|
|
150
|
+
"default": "false",
|
|
151
|
+
"description": "One-time code field: sets `autoComplete=\"one-time-code\"` and `inputMode=\"numeric\"` (unless you pass them)."
|
|
152
|
+
},
|
|
135
153
|
{
|
|
136
154
|
"prop": "ref",
|
|
137
155
|
"type": "`Ref<HTMLInputElement>`",
|
|
@@ -143,9 +161,45 @@
|
|
|
143
161
|
"type": "—",
|
|
144
162
|
"default": "",
|
|
145
163
|
"description": "Passed to native `<input>`"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"prop": "type",
|
|
167
|
+
"type": "Adds",
|
|
168
|
+
"default": "",
|
|
169
|
+
"description": ""
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"prop": "tel",
|
|
173
|
+
"type": "`inputMode=\"tel\"` `autoComplete=\"tel\"`",
|
|
174
|
+
"default": "",
|
|
175
|
+
"description": ""
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"prop": "email",
|
|
179
|
+
"type": "`inputMode=\"email\"` `autoComplete=\"email\"`",
|
|
180
|
+
"default": "",
|
|
181
|
+
"description": ""
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"prop": "url",
|
|
185
|
+
"type": "`inputMode=\"url\"` `autoComplete=\"url\"`",
|
|
186
|
+
"default": "",
|
|
187
|
+
"description": ""
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"prop": "search",
|
|
191
|
+
"type": "`inputMode=\"search\"` `enterKeyHint=\"search\"`",
|
|
192
|
+
"default": "",
|
|
193
|
+
"description": ""
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"prop": "otp prop",
|
|
197
|
+
"type": "`inputMode=\"numeric\"` `autoComplete=\"one-time-code\"`",
|
|
198
|
+
"default": "",
|
|
199
|
+
"description": ""
|
|
146
200
|
}
|
|
147
201
|
],
|
|
148
|
-
"examples": "### Basic\n\n```jsx\n<Input label=\"Email\" placeholder=\"you@example.com\" />\n```\n\n### With helper text\n\n```jsx\n<Input label=\"Email\" type=\"email\" helperText=\"Use your work email.\" />\n```\n\n### Helper text and errors\n\n`helperText` renders a muted line below the control and is linked to it via\n`aria-describedby`, so screen readers announce it with the field. **While\n`error` is set, the error message replaces the helper text** (only one line is\nshown, and `aria-describedby` points at the error). This rule is the same on\nevery field-like component: `Input`, `Select`, `DatePicker`, `DateRangePicker`\nand `Form` fields.\n\n### With error\n\n```jsx\n<Input label=\"Email\" error=\"Invalid email address\" />\n```\n\n### Password with toggle\n\n```jsx\n<Input type=\"password\" label=\"Password\" placeholder=\"Enter password\" />\n```\n\n### Clearable and character count\n\n```jsx\n<Input\n label=\"Bio\"\n clearable\n showCount\n maxLength={200}\n placeholder=\"Tell us about yourself\"\n/>\n```\n\n[← Component overview](README.md)"
|
|
202
|
+
"examples": "### Basic\n\n```jsx\n<Input label=\"Email\" placeholder=\"you@example.com\" />\n```\n\n### With helper text\n\n```jsx\n<Input label=\"Email\" type=\"email\" helperText=\"Use your work email.\" />\n```\n\n### Helper text and errors\n\n`helperText` renders a muted line below the control and is linked to it via\n`aria-describedby`, so screen readers announce it with the field. **While\n`error` is set, the error message replaces the helper text** (only one line is\nshown, and `aria-describedby` points at the error). This rule is the same on\nevery field-like component: `Input`, `Select`, `DatePicker`, `DateRangePicker`\nand `Form` fields.\n\n### With error\n\n```jsx\n<Input label=\"Email\" error=\"Invalid email address\" />\n```\n\n### Password with toggle\n\n```jsx\n<Input type=\"password\" label=\"Password\" placeholder=\"Enter password\" />\n```\n\n### Clearable and character count\n\n```jsx\n<Input\n label=\"Bio\"\n clearable\n showCount\n maxLength={200}\n placeholder=\"Tell us about yourself\"\n/>\n\n// Controlled: clear the value yourself in onClear (2.0)\n<Input\n label=\"Search\"\n clearable\n value={q}\n onChange={(e) => setQ(e.target.value)}\n onClear={() => setQ(\"\")}\n/>\n```\n\n### Icons\n\n```jsx\n<Input label=\"Amount\" leftIcon={<IndianRupee className=\"h-4 w-4\" />} />\n<Input label=\"Email\" type=\"email\" rightIcon={<Mail className=\"h-4 w-4\" />} />\n```\n\n### Sizes and floating label\n\n```jsx\n<Input size=\"lg\" label=\"Phone\" type=\"tel\" />\n<Input floatingLabel label=\"Email\" type=\"email\" />\n```\n\n### One-time code\n\n```jsx\n<Input otp label=\"Verification code\" maxLength={6} />\n```\n\n[← Component overview](README.md)"
|
|
149
203
|
},
|
|
150
204
|
{
|
|
151
205
|
"name": "Button",
|
|
@@ -166,7 +220,7 @@
|
|
|
166
220
|
"prop": "size",
|
|
167
221
|
"type": "`string`",
|
|
168
222
|
"default": "\"md\"",
|
|
169
|
-
"description": "`xs` (h-7), `sm` (h-8), `md` (h-10), `lg` (h-12), `xl` (h-14), `2xl` (h-16) — every value of the shared `Size` type"
|
|
223
|
+
"description": "`xs` (h-7), `sm` (h-8), `md` (h-10), `lg` (h-12), `xl` (h-14), `2xl` (h-16) — every value of the shared `Size` type — plus the square icon-only sizes `icon-sm` (32px), `icon` (40px), `icon-lg` (48px); pair an icon size with `aria-label`. `xs`, `sm`, `icon-sm` and `icon` grow to 44px on coarse pointers (touch)"
|
|
170
224
|
},
|
|
171
225
|
{
|
|
172
226
|
"prop": "disabled",
|
|
@@ -178,7 +232,7 @@
|
|
|
178
232
|
"prop": "loading",
|
|
179
233
|
"type": "`boolean`",
|
|
180
234
|
"default": "",
|
|
181
|
-
"description": "
|
|
235
|
+
"description": "Disable and overlay a centred spinner. The label (and icons) stay in the layout at `opacity-0`, so the button keeps its width and its accessible name; sets `aria-busy=\"true\"`"
|
|
182
236
|
},
|
|
183
237
|
{
|
|
184
238
|
"prop": "leftIcon",
|
|
@@ -223,7 +277,7 @@
|
|
|
223
277
|
"description": "Passed to the rendered element (native `<button>` by default; `to`, `target`, `rel` etc. reach a link)"
|
|
224
278
|
}
|
|
225
279
|
],
|
|
226
|
-
"examples": "### Basic\n\n```jsx\n<Button variant=\"primary\">Save</Button>\n<Button variant=\"outline\">Cancel</Button>\n```\n\n### Sizes and loading\n\n```jsx\n<Button size=\"xs\">Extra small</Button>\n<Button size=\"sm\">Small</Button>\n<Button size=\"lg\">Large</Button>\n<Button size=\"xl\">Extra large</Button>\n<Button loading>Saving...</Button>\n```\n\n### With icons\n\n```jsx\nimport { Download } from 'lucide-react';\n\n<Button leftIcon={<Download className=\"h-4 w-4\" />}>Download</Button>\n```\n\n[← Component overview](README.md)"
|
|
280
|
+
"examples": "### Basic\n\n```jsx\n<Button variant=\"primary\">Save</Button>\n<Button variant=\"outline\">Cancel</Button>\n```\n\n### Sizes and loading\n\n```jsx\n<Button size=\"xs\">Extra small</Button>\n<Button size=\"sm\">Small</Button>\n<Button size=\"lg\">Large</Button>\n<Button size=\"xl\">Extra large</Button>\n<Button loading>Saving...</Button>\n```\n\n### With icons\n\n```jsx\nimport { Download } from 'lucide-react';\n\n<Button leftIcon={<Download className=\"h-4 w-4\" />}>Download</Button>\n```\n\n### Icon-only\n\n```jsx\nimport { Plus } from 'lucide-react';\n\n<Button size=\"icon\" variant=\"outline\" aria-label=\"Add\"><Plus className=\"h-4 w-4\" /></Button>\n<Button size=\"icon-sm\" variant=\"ghost\" aria-label=\"Add\"><Plus className=\"h-4 w-4\" /></Button>\n```\n\n[← Component overview](README.md)"
|
|
227
281
|
},
|
|
228
282
|
{
|
|
229
283
|
"name": "Select",
|
|
@@ -242,9 +296,9 @@
|
|
|
242
296
|
},
|
|
243
297
|
{
|
|
244
298
|
"prop": "value",
|
|
245
|
-
"type": "`any` ",
|
|
246
|
-
"default": "
|
|
247
|
-
"description": "
|
|
299
|
+
"type": "`any` | `any[]`",
|
|
300
|
+
"default": "",
|
|
301
|
+
"description": "Selected value(s); array when `multiSelect`"
|
|
248
302
|
},
|
|
249
303
|
{
|
|
250
304
|
"prop": "onChange",
|
|
@@ -378,6 +432,12 @@
|
|
|
378
432
|
"default": "\"md\"",
|
|
379
433
|
"description": "`sm`, `md` or `lg`. Trigger height: `sm` 32px, `md` 38px (44px on coarse pointers, so it meets the touch-target minimum on phones), `lg` 44px everywhere"
|
|
380
434
|
},
|
|
435
|
+
{
|
|
436
|
+
"prop": "presentation",
|
|
437
|
+
"type": "`string`",
|
|
438
|
+
"default": "\"auto\"",
|
|
439
|
+
"description": "`auto`, `popover` or `sheet`. How the open menu presents. `auto`: a bottom sheet on small touch screens (`(pointer: coarse) and (max-width: 639px)`), an anchored popover elsewhere and on the server. `popover` and `sheet` force one."
|
|
440
|
+
},
|
|
381
441
|
{
|
|
382
442
|
"prop": "className",
|
|
383
443
|
"type": "`string`",
|
|
@@ -430,7 +490,7 @@
|
|
|
430
490
|
"prop": "sortable",
|
|
431
491
|
"type": "`boolean`",
|
|
432
492
|
"default": "true",
|
|
433
|
-
"description": "Enable column sort"
|
|
493
|
+
"description": "Enable column sort. Each sortable header is a real `<button>` inside its `<th>` (Tab to it, Enter / Space to sort) and the `<th>` carries `aria-sort` (`none`, `ascending` or `descending`)."
|
|
434
494
|
},
|
|
435
495
|
{
|
|
436
496
|
"prop": "filterable",
|
|
@@ -520,7 +580,13 @@
|
|
|
520
580
|
"prop": "actions",
|
|
521
581
|
"type": "`Array` or `(row, index) => Array`",
|
|
522
582
|
"default": "",
|
|
523
|
-
"description": "Row actions. Entries are `{ name, label, icon, render?, onClick?, hidden?, disabled?, inline? }`; `label`, `icon`, `hidden` and `disabled` each accept a value or `(row, index) => value`. `inline: true` renders the action as an icon button in the actions cell instead of inside the kebab menu; the kebab is hidden when no menu actions remain. Entries are merged onto the built-in Edit/Delete/View set by `name` (so `{ name: 'edit', onClick }` keeps the default label and icon, and `{ name: 'delete', hidden: true }` removes a default); unmatched entries are appended. Set `replaceDefaultActions` to opt out."
|
|
583
|
+
"description": "Row actions. Entries are `{ name, label, icon, render?, onClick?, hidden?, disabled?, inline?, tone? }`; `tone: 'destructive'` (or `destructive: true`) styles a menu action in the error tone; `label`, `icon`, `hidden` and `disabled` each accept a value or `(row, index) => value`. `inline: true` renders the action as an icon button in the actions cell instead of inside the kebab menu; the kebab is hidden when no menu actions remain. Entries are merged onto the built-in Edit/Delete/View set by `name` (so `{ name: 'edit', onClick }` keeps the default label and icon, and `{ name: 'delete', hidden: true }` removes a default); unmatched entries are appended. Set `replaceDefaultActions` to opt out."
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
"prop": "actionMenuPresentation",
|
|
587
|
+
"type": "`string`",
|
|
588
|
+
"default": "\"auto\"",
|
|
589
|
+
"description": "One of `auto`, `popover`, `sheet`. How the row kebab menu opens. `popover`: a floating menu portaled to `document.body` that flips/shifts to stay on screen (never clipped by the table's overflow). `sheet`: an iOS-style action sheet from the bottom with a separate Cancel button. `auto` uses the sheet on small touch screens (`(pointer: coarse) and (max-width: 639px)`) and the popover elsewhere."
|
|
524
590
|
},
|
|
525
591
|
{
|
|
526
592
|
"prop": "replaceDefaultActions",
|
|
@@ -536,15 +602,15 @@
|
|
|
536
602
|
},
|
|
537
603
|
{
|
|
538
604
|
"prop": "cellClass",
|
|
539
|
-
"type": "`string` ",
|
|
540
|
-
"default": "
|
|
541
|
-
"description": "
|
|
605
|
+
"type": "`string` | `function`",
|
|
606
|
+
"default": "",
|
|
607
|
+
"description": "Cell className"
|
|
542
608
|
},
|
|
543
609
|
{
|
|
544
610
|
"prop": "rowClass",
|
|
545
|
-
"type": "`string` ",
|
|
546
|
-
"default": "
|
|
547
|
-
"description": "
|
|
611
|
+
"type": "`string` | `function`",
|
|
612
|
+
"default": "",
|
|
613
|
+
"description": "Row className"
|
|
548
614
|
},
|
|
549
615
|
{
|
|
550
616
|
"prop": "globalSearch",
|
|
@@ -616,7 +682,31 @@
|
|
|
616
682
|
"prop": "responsiveBreakpoint",
|
|
617
683
|
"type": "`number`",
|
|
618
684
|
"default": "768",
|
|
619
|
-
"description": "px breakpoint for mobile cards"
|
|
685
|
+
"description": "px breakpoint for mobile cards: card mode while the viewport is narrower than this. Read with an SSR-safe media query (`not all and (min-width: <n>px)`), so client renders pick the right layout on the first render and follow resizes live."
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
"prop": "defaultMobileView",
|
|
689
|
+
"type": "`boolean`",
|
|
690
|
+
"default": "false",
|
|
691
|
+
"description": "Layout used on the server and while hydrating, before the media query can be read. Set `true` on phone-first pages so server-rendered HTML starts as cards."
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
"prop": "mobileFiltersPresentation",
|
|
695
|
+
"type": "`string`",
|
|
696
|
+
"default": "\"auto\"",
|
|
697
|
+
"description": "Card-mode filters (with `filterable` / `globalSearch`). `sheet`: a \"Filters\" button (with an active-filter count) opening a bottom `Sheet`; edits are a draft until **Apply**, **Clear** empties the draft. `inline`: the collapsible inline panel (the previous behaviour, live filtering). `auto` uses the sheet on small touch screens (`(pointer: coarse) and (max-width: 639px)`) and the inline panel elsewhere."
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
"prop": "swipeActions",
|
|
701
|
+
"type": "`(row, index) => { leading?, trailing? } | null`",
|
|
702
|
+
"default": "",
|
|
703
|
+
"description": "Swipe actions on the mobile cards, built on [`SwipeableListItem`](SwipeableListItem.md). Each side is an array of `{ key, label, icon?, tone?, disabled?, onSelect }`; swipe right for `leading`, left for `trailing`, a full swipe commits the first action of that side. Only one card is open at a time. Every action is also reachable without a gesture through the card's \"Swipe actions\" button. Return `null` (or empty arrays) for rows without swipe actions."
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
"prop": "stickyHeader",
|
|
707
|
+
"type": "`boolean` | `string` | `number`",
|
|
708
|
+
"default": "false",
|
|
709
|
+
"description": "Keep the desktop header visible while the body scrolls. `true` sticks within the table's own scroll container; a CSS length (`\"24rem\"`) or a number (px) also caps that container's height so the rows scroll under the header."
|
|
620
710
|
},
|
|
621
711
|
{
|
|
622
712
|
"prop": "showToolbar",
|
|
@@ -643,7 +733,7 @@
|
|
|
643
733
|
"description": "Cap the fields shown on each mobile card. Applied in column order, after `hideOnMobile` columns are removed."
|
|
644
734
|
}
|
|
645
735
|
],
|
|
646
|
-
"examples": "### Basic table\n\n```jsx\nconst columns = [\n { key: 'name', label: 'Name' },\n { key: 'email', label: 'Email' },\n];\nconst data = [\n { id: 1, name: 'Alice', email: 'alice@example.com' },\n { id: 2, name: 'Bob', email: 'bob@example.com' },\n];\n<Table data={data} columns={columns} />\n```\n\n### With pagination and selection\n\n```jsx\n<Table\n data={data}\n columns={columns}\n pagination\n pageSize={10}\n selectable\n onSelectionChange={(selected) => console.log(selected)}\n/>\n```\n\n### Server-side data\n\n```jsx\n<Table\n data={data}\n columns={columns}\n serverSide\n pagination\n totalRecords={total}\n onFetch={async ({ setData, setLoading, page, limit, sort }) => {\n setLoading(true);\n const res = await fetch(`/api/users?page=${page}&limit=${limit}`);\n const json = await res.json();\n setData(json.rows);\n setLoading(false);\n }}\n/>\n```\n\n### Filtering and page clamping (client-side)\n\nWithout `serverSide`, the table filters the rows itself:\n\n- **Global search** (`globalSearch`) applies on Enter or the search button,\n with or without `filterable`. Columns with `searchable: false` are skipped.\n- **Column filters** (`filterable`) apply as you type. Each one matches only its\n own column's raw value (not a `render` output).\n- All matches are case-insensitive \"contains\" tests, and every active filter\n must match (AND).\n- `onFilter` / `onFilterChange` still receive the whole `filters` object\n (`{ global?, [columnKey]: text }`) on every change.\n\nWhen the rows shrink (filtering, deleted rows, a new `data` prop) and the\ncurrent page no longer exists, the table shows the last page that does and\ncalls `onPageChange` with it. A parent that drives `pageNumber` can simply\nstore that value, and no `clampPage` helper is needed. The clamp waits while\nthere are no rows, so a page restored from the URL survives until the data\narrives.\n\nWith `serverSide`, none of this runs locally: filters go to `onFetch` /\n`onFilter` / `onFilterChange`, the rows shown are exactly the rows you pass,\nand keeping `pageNumber` within `totalRecords` is up to you.\n\n### Compact, read-only table\n\nHide the chrome for a dashboard card, keep id columns out of the search, let\nlong text wrap, and keep phone cards short.\n\n```jsx\nconst columns = [\n { key: 'id', label: 'ID', searchable: false, hideOnMobile: true },\n { key: 'client', label: 'Client' },\n { key: 'status', label: 'Status' },\n { key: 'notes', label: 'Notes', wrap: true },\n { key: 'owner', label: 'Owner' },\n];\n\n<Table\n data={rows}\n columns={columns}\n showToolbar={false}\n showSerial={false}\n withAction={false}\n mobileMaxFields={3}\n/>\n```\n\nOn a phone each card shows Client, Status and Notes. With no serial, selection,\nexpand toggle or actions, the card has no header strip either.\n\n`showToolbar={false}` does not change pagination: with `pagination` on, the pager\nfooter still renders. Hide only the picker with `showColumnPicker={false}`.\n\n### Conditional row details\n\n`hasDetails` accepts a predicate so only rows that actually have something to\nshow get an expand toggle. Rows that don't qualify still render the (empty)\ntoggle cell, so the columns stay aligned.\n\n```jsx\nconst ErrorDetails = ({ row }) => <pre>{row.stackTrace}</pre>;\n\n<Table\n data={jobs}\n columns={columns}\n hasDetails={(row) => row.status === 'failed'}\n DetailsComponent={ErrorDetails}\n/>\n```\n\n### Conditional and inline actions\n\n`hidden` and `disabled` accept a `(row, index)` predicate, `label` and `icon`\naccept a `(row, index)` producer, and `inline: true` promotes an action out of\nthe kebab menu into the row itself. When no menu actions remain for a row, the\nkebab button is not rendered at all.\n\n```jsx\n<Table\n data={jobs}\n columns={columns}\n replaceDefaultActions\n actions={[\n { name: 'view', label: 'View', icon: <Eye size={16} />, inline: true },\n { name: 'edit', label: 'Edit', icon: <Edit size={16} />,\n disabled: (row) => row.locked },\n { name: 'retry', label: (row) => `Retry (${row.tries})`,\n hidden: (row) => row.status !== 'failed' },\n ]}\n onAction={({ action, row }) => handle(action.name, row)}\n/>\n```\n\nFor a large table, wrap a function-valued `actions` in `useCallback` — a fresh\narrow on every render defeats the per-row memoization.\n\n```jsx\nconst actions = useCallback(\n (row) => (row.role === 'admin' ? adminActions : userActions),\n []\n);\n```\n\nNote that because `icon` may be a `(row, index) => node` producer, passing a\nbare component reference (`icon: Edit`) calls it rather than rendering it. Pass\nan element instead: `icon: <Edit size={16} />`.\n\n### Patching the default actions\n\nEntries you supply are merged onto the built-in Edit/Delete/View set by `name`,\nso you can swap one handler without restating the rest. Unmatched names are\nappended after the defaults, and `hidden: true` removes a default outright.\n\n```jsx\n<Table\n data={users}\n columns={columns}\n actions={[\n { name: 'edit', onClick: ({ row }) => edit(row) }, // keeps default label + icon\n { name: 'delete', hidden: (row) => !row.canDelete },\n { name: 'invite', label: 'Invite', icon: <Mail size={16} /> }, // appended\n ]}\n/>\n```\n\nPass `replaceDefaultActions` to skip merging and use only your own set:\n\n```jsx\n<Table\n data={users}\n columns={columns}\n replaceDefaultActions\n actions={[{ name: 'approve', label: 'Approve' }]}\n/>\n```\n\n### Interactive Table Mode\n\nPass `interactive={true}` to enable inline cell editing (double-click a cell or press standard `'e'` key on a focused active cell to enter edit mode), instant record addition (at top or bottom), keyboard navigation (`Tab`/`Shift+Tab` to move between cells, `Escape` to exit edit mode, `Enter` to save modified changes), and constraint validation before saving.\n\n#### Interactive Props & Options\n\n- `interactive`: `boolean` (default: `false`) — Enables interactive mode.\n- `editKeyBinding`: `string | ((e) => boolean)` (default: `\"e\"`) — Standard key binding to enter cell edit mode on active focused cell.\n- `newRowPosition`: `\"bottom\" | \"top\"` (default: `\"bottom\"`) — Position where empty new rows are inserted (at the bottom or top of table dataset).\n- `addRowRef`: `React.MutableRefObject` — External ref populated with `(position?: \"top\" | \"bottom\") => void` to trigger adding a new row from external buttons.\n- `onInitAddRow`: `(addRowFn: (position?: \"top\" | \"bottom\") => void) => void` — Callback receiving the add-row function on component initialization.\n- `renderAddRowButton`: `({ addRow, newRowPosition }) => ReactNode` — Render prop for rendering a custom external add-row button in the table toolbar.\n\n#### Interactive Usage Example\n\n```jsx\nconst columns = [\n { key: 'name', label: 'Name', type: 'text', required: true },\n { key: 'role', label: 'Role', type: 'select', options: ['Admin', 'User'], required: true },\n { key: 'notes', label: 'Notes', type: 'textarea' },\n];\n\nconst addRowRef = useRef();\n\nreturn (\n <div>\n <button onClick={() => addRowRef.current?.(\"top\")}>\n Add Row at Top\n </button>\n\n <Table\n interactive\n editKeyBinding=\"e\"\n newRowPosition=\"top\"\n addRowRef={addRowRef}\n columns={columns}\n data={users}\n onSave={async ({ updatedRows, newRows, allData }) => {\n await api.saveUsers({ updatedRows, newRows });\n }}\n />\n </div>\n);\n```\n\n[← Component overview](README.md)"
|
|
736
|
+
"examples": "### Basic table\n\n```jsx\nconst columns = [\n { key: 'name', label: 'Name' },\n { key: 'email', label: 'Email' },\n];\nconst data = [\n { id: 1, name: 'Alice', email: 'alice@example.com' },\n { id: 2, name: 'Bob', email: 'bob@example.com' },\n];\n<Table data={data} columns={columns} />\n```\n\n### With pagination and selection\n\n```jsx\n<Table\n data={data}\n columns={columns}\n pagination\n pageSize={10}\n selectable\n onSelectionChange={(selected) => console.log(selected)}\n/>\n```\n\n### Server-side data\n\n```jsx\n<Table\n data={data}\n columns={columns}\n serverSide\n pagination\n totalRecords={total}\n onFetch={async ({ setData, setLoading, page, limit, sort }) => {\n setLoading(true);\n const res = await fetch(`/api/users?page=${page}&limit=${limit}`);\n const json = await res.json();\n setData(json.rows);\n setLoading(false);\n }}\n/>\n```\n\n### Filtering and page clamping (client-side)\n\nWithout `serverSide`, the table filters the rows itself:\n\n- **Global search** (`globalSearch`) applies on Enter or the search button,\n with or without `filterable`. Columns with `searchable: false` are skipped.\n- **Column filters** (`filterable`) apply as you type. Each one matches only its\n own column's raw value (not a `render` output).\n- All matches are case-insensitive \"contains\" tests, and every active filter\n must match (AND).\n- `onFilter` / `onFilterChange` still receive the whole `filters` object\n (`{ global?, [columnKey]: text }`) on every change.\n\nWhen the rows shrink (filtering, deleted rows, a new `data` prop) and the\ncurrent page no longer exists, the table shows the last page that does and\ncalls `onPageChange` with it. A parent that drives `pageNumber` can simply\nstore that value, and no `clampPage` helper is needed. The clamp waits while\nthere are no rows, so a page restored from the URL survives until the data\narrives.\n\nWith `serverSide`, none of this runs locally: filters go to `onFetch` /\n`onFilter` / `onFilterChange`, the rows shown are exactly the rows you pass,\nand keeping `pageNumber` within `totalRecords` is up to you.\n\n### Compact, read-only table\n\nHide the chrome for a dashboard card, keep id columns out of the search, let\nlong text wrap, and keep phone cards short.\n\n```jsx\nconst columns = [\n { key: 'id', label: 'ID', searchable: false, hideOnMobile: true },\n { key: 'client', label: 'Client' },\n { key: 'status', label: 'Status' },\n { key: 'notes', label: 'Notes', wrap: true },\n { key: 'owner', label: 'Owner' },\n];\n\n<Table\n data={rows}\n columns={columns}\n showToolbar={false}\n showSerial={false}\n withAction={false}\n mobileMaxFields={3}\n/>\n```\n\nOn a phone each card shows Client, Status and Notes. With no serial, selection,\nexpand toggle or actions, the card has no header strip either.\n\n`showToolbar={false}` does not change pagination: with `pagination` on, the pager\nfooter still renders. Hide only the picker with `showColumnPicker={false}`.\n\n### Conditional row details\n\n`hasDetails` accepts a predicate so only rows that actually have something to\nshow get an expand toggle. Rows that don't qualify still render the (empty)\ntoggle cell, so the columns stay aligned.\n\n```jsx\nconst ErrorDetails = ({ row }) => <pre>{row.stackTrace}</pre>;\n\n<Table\n data={jobs}\n columns={columns}\n hasDetails={(row) => row.status === 'failed'}\n DetailsComponent={ErrorDetails}\n/>\n```\n\n### Mobile cards, filters and swipe actions\n\nBelow `responsiveBreakpoint` rows render as cards. Card controls (expand\ntoggle, selection checkbox, inline actions, kebab) have 44×44 px hit areas.\nWith `filterable` on a phone, the filters open in a bottom sheet with an\nApply / Clear footer; pass `mobileFiltersPresentation=\"inline\"` to keep the\ninline panel.\n\n```jsx\n<Table\n data={messages}\n columns={columns}\n filterable\n defaultMobileView // server-rendered HTML starts as cards\n swipeActions={(row) => ({\n leading: [{ key: 'read', label: 'Read', icon: MailOpen, tone: 'primary', onSelect: () => markRead(row) }],\n trailing: [\n { key: 'delete', label: 'Delete', icon: Trash2, tone: 'destructive', onSelect: () => remove(row) },\n { key: 'flag', label: 'Flag', icon: Flag, tone: 'warning', onSelect: () => flag(row) },\n ],\n })}\n/>\n```\n\n### Sticky header\n\n```jsx\n<Table data={rows} columns={columns} stickyHeader=\"28rem\" />\n```\n\n### Row action menu\n\nThe kebab button (`aria-haspopup=\"menu\"`) opens the row's menu actions. On\ndesktop it is a `role=\"menu\"` popover: focus moves to the first action,\nArrowUp/ArrowDown (wrapping), Home/End and first-letter typeahead move between\nactions, Escape or Tab closes it and returns focus to the kebab, and a press\noutside or a page scroll closes it (pressing the kebab again closes it too). On phones (`actionMenuPresentation=\"auto\"`,\nthe default) the same actions open as an action sheet: 48px rows in a rounded\ncard, destructive actions in red, and a Cancel button below. Pass\n`actionMenuPresentation=\"popover\"` to keep the desktop menu everywhere.\n\n```jsx\n<Table\n data={users}\n columns={columns}\n actions={[{ name: 'delete', tone: 'destructive' }]}\n actionMenuPresentation=\"auto\"\n/>\n```\n\n### Conditional and inline actions\n\n`hidden` and `disabled` accept a `(row, index)` predicate, `label` and `icon`\naccept a `(row, index)` producer, and `inline: true` promotes an action out of\nthe kebab menu into the row itself. When no menu actions remain for a row, the\nkebab button is not rendered at all.\n\n```jsx\n<Table\n data={jobs}\n columns={columns}\n replaceDefaultActions\n actions={[\n { name: 'view', label: 'View', icon: <Eye size={16} />, inline: true },\n { name: 'edit', label: 'Edit', icon: <Edit size={16} />,\n disabled: (row) => row.locked },\n { name: 'retry', label: (row) => `Retry (${row.tries})`,\n hidden: (row) => row.status !== 'failed' },\n ]}\n onAction={({ action, row }) => handle(action.name, row)}\n/>\n```\n\nFor a large table, wrap a function-valued `actions` in `useCallback` — a fresh\narrow on every render defeats the per-row memoization.\n\n```jsx\nconst actions = useCallback(\n (row) => (row.role === 'admin' ? adminActions : userActions),\n []\n);\n```\n\nNote that because `icon` may be a `(row, index) => node` producer, passing a\nbare component reference (`icon: Edit`) calls it rather than rendering it. Pass\nan element instead: `icon: <Edit size={16} />`.\n\n### Patching the default actions\n\nEntries you supply are merged onto the built-in Edit/Delete/View set by `name`,\nso you can swap one handler without restating the rest. Unmatched names are\nappended after the defaults, and `hidden: true` removes a default outright.\n\n```jsx\n<Table\n data={users}\n columns={columns}\n actions={[\n { name: 'edit', onClick: ({ row }) => edit(row) }, // keeps default label + icon\n { name: 'delete', hidden: (row) => !row.canDelete },\n { name: 'invite', label: 'Invite', icon: <Mail size={16} /> }, // appended\n ]}\n/>\n```\n\nPass `replaceDefaultActions` to skip merging and use only your own set:\n\n```jsx\n<Table\n data={users}\n columns={columns}\n replaceDefaultActions\n actions={[{ name: 'approve', label: 'Approve' }]}\n/>\n```\n\n### Interactive Table Mode\n\nPass `interactive={true}` to enable inline cell editing (double-click a cell or press standard `'e'` key on a focused active cell to enter edit mode), instant record addition (at top or bottom), keyboard navigation (`Tab`/`Shift+Tab` to move between cells, `Escape` to exit edit mode, `Enter` to save modified changes), and constraint validation before saving.\n\n#### Interactive Props & Options\n\n- `interactive`: `boolean` (default: `false`) — Enables interactive mode.\n- `editKeyBinding`: `string | ((e) => boolean)` (default: `\"e\"`) — Standard key binding to enter cell edit mode on active focused cell.\n- `newRowPosition`: `\"bottom\" | \"top\"` (default: `\"bottom\"`) — Position where empty new rows are inserted (at the bottom or top of table dataset).\n- `addRowRef`: `React.MutableRefObject` — External ref populated with `(position?: \"top\" | \"bottom\") => void` to trigger adding a new row from external buttons.\n- `onInitAddRow`: `(addRowFn: (position?: \"top\" | \"bottom\") => void) => void` — Callback receiving the add-row function on component initialization.\n- `renderAddRowButton`: `({ addRow, newRowPosition }) => ReactNode` — Render prop for rendering a custom external add-row button in the table toolbar.\n\n#### Interactive Usage Example\n\n```jsx\nconst columns = [\n { key: 'name', label: 'Name', type: 'text', required: true },\n { key: 'role', label: 'Role', type: 'select', options: ['Admin', 'User'], required: true },\n { key: 'notes', label: 'Notes', type: 'textarea' },\n];\n\nconst addRowRef = useRef();\n\nreturn (\n <div>\n <button onClick={() => addRowRef.current?.(\"top\")}>\n Add Row at Top\n </button>\n\n <Table\n interactive\n editKeyBinding=\"e\"\n newRowPosition=\"top\"\n addRowRef={addRowRef}\n columns={columns}\n data={users}\n onSave={async ({ updatedRows, newRows, allData }) => {\n await api.saveUsers({ updatedRows, newRows });\n }}\n />\n </div>\n);\n```\n\n[← Component overview](README.md)"
|
|
647
737
|
},
|
|
648
738
|
{
|
|
649
739
|
"name": "Form",
|
|
@@ -662,9 +752,9 @@
|
|
|
662
752
|
},
|
|
663
753
|
{
|
|
664
754
|
"prop": "onSubmit",
|
|
665
|
-
"type": "`(data: object) => void ",
|
|
666
|
-
"default": "
|
|
667
|
-
"description": "
|
|
755
|
+
"type": "`(data: object) => void | Promise<void>`",
|
|
756
|
+
"default": "",
|
|
757
|
+
"description": "Submit handler; receives form data. If it returns a promise, the form awaits it: the submit button shows its loading state and stays disabled until the promise settles."
|
|
668
758
|
},
|
|
669
759
|
{
|
|
670
760
|
"prop": "defaultValues",
|
|
@@ -782,9 +872,9 @@
|
|
|
782
872
|
},
|
|
783
873
|
{
|
|
784
874
|
"prop": "chevronPosition",
|
|
785
|
-
"type": "`\"left\"` ",
|
|
875
|
+
"type": "`\"left\"` | `\"right\"`",
|
|
786
876
|
"default": "\"right\"",
|
|
787
|
-
"description": "
|
|
877
|
+
"description": "Position of chevron icon."
|
|
788
878
|
},
|
|
789
879
|
{
|
|
790
880
|
"prop": "chevronIcon",
|
|
@@ -800,15 +890,15 @@
|
|
|
800
890
|
},
|
|
801
891
|
{
|
|
802
892
|
"prop": "rounded",
|
|
803
|
-
"type": "`\"none\"` ",
|
|
804
|
-
"default": "\"
|
|
805
|
-
"description": "
|
|
893
|
+
"type": "`\"none\"` | `\"sm\"` | `\"md\"` | `\"lg\"` | `\"xl\"`",
|
|
894
|
+
"default": "\"md\"",
|
|
895
|
+
"description": "Border radius of wrapper."
|
|
806
896
|
},
|
|
807
897
|
{
|
|
808
898
|
"prop": "size",
|
|
809
|
-
"type": "`\"sm\"` ",
|
|
810
|
-
"default": "\"md\"
|
|
811
|
-
"description": "
|
|
899
|
+
"type": "`\"sm\"` | `\"md\"` | `\"lg\"`",
|
|
900
|
+
"default": "\"md\"",
|
|
901
|
+
"description": "Header and content padding/text size."
|
|
812
902
|
},
|
|
813
903
|
{
|
|
814
904
|
"prop": "transitionDuration",
|
|
@@ -898,19 +988,13 @@
|
|
|
898
988
|
"prop": "indeterminate",
|
|
899
989
|
"type": "`boolean`",
|
|
900
990
|
"default": "false",
|
|
901
|
-
"description": "
|
|
902
|
-
},
|
|
903
|
-
{
|
|
904
|
-
"prop": "borderColor",
|
|
905
|
-
"type": "`string`",
|
|
906
|
-
"default": "",
|
|
907
|
-
"description": "**Deprecated.** Inline box border colour. Omit it to use the `border-gray-300` token class."
|
|
991
|
+
"description": "Mixed state: fills the box with a dash and sets `aria-checked=\"mixed\"`"
|
|
908
992
|
},
|
|
909
993
|
{
|
|
910
994
|
"prop": "color",
|
|
911
995
|
"type": "`string`",
|
|
912
996
|
"default": "\"primary\"",
|
|
913
|
-
"description": "
|
|
997
|
+
"description": "Token preset (`primary`, `secondary`, `success`, `error`, `warning`; aliases `green`, `danger`, `red`) or a custom CSS colour string. The raw-palette presets `blue`, `indigo`, `purple` and the `borderColor` prop were **removed in 2.0.0** ([migration](MIGRATION-2.0.md#checkbox-and-switch-raw-colour-presets)); the box border is always the `border-gray-300` token."
|
|
914
998
|
},
|
|
915
999
|
{
|
|
916
1000
|
"prop": "iconClass",
|
|
@@ -929,6 +1013,12 @@
|
|
|
929
1013
|
"type": "`object`",
|
|
930
1014
|
"default": "",
|
|
931
1015
|
"description": "Extra props spread onto the outer wrapper element"
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
"prop": "haptics",
|
|
1019
|
+
"type": "`boolean`",
|
|
1020
|
+
"default": "false",
|
|
1021
|
+
"description": "Opt-in: a `selection` vibration (10ms) on every change. No-op without `navigator.vibrate` (iOS Safari) and under reduced motion. See [useHaptics](useHaptics.md)"
|
|
932
1022
|
}
|
|
933
1023
|
],
|
|
934
1024
|
"examples": "```jsx\n<Checkbox label=\"Accept terms\" checked={checked} onChange={e => setChecked(e.target.checked)} />\n<Checkbox label=\"Remember me\" defaultChecked />\n```\n\n[← Component overview](README.md)"
|
|
@@ -970,13 +1060,13 @@
|
|
|
970
1060
|
"prop": "showAlpha",
|
|
971
1061
|
"type": "`boolean`",
|
|
972
1062
|
"default": "false",
|
|
973
|
-
"description": "Show alpha slider. When off (or when the alpha channel is fully opaque), the displayed hex is the 6-char `#rrggbb` form; the 8-char `#rrggbbff` form is shown only when `showAlpha` is on **and** alpha < 1."
|
|
1063
|
+
"description": "Show the alpha slider (the library `Slider`, labelled \"Alpha\", value shown as a percentage). When off (or when the alpha channel is fully opaque), the displayed hex is the 6-char `#rrggbb` form; the 8-char `#rrggbbff` form is shown only when `showAlpha` is on **and** alpha < 1."
|
|
974
1064
|
},
|
|
975
1065
|
{
|
|
976
1066
|
"prop": "size",
|
|
977
|
-
"type": "`\"sm\"` ",
|
|
978
|
-
"default": "\"md\"
|
|
979
|
-
"description": "
|
|
1067
|
+
"type": "`\"sm\"` | `\"md\"` | `\"lg\"`",
|
|
1068
|
+
"default": "\"md\"",
|
|
1069
|
+
"description": "Picker size. Other `Size` values (`xs`, `xl`, `2xl`) fall back to `md` (they used to crash)."
|
|
980
1070
|
},
|
|
981
1071
|
{
|
|
982
1072
|
"prop": "disabled",
|
|
@@ -1012,19 +1102,19 @@
|
|
|
1012
1102
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
1013
1103
|
"peerDependencies": [],
|
|
1014
1104
|
"hasDoc": true,
|
|
1015
|
-
"description": "Date range picker: user selects start then end. Value is `{ start: Date, end: Date }`.
|
|
1105
|
+
"description": "Date range picker: user selects start then end. Value is `{ start: Date, end: Date }`. The popover stays open on outside click / Escape until both are selected (or the user clears); the trigger always toggles it, and closing mid-pick drops the uncommitted start. Supports min/max, clear, preset chips, optional portal, and a phone-native bottom sheet.",
|
|
1016
1106
|
"props": [
|
|
1017
1107
|
{
|
|
1018
1108
|
"prop": "value",
|
|
1019
|
-
"type": "`{ start: Date",
|
|
1020
|
-
"default": "
|
|
1021
|
-
"description": "
|
|
1109
|
+
"type": "`{ start: Date|string, end: Date|string }`",
|
|
1110
|
+
"default": "",
|
|
1111
|
+
"description": "Selected range."
|
|
1022
1112
|
},
|
|
1023
1113
|
{
|
|
1024
1114
|
"prop": "onChange",
|
|
1025
|
-
"type": "`(range: { start, end } ",
|
|
1026
|
-
"default": "
|
|
1027
|
-
"description": "
|
|
1115
|
+
"type": "`(range: { start, end } | null) => void`",
|
|
1116
|
+
"default": "",
|
|
1117
|
+
"description": "Called when range changes."
|
|
1028
1118
|
},
|
|
1029
1119
|
{
|
|
1030
1120
|
"prop": "placeholder",
|
|
@@ -1082,9 +1172,9 @@
|
|
|
1082
1172
|
},
|
|
1083
1173
|
{
|
|
1084
1174
|
"prop": "weekStartsOn",
|
|
1085
|
-
"type": "`0` ",
|
|
1086
|
-
"default": "
|
|
1087
|
-
"description": "
|
|
1175
|
+
"type": "`0` | `1`",
|
|
1176
|
+
"default": "0",
|
|
1177
|
+
"description": "0 = Sunday, 1 = Monday."
|
|
1088
1178
|
},
|
|
1089
1179
|
{
|
|
1090
1180
|
"prop": "portal",
|
|
@@ -1114,10 +1204,22 @@
|
|
|
1114
1204
|
"prop": "closeOnSelect",
|
|
1115
1205
|
"type": "`boolean`",
|
|
1116
1206
|
"default": "true",
|
|
1117
|
-
"description": "Close when both dates selected."
|
|
1207
|
+
"description": "Close when both dates selected (popover). The sheet always closes on \"Apply\"."
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
"prop": "presentation",
|
|
1211
|
+
"type": "`'auto'` | `'popover'` | `'sheet'`",
|
|
1212
|
+
"default": "'auto'",
|
|
1213
|
+
"description": "How the calendar opens. `auto` uses a bottom `Sheet` on coarse-pointer screens under 640px and an anchored popover elsewhere (popover on the server). Pass `'popover'` to keep the desktop popup everywhere."
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
"prop": "presets",
|
|
1217
|
+
"type": "`{ label: string, range: [Date|string, Date|string] }[]`",
|
|
1218
|
+
"default": "",
|
|
1219
|
+
"description": "Quick-pick chips above the calendar. In the popover a chip commits its range (`onChange`) at once; in the sheet it fills the draft for \"Apply\". The chip matching the current selection has `aria-pressed=\"true\"`."
|
|
1118
1220
|
}
|
|
1119
1221
|
],
|
|
1120
|
-
"examples": "```jsx\nconst [range, setRange] = useState({ start: null, end: null });\n<DateRangePicker value={range} onChange={setRange} label=\"Period\" />\n```\n\n[← Component overview](README.md)"
|
|
1222
|
+
"examples": "```jsx\nconst [range, setRange] = useState({ start: null, end: null });\n<DateRangePicker value={range} onChange={setRange} label=\"Period\" />\n```\n\nWith presets:\n\n```jsx\nconst day = 24 * 60 * 60 * 1000;\n<DateRangePicker\n value={range}\n onChange={setRange}\n label=\"Report period\"\n presets={[\n { label: 'Last 7 days', range: [new Date(Date.now() - 6 * day), new Date()] },\n { label: 'Last 30 days', range: [new Date(Date.now() - 29 * day), new Date()] },\n ]}\n/>\n```\n\n[← Component overview](README.md)"
|
|
1121
1223
|
},
|
|
1122
1224
|
{
|
|
1123
1225
|
"name": "Loader",
|
|
@@ -1130,15 +1232,15 @@
|
|
|
1130
1232
|
"props": [
|
|
1131
1233
|
{
|
|
1132
1234
|
"prop": "size",
|
|
1133
|
-
"type": "`\"sm\"` ",
|
|
1134
|
-
"default": "\"md\"
|
|
1135
|
-
"description": "
|
|
1235
|
+
"type": "`\"sm\"` | `\"md\"` | `\"lg\"` | `number`",
|
|
1236
|
+
"default": "\"md\"",
|
|
1237
|
+
"description": "Size: sm=14px, md=20px, lg=36px, or numeric px."
|
|
1136
1238
|
},
|
|
1137
1239
|
{
|
|
1138
1240
|
"prop": "speed",
|
|
1139
1241
|
"type": "`number`",
|
|
1140
1242
|
"default": "0.9",
|
|
1141
|
-
"description": "Animation duration in seconds."
|
|
1243
|
+
"description": "Animation duration in seconds. Applied through the `--dt-loader-speed` CSS custom property on the wrapper."
|
|
1142
1244
|
},
|
|
1143
1245
|
{
|
|
1144
1246
|
"prop": "thickness",
|
|
@@ -1155,14 +1257,14 @@
|
|
|
1155
1257
|
{
|
|
1156
1258
|
"prop": "secondaryColor",
|
|
1157
1259
|
"type": "`string`",
|
|
1158
|
-
"default": "
|
|
1159
|
-
"description": "Track/background color."
|
|
1260
|
+
"default": "",
|
|
1261
|
+
"description": "Track/background color. When omitted, a dark-safe track is used: 8% black in light mode (as before) and 15% white in dark mode."
|
|
1160
1262
|
},
|
|
1161
1263
|
{
|
|
1162
1264
|
"prop": "variant",
|
|
1163
|
-
"type": "`\"spinner\"` ",
|
|
1164
|
-
"default": "\"
|
|
1165
|
-
"description": "
|
|
1265
|
+
"type": "`\"spinner\"` | `\"dots\"` | `\"bar\"`",
|
|
1266
|
+
"default": "\"spinner\"",
|
|
1267
|
+
"description": "Visual style."
|
|
1166
1268
|
},
|
|
1167
1269
|
{
|
|
1168
1270
|
"prop": "className",
|
|
@@ -1238,9 +1340,9 @@
|
|
|
1238
1340
|
},
|
|
1239
1341
|
{
|
|
1240
1342
|
"prop": "value",
|
|
1241
|
-
"type": "`{ min, max }` ",
|
|
1343
|
+
"type": "`{ min, max }` | `null`",
|
|
1242
1344
|
"default": "null",
|
|
1243
|
-
"description": "
|
|
1345
|
+
"description": "Controlled value."
|
|
1244
1346
|
},
|
|
1245
1347
|
{
|
|
1246
1348
|
"prop": "defaultValue",
|
|
@@ -1298,9 +1400,9 @@
|
|
|
1298
1400
|
},
|
|
1299
1401
|
{
|
|
1300
1402
|
"prop": "size",
|
|
1301
|
-
"type": "`\"sm\"` ",
|
|
1302
|
-
"default": "\"md\"
|
|
1303
|
-
"description": "
|
|
1403
|
+
"type": "`\"sm\"` | `\"md\"` | `\"lg\"`",
|
|
1404
|
+
"default": "\"md\"",
|
|
1405
|
+
"description": "Thumb and spacing size."
|
|
1304
1406
|
},
|
|
1305
1407
|
{
|
|
1306
1408
|
"prop": "colorClass",
|
|
@@ -1324,7 +1426,7 @@
|
|
|
1324
1426
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
1325
1427
|
"peerDependencies": [],
|
|
1326
1428
|
"hasDoc": true,
|
|
1327
|
-
"description": "Progress bar showing a value between 0 and max. Uses the brand's `primary` tokens by default (`bg-primary-600` fill, `bg-primary-100` track), so it follows `--dt-*` theming.
|
|
1429
|
+
"description": "Progress bar showing a value between 0 and max. Uses the brand's `primary` tokens by default (`bg-primary-600` fill, `bg-primary-100` track), so it follows `--dt-*` theming. Pick another token tone with `variant`. Also supports an optional label, value text inside the bar, striped and animated styles.",
|
|
1328
1430
|
"props": [
|
|
1329
1431
|
{
|
|
1330
1432
|
"prop": "value",
|
|
@@ -1340,9 +1442,9 @@
|
|
|
1340
1442
|
},
|
|
1341
1443
|
{
|
|
1342
1444
|
"prop": "size",
|
|
1343
|
-
"type": "`\"xs\"` ",
|
|
1344
|
-
"default": "\"
|
|
1345
|
-
"description": "
|
|
1445
|
+
"type": "`\"xs\"` | `\"sm\"` | `\"md\"` | `\"lg\"` | `number`",
|
|
1446
|
+
"default": "\"md\"",
|
|
1447
|
+
"description": "Height: xs/sm/md/lg or px."
|
|
1346
1448
|
},
|
|
1347
1449
|
{
|
|
1348
1450
|
"prop": "showValue",
|
|
@@ -1357,22 +1459,10 @@
|
|
|
1357
1459
|
"description": "Rounded corners."
|
|
1358
1460
|
},
|
|
1359
1461
|
{
|
|
1360
|
-
"prop": "
|
|
1361
|
-
"type": "`
|
|
1362
|
-
"default": "",
|
|
1363
|
-
"description": "
|
|
1364
|
-
},
|
|
1365
|
-
{
|
|
1366
|
-
"prop": "bgColor",
|
|
1367
|
-
"type": "`string` (HEX)",
|
|
1368
|
-
"default": "",
|
|
1369
|
-
"description": "**Deprecated.** Inline track colour (HEX only). Omit it to use `bg-primary-100`."
|
|
1370
|
-
},
|
|
1371
|
-
{
|
|
1372
|
-
"prop": "valueColor",
|
|
1373
|
-
"type": "`string` (HEX)",
|
|
1374
|
-
"default": "auto",
|
|
1375
|
-
"description": "Text colour inside the fill. Without it: `text-white` on the token fill, or black/white by luminance when `color` is set."
|
|
1462
|
+
"prop": "variant",
|
|
1463
|
+
"type": "`\"primary\"` | `\"secondary\"` | `\"success\"` | `\"warning\"` | `\"destructive\"`",
|
|
1464
|
+
"default": "\"primary\"",
|
|
1465
|
+
"description": "Token tone: fill `bg-<tone>-600`, track `bg-<tone>-100` (`dark:bg-<tone>-900/40`), value text `text-white`. `destructive` uses the `error` palette. Replaces the HEX `color` / `bgColor` / `valueColor` props removed in 2.0.0 ([migration](MIGRATION-2.0.md#progressbar-hex-colour-props))."
|
|
1376
1466
|
},
|
|
1377
1467
|
{
|
|
1378
1468
|
"prop": "striped",
|
|
@@ -1384,7 +1474,7 @@
|
|
|
1384
1474
|
"prop": "animated",
|
|
1385
1475
|
"type": "`boolean`",
|
|
1386
1476
|
"default": "false",
|
|
1387
|
-
"description": "Animate stripe movement."
|
|
1477
|
+
"description": "Animate stripe movement (`animate-progress-stripes`, a Tailwind keyframe, so no `<style>` tag is injected). Stops under `prefers-reduced-motion`. Value changes animate the fill width over 300 ms, with no transition under reduced motion."
|
|
1388
1478
|
},
|
|
1389
1479
|
{
|
|
1390
1480
|
"prop": "label",
|
|
@@ -1411,7 +1501,7 @@
|
|
|
1411
1501
|
"description": "Wrapper inline style."
|
|
1412
1502
|
}
|
|
1413
1503
|
],
|
|
1414
|
-
"examples": "```jsx\n<ProgressBar value={60} max={100} />\n<ProgressBar value={75} showValue label=\"Upload\" />\n<ProgressBar value={40} striped animated size=\"lg\" />\n```\n\n[← Component overview](README.md)"
|
|
1504
|
+
"examples": "```jsx\n<ProgressBar value={60} max={100} />\n<ProgressBar value={75} showValue label=\"Upload\" />\n<ProgressBar value={40} striped animated size=\"lg\" />\n<ProgressBar value={90} variant=\"success\" showValue label=\"Storage\" />\n```\n\n[← Component overview](README.md)"
|
|
1415
1505
|
},
|
|
1416
1506
|
{
|
|
1417
1507
|
"name": "Radio",
|
|
@@ -1478,15 +1568,15 @@
|
|
|
1478
1568
|
},
|
|
1479
1569
|
{
|
|
1480
1570
|
"prop": "size",
|
|
1481
|
-
"type": "`\"sm\"` ",
|
|
1482
|
-
"default": "\"md\"
|
|
1483
|
-
"description": "
|
|
1571
|
+
"type": "`\"sm\"` | `\"md\"` | `\"lg\"`",
|
|
1572
|
+
"default": "\"md\"",
|
|
1573
|
+
"description": "Size."
|
|
1484
1574
|
},
|
|
1485
1575
|
{
|
|
1486
1576
|
"prop": "labelPosition",
|
|
1487
|
-
"type": "`\"left\"` ",
|
|
1577
|
+
"type": "`\"left\"` | `\"right\"`",
|
|
1488
1578
|
"default": "\"right\"",
|
|
1489
|
-
"description": "
|
|
1579
|
+
"description": "Label side."
|
|
1490
1580
|
},
|
|
1491
1581
|
{
|
|
1492
1582
|
"prop": "radioColor",
|
|
@@ -1516,7 +1606,7 @@
|
|
|
1516
1606
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
1517
1607
|
"peerDependencies": [],
|
|
1518
1608
|
"hasDoc": true,
|
|
1519
|
-
"description": "Star (or custom icon) rating input/display. Controlled via `value` or uncontrolled via `defaultValue`. Supports hover preview and optional read-only.",
|
|
1609
|
+
"description": "Star (or custom icon) rating input/display. Controlled via `value` or uncontrolled via `defaultValue`. Supports hover preview and optional read-only. Stars use token classes (`text-warning-400` active, `text-gray-200` inactive); the inline `activeColor` / `inactiveColor` overrides were removed in 2.0.0 ([migration](MIGRATION-2.0.md#rate-activecolor-and-inactivecolor)).",
|
|
1520
1610
|
"props": [
|
|
1521
1611
|
{
|
|
1522
1612
|
"prop": "count",
|
|
@@ -1554,18 +1644,6 @@
|
|
|
1554
1644
|
"default": "<StarFilled />",
|
|
1555
1645
|
"description": "Icon for active star."
|
|
1556
1646
|
},
|
|
1557
|
-
{
|
|
1558
|
-
"prop": "activeColor",
|
|
1559
|
-
"type": "`string`",
|
|
1560
|
-
"default": "",
|
|
1561
|
-
"description": "**Deprecated.** Inline colour for active stars. Omit it to use `text-warning-400`."
|
|
1562
|
-
},
|
|
1563
|
-
{
|
|
1564
|
-
"prop": "inactiveColor",
|
|
1565
|
-
"type": "`string`",
|
|
1566
|
-
"default": "",
|
|
1567
|
-
"description": "**Deprecated.** Inline colour for inactive stars. Omit it to use `text-gray-200`."
|
|
1568
|
-
},
|
|
1569
1647
|
{
|
|
1570
1648
|
"prop": "size",
|
|
1571
1649
|
"type": "`number`",
|
|
@@ -1646,9 +1724,9 @@
|
|
|
1646
1724
|
},
|
|
1647
1725
|
{
|
|
1648
1726
|
"prop": "size",
|
|
1649
|
-
"type": "`\"sm\"` ",
|
|
1650
|
-
"default": "\"md\"
|
|
1651
|
-
"description": "
|
|
1727
|
+
"type": "`\"sm\"` | `\"md\"` | `\"lg\"`",
|
|
1728
|
+
"default": "\"md\"",
|
|
1729
|
+
"description": "Size."
|
|
1652
1730
|
},
|
|
1653
1731
|
{
|
|
1654
1732
|
"prop": "onRemove",
|
|
@@ -1682,9 +1760,9 @@
|
|
|
1682
1760
|
},
|
|
1683
1761
|
{
|
|
1684
1762
|
"prop": "avatarPosition",
|
|
1685
|
-
"type": "`\"left\"` ",
|
|
1686
|
-
"default": "\"
|
|
1687
|
-
"description": "
|
|
1763
|
+
"type": "`\"left\"` | `\"right\"`",
|
|
1764
|
+
"default": "\"left\"",
|
|
1765
|
+
"description": "Avatar position."
|
|
1688
1766
|
},
|
|
1689
1767
|
{
|
|
1690
1768
|
"prop": "avatarClassname",
|
|
@@ -1724,15 +1802,15 @@
|
|
|
1724
1802
|
},
|
|
1725
1803
|
{
|
|
1726
1804
|
"prop": "height",
|
|
1727
|
-
"type": "`number` ",
|
|
1728
|
-
"default": "
|
|
1729
|
-
"description": "`
|
|
1805
|
+
"type": "`number` | `string`",
|
|
1806
|
+
"default": "16",
|
|
1807
|
+
"description": "Height in px or CSS unit (e.g. `\"2rem\"`)."
|
|
1730
1808
|
},
|
|
1731
1809
|
{
|
|
1732
1810
|
"prop": "width",
|
|
1733
|
-
"type": "`number` ",
|
|
1734
|
-
"default": "
|
|
1735
|
-
"description": "
|
|
1811
|
+
"type": "`number` | `string`",
|
|
1812
|
+
"default": "",
|
|
1813
|
+
"description": "Width; default 100% (column) or height (inline/circle)."
|
|
1736
1814
|
},
|
|
1737
1815
|
{
|
|
1738
1816
|
"prop": "rounded",
|
|
@@ -1744,7 +1822,7 @@
|
|
|
1744
1822
|
"prop": "animated",
|
|
1745
1823
|
"type": "`boolean`",
|
|
1746
1824
|
"default": "true",
|
|
1747
|
-
"description": "Shimmer animation."
|
|
1825
|
+
"description": "Shimmer animation (`animate-shimmer`, a Tailwind keyframe, so no `<style>` tag is injected). The highlight is soft white in light mode and a faint 10% white in dark mode, so no bright streak shows. Under `prefers-reduced-motion` the bone is static and plain."
|
|
1748
1826
|
},
|
|
1749
1827
|
{
|
|
1750
1828
|
"prop": "active",
|
|
@@ -1822,13 +1900,13 @@
|
|
|
1822
1900
|
"prop": "size",
|
|
1823
1901
|
"type": "`string`",
|
|
1824
1902
|
"default": "\"md\"",
|
|
1825
|
-
"description": "`sm`, `md`, `lg`"
|
|
1903
|
+
"description": "`sm`, `md`, `lg`. `md` takes the iOS 51×31 size on coarse pointers (touch screens)."
|
|
1826
1904
|
},
|
|
1827
1905
|
{
|
|
1828
1906
|
"prop": "color",
|
|
1829
1907
|
"type": "`string`",
|
|
1830
1908
|
"default": "\"primary\"",
|
|
1831
|
-
"description": "
|
|
1909
|
+
"description": "Token preset (`primary`, `secondary`, `success`, `error`, `warning`; aliases `green`, `danger`, `red`) or a custom CSS colour string. The raw-palette presets `blue`, `indigo`, `purple` were **removed in 2.0.0** ([migration](MIGRATION-2.0.md#checkbox-and-switch-raw-colour-presets))."
|
|
1832
1910
|
},
|
|
1833
1911
|
{
|
|
1834
1912
|
"prop": "name",
|
|
@@ -1859,6 +1937,12 @@
|
|
|
1859
1937
|
"type": "`string`",
|
|
1860
1938
|
"default": "",
|
|
1861
1939
|
"description": "Label right"
|
|
1940
|
+
},
|
|
1941
|
+
{
|
|
1942
|
+
"prop": "haptics",
|
|
1943
|
+
"type": "`boolean`",
|
|
1944
|
+
"default": "false",
|
|
1945
|
+
"description": "Opt-in: a `selection` vibration (10ms) on every toggle (tap, drag or keyboard). No-op without `navigator.vibrate` (iOS Safari) and under reduced motion. See [useHaptics](useHaptics.md)"
|
|
1862
1946
|
}
|
|
1863
1947
|
],
|
|
1864
1948
|
"examples": "```jsx\n<Switch checked={enabled} onChange={checked => setEnabled(checked)} />\n<Switch defaultChecked rightLabel=\"Enable notifications\" />\n```\n\n[← Component overview](README.md)"
|
|
@@ -1898,21 +1982,21 @@
|
|
|
1898
1982
|
},
|
|
1899
1983
|
{
|
|
1900
1984
|
"prop": "orientation",
|
|
1901
|
-
"type": "`\"horizontal\"` ",
|
|
1902
|
-
"default": "\"
|
|
1903
|
-
"description": "
|
|
1985
|
+
"type": "`\"horizontal\"` | `\"vertical\"`",
|
|
1986
|
+
"default": "\"horizontal\"",
|
|
1987
|
+
"description": "Layout direction."
|
|
1904
1988
|
},
|
|
1905
1989
|
{
|
|
1906
1990
|
"prop": "size",
|
|
1907
|
-
"type": "`\"sm\"` ",
|
|
1908
|
-
"default": "\"md\"
|
|
1909
|
-
"description": "
|
|
1991
|
+
"type": "`\"sm\"` | `\"md\"` | `\"lg\"`",
|
|
1992
|
+
"default": "\"md\"",
|
|
1993
|
+
"description": "Tab size. Other `Size` values (`xs`, `xl`, `2xl`) fall back to `md`."
|
|
1910
1994
|
},
|
|
1911
1995
|
{
|
|
1912
1996
|
"prop": "variant",
|
|
1913
|
-
"type": "`\"line\"` ",
|
|
1914
|
-
"default": "\"
|
|
1915
|
-
"description": "
|
|
1997
|
+
"type": "`\"line\"` | `\"pills\"` | `\"segmented\"` | `\"unstyled\"`",
|
|
1998
|
+
"default": "\"line\"",
|
|
1999
|
+
"description": "Tab style. `segmented` is an iOS-style segmented control: equal-width segments on a tinted `bg-line` track (same as `SegmentedControl`, follows dark mode) with a sliding surface thumb."
|
|
1916
2000
|
},
|
|
1917
2001
|
{
|
|
1918
2002
|
"prop": "animated",
|
|
@@ -1920,6 +2004,12 @@
|
|
|
1920
2004
|
"default": "true",
|
|
1921
2005
|
"description": "Fade-in (`animate-fade-in`) on the newly active panel; skipped under `prefers-reduced-motion`."
|
|
1922
2006
|
},
|
|
2007
|
+
{
|
|
2008
|
+
"prop": "swipeable",
|
|
2009
|
+
"type": "`boolean`",
|
|
2010
|
+
"default": "false",
|
|
2011
|
+
"description": "Swipe the panels left/right (touch or pen, at least 50 px and mostly horizontal) to move to the next/previous enabled tab. Vertical scrolling is never blocked (`touch-action: pan-y`); mouse drags are ignored."
|
|
2012
|
+
},
|
|
1923
2013
|
{
|
|
1924
2014
|
"prop": "className",
|
|
1925
2015
|
"type": "`string`",
|
|
@@ -1936,19 +2026,25 @@
|
|
|
1936
2026
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
1937
2027
|
"peerDependencies": [],
|
|
1938
2028
|
"hasDoc": true,
|
|
1939
|
-
"description": "
|
|
2029
|
+
"description": "Action menu triggered by a three-dot (kebab) button. Use for row actions (Edit, Delete, etc.). On desktop it opens a floating menu; on phones it opens an iOS-style action sheet. Closes on outside press, Escape and (by default) on item select.",
|
|
1940
2030
|
"props": [
|
|
1941
2031
|
{
|
|
1942
2032
|
"prop": "items",
|
|
1943
2033
|
"type": "`Array`",
|
|
1944
2034
|
"default": "[]",
|
|
1945
|
-
"description": "Menu items: `{ key, label, icon?, onClick?, disabled?, destructive? }`. `icon` can be component (e.g. `Edit2`)."
|
|
2035
|
+
"description": "Menu items: `{ key, label, icon?, onClick?, disabled?, destructive?, tone? }`. `icon` can be a component (e.g. `Edit2`) or a node."
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
"prop": "presentation",
|
|
2039
|
+
"type": "`string`",
|
|
2040
|
+
"default": "\"auto\"",
|
|
2041
|
+
"description": "One of `auto`, `popover`, `sheet`. `popover`: floating menu next to the trigger. `sheet`: action sheet from the bottom of the screen. `auto`: the sheet on small touch screens (`(pointer: coarse) and (max-width: 639px)`), the popover elsewhere and on the server."
|
|
1946
2042
|
},
|
|
1947
2043
|
{
|
|
1948
2044
|
"prop": "trigger",
|
|
1949
|
-
"type": "`
|
|
2045
|
+
"type": "`ReactElement`",
|
|
1950
2046
|
"default": "",
|
|
1951
|
-
"description": "Custom trigger; default is three-dot button."
|
|
2047
|
+
"description": "Custom trigger; default is three-dot button. It receives `ref`, `onClick`, `onKeyDown`, `aria-haspopup=\"menu\"`, `aria-expanded` and `aria-controls`."
|
|
1952
2048
|
},
|
|
1953
2049
|
{
|
|
1954
2050
|
"prop": "className",
|
|
@@ -1966,13 +2062,13 @@
|
|
|
1966
2062
|
"prop": "menuClass",
|
|
1967
2063
|
"type": "`string`",
|
|
1968
2064
|
"default": "\"\"",
|
|
1969
|
-
"description": "
|
|
2065
|
+
"description": "Classes for the popover menu container (popover mode only)."
|
|
1970
2066
|
},
|
|
1971
2067
|
{
|
|
1972
2068
|
"prop": "menuItemClass",
|
|
1973
2069
|
"type": "`string`",
|
|
1974
2070
|
"default": "\"\"",
|
|
1975
|
-
"description": "
|
|
2071
|
+
"description": "Classes for every menu item (popover and sheet)."
|
|
1976
2072
|
},
|
|
1977
2073
|
{
|
|
1978
2074
|
"prop": "closeOnSelect",
|
|
@@ -1984,10 +2080,16 @@
|
|
|
1984
2080
|
"prop": "ariaLabel",
|
|
1985
2081
|
"type": "`string`",
|
|
1986
2082
|
"default": "\"More options\"",
|
|
1987
|
-
"description": "Trigger button aria-label."
|
|
2083
|
+
"description": "Trigger button aria-label; also names the menu / sheet."
|
|
2084
|
+
},
|
|
2085
|
+
{
|
|
2086
|
+
"prop": "cancelLabel",
|
|
2087
|
+
"type": "`ReactNode`",
|
|
2088
|
+
"default": "\"Cancel\"",
|
|
2089
|
+
"description": "Text of the action sheet's Cancel button (sheet mode only)."
|
|
1988
2090
|
}
|
|
1989
2091
|
],
|
|
1990
|
-
"examples": "```jsx\nimport { Edit2, Eye, Trash } from 'lucide-react';\n\n<ThreeDotPopover\n items={[\n { key: 'edit', label: 'Edit', icon: Edit2, onClick: () => edit(row) },\n { key: 'view', label: 'View', icon: Eye, onClick: () => view(row) },\n { key: 'delete', label: 'Delete',
|
|
2092
|
+
"examples": "```jsx\nimport { Edit2, Eye, Trash } from 'lucide-react';\n\n<ThreeDotPopover\n items={[\n { key: 'edit', label: 'Edit', icon: Edit2, onClick: () => edit(row) },\n { key: 'view', label: 'View', icon: Eye, onClick: () => view(row) },\n { key: 'delete', label: 'Delete', tone: 'destructive', onClick: () => remove(row) },\n ]}\n closeOnSelect\n/>\n```\n\nKeep the desktop menu on every device:\n\n```jsx\n<ThreeDotPopover presentation=\"popover\" items={items} />\n```"
|
|
1991
2093
|
},
|
|
1992
2094
|
{
|
|
1993
2095
|
"name": "Tooltip",
|
|
@@ -1996,25 +2098,25 @@
|
|
|
1996
2098
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
1997
2099
|
"peerDependencies": [],
|
|
1998
2100
|
"hasDoc": true,
|
|
1999
|
-
"description": "Tooltip shown on hover, focus, or click. Supports placement (auto/top/bottom/left/right), delay, offset, and optional portal. Content can be string or render prop `({ close }) => ReactNode`.",
|
|
2101
|
+
"description": "Tooltip shown on hover, focus, or click — and on long-press on touch screens. Supports placement (auto/top/bottom/left/right), delay, offset, and optional portal. Positioned by the shared floating engine (`position: fixed`, flips and shifts to stay in view) in the `z-tooltip` layer. Content can be string or render prop `({ close }) => ReactNode`. Tooltip has no sheet presentation: for interactive content on phones use [`Popover`](Popover.md).",
|
|
2000
2102
|
"props": [
|
|
2001
2103
|
{
|
|
2002
2104
|
"prop": "position",
|
|
2003
|
-
"type": "`\"auto\"` ",
|
|
2004
|
-
"default": "\"
|
|
2005
|
-
"description": "
|
|
2105
|
+
"type": "`\"auto\"` | `\"top\"` | `\"bottom\"` | `\"left\"` | `\"right\"`",
|
|
2106
|
+
"default": "\"auto\"",
|
|
2107
|
+
"description": "Preferred side. `auto` means top; every side flips to the opposite side when there is no room and shifts along its axis to stay 8px inside the viewport. The resolved placement is on the tooltip as `data-placement`."
|
|
2006
2108
|
},
|
|
2007
2109
|
{
|
|
2008
2110
|
"prop": "trigger",
|
|
2009
|
-
"type": "`\"hover\"` ",
|
|
2010
|
-
"default": "\"
|
|
2011
|
-
"description": "
|
|
2111
|
+
"type": "`\"hover\"` | `\"click\"` | `\"focus\"` | `Array`",
|
|
2112
|
+
"default": "\"hover\"",
|
|
2113
|
+
"description": "How tooltip opens. `hover` also opens on keyboard focus (unless `click` is in the list), and on touch screens `hover` / `focus` open on a long-press (see **Touch**)."
|
|
2012
2114
|
},
|
|
2013
2115
|
{
|
|
2014
2116
|
"prop": "content",
|
|
2015
|
-
"type": "`ReactNode` ",
|
|
2016
|
-
"default": "
|
|
2017
|
-
"description": "
|
|
2117
|
+
"type": "`ReactNode` | `({ close }) => ReactNode`",
|
|
2118
|
+
"default": "",
|
|
2119
|
+
"description": "Tooltip content."
|
|
2018
2120
|
},
|
|
2019
2121
|
{
|
|
2020
2122
|
"prop": "customTrigger",
|
|
@@ -2062,13 +2164,13 @@
|
|
|
2062
2164
|
"prop": "textClass",
|
|
2063
2165
|
"type": "`string`",
|
|
2064
2166
|
"default": "\"text-foreground\"",
|
|
2065
|
-
"description": "Tailwind class for the tooltip text color
|
|
2167
|
+
"description": "Tailwind class for the tooltip text color (role token, flips in dark mode)."
|
|
2066
2168
|
},
|
|
2067
2169
|
{
|
|
2068
2170
|
"prop": "bgClass",
|
|
2069
2171
|
"type": "`string`",
|
|
2070
2172
|
"default": "\"bg-surface\"",
|
|
2071
|
-
"description": "Tailwind class for the tooltip background
|
|
2173
|
+
"description": "Tailwind class for the tooltip background (role token, flips in dark mode)."
|
|
2072
2174
|
},
|
|
2073
2175
|
{
|
|
2074
2176
|
"prop": "className",
|
|
@@ -2080,7 +2182,13 @@
|
|
|
2080
2182
|
"prop": "portal",
|
|
2081
2183
|
"type": "`boolean`",
|
|
2082
2184
|
"default": "true",
|
|
2083
|
-
"description": "Render tooltip in document.body. With `false` it renders inline
|
|
2185
|
+
"description": "Render tooltip in document.body. With `false` it renders inline; it is still `position: fixed`, so avoid ancestors with `transform`/`filter` (they become its containing block)."
|
|
2186
|
+
},
|
|
2187
|
+
{
|
|
2188
|
+
"prop": "style",
|
|
2189
|
+
"type": "`CSSProperties`",
|
|
2190
|
+
"default": "",
|
|
2191
|
+
"description": "Merged over the computed position styles on the tooltip box."
|
|
2084
2192
|
},
|
|
2085
2193
|
{
|
|
2086
2194
|
"prop": "id",
|
|
@@ -2102,15 +2210,15 @@
|
|
|
2102
2210
|
"props": [
|
|
2103
2211
|
{
|
|
2104
2212
|
"prop": "value",
|
|
2105
|
-
"type": "`Date` ",
|
|
2106
|
-
"default": "
|
|
2107
|
-
"description": "
|
|
2213
|
+
"type": "`Date` | `string`",
|
|
2214
|
+
"default": "",
|
|
2215
|
+
"description": "Selected date (Date or ISO string)."
|
|
2108
2216
|
},
|
|
2109
2217
|
{
|
|
2110
2218
|
"prop": "onChange",
|
|
2111
|
-
"type": "`(date: Date ",
|
|
2112
|
-
"default": "
|
|
2113
|
-
"description": "
|
|
2219
|
+
"type": "`(date: Date | null) => void`",
|
|
2220
|
+
"default": "",
|
|
2221
|
+
"description": "Called when date changes."
|
|
2114
2222
|
},
|
|
2115
2223
|
{
|
|
2116
2224
|
"prop": "placeholder",
|
|
@@ -2168,9 +2276,9 @@
|
|
|
2168
2276
|
},
|
|
2169
2277
|
{
|
|
2170
2278
|
"prop": "weekStartsOn",
|
|
2171
|
-
"type": "`0` ",
|
|
2172
|
-
"default": "
|
|
2173
|
-
"description": "
|
|
2279
|
+
"type": "`0` | `1`",
|
|
2280
|
+
"default": "0",
|
|
2281
|
+
"description": "0 = Sunday, 1 = Monday."
|
|
2174
2282
|
},
|
|
2175
2283
|
{
|
|
2176
2284
|
"prop": "portal",
|
|
@@ -2194,13 +2302,19 @@
|
|
|
2194
2302
|
"prop": "showClear",
|
|
2195
2303
|
"type": "`boolean`",
|
|
2196
2304
|
"default": "true",
|
|
2197
|
-
"description": "Show the clear controls: a \"Clear date\" button overlaid on the field (a sibling `<button>`, keyboard-reachable) when a value is set, plus \"Clear\" in the
|
|
2305
|
+
"description": "Show the clear controls: a \"Clear date\" button overlaid on the field (a sibling `<button>`, keyboard-reachable) when a value is set, plus \"Clear\" in the calendar."
|
|
2198
2306
|
},
|
|
2199
2307
|
{
|
|
2200
2308
|
"prop": "closeOnSelect",
|
|
2201
2309
|
"type": "`boolean`",
|
|
2202
2310
|
"default": "true",
|
|
2203
2311
|
"description": "Close popup on date select."
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
"prop": "presentation",
|
|
2315
|
+
"type": "`'auto'` | `'popover'` | `'sheet'`",
|
|
2316
|
+
"default": "'auto'",
|
|
2317
|
+
"description": "How the calendar opens. `auto` uses a bottom `Sheet` on coarse-pointer screens under 640px and an anchored popover elsewhere (popover on the server). Pass `'popover'` to keep the desktop popup everywhere."
|
|
2204
2318
|
}
|
|
2205
2319
|
],
|
|
2206
2320
|
"examples": "```jsx\nconst [date, setDate] = useState(null);\n<DatePicker\n value={date}\n onChange={setDate}\n placeholder=\"Select date\"\n minDate={new Date()}\n label=\"Start date\"\n/>\n```\n\n[← Component overview](README.md)"
|
|
@@ -2216,15 +2330,15 @@
|
|
|
2216
2330
|
"props": [
|
|
2217
2331
|
{
|
|
2218
2332
|
"prop": "value",
|
|
2219
|
-
"type": "`{ lat: number, lng: number }` ",
|
|
2333
|
+
"type": "`{ lat: number, lng: number }` | `null`",
|
|
2220
2334
|
"default": "null",
|
|
2221
|
-
"description": "
|
|
2335
|
+
"description": "Current coordinates."
|
|
2222
2336
|
},
|
|
2223
2337
|
{
|
|
2224
2338
|
"prop": "onChange",
|
|
2225
|
-
"type": "`(coords: { lat, lng } ",
|
|
2226
|
-
"default": "
|
|
2227
|
-
"description": "
|
|
2339
|
+
"type": "`(coords: { lat, lng } | null) => void`",
|
|
2340
|
+
"default": "",
|
|
2341
|
+
"description": "Called when value changes (typed or from geolocation)."
|
|
2228
2342
|
},
|
|
2229
2343
|
{
|
|
2230
2344
|
"prop": "inputProps",
|
|
@@ -2252,15 +2366,21 @@
|
|
|
2252
2366
|
},
|
|
2253
2367
|
{
|
|
2254
2368
|
"prop": "iconPosition",
|
|
2255
|
-
"type": "`\"left\"` ",
|
|
2369
|
+
"type": "`\"left\"` | `\"right\"`",
|
|
2256
2370
|
"default": "\"right\"",
|
|
2257
|
-
"description": "
|
|
2371
|
+
"description": "Position of the icon button."
|
|
2258
2372
|
},
|
|
2259
2373
|
{
|
|
2260
2374
|
"prop": "placeholder",
|
|
2261
2375
|
"type": "`string`",
|
|
2262
2376
|
"default": "\"lat, lng\"",
|
|
2263
2377
|
"description": "Input placeholder."
|
|
2378
|
+
},
|
|
2379
|
+
{
|
|
2380
|
+
"prop": "fullWidth",
|
|
2381
|
+
"type": "`boolean`",
|
|
2382
|
+
"default": "false",
|
|
2383
|
+
"description": "Stretch the root and input to the container width (drops the input's 200 px minimum)."
|
|
2264
2384
|
}
|
|
2265
2385
|
],
|
|
2266
2386
|
"examples": "```jsx\nconst [location, setLocation] = useState(null);\n<LocationPicker\n value={location}\n onChange={setLocation}\n placeholder=\"Enter lat, lng or use location\"\n/>\n```\n\n[← Component overview](README.md)"
|
|
@@ -2496,6 +2616,12 @@
|
|
|
2496
2616
|
"default": "",
|
|
2497
2617
|
"description": "With `autoResize`, cap the height and scroll beyond it"
|
|
2498
2618
|
},
|
|
2619
|
+
{
|
|
2620
|
+
"prop": "size",
|
|
2621
|
+
"type": "`\"sm\"` | `\"md\"` | `\"lg\"`",
|
|
2622
|
+
"default": "\"md\"",
|
|
2623
|
+
"description": "Padding and text size (`md` is the original look); `rows` still sets the height"
|
|
2624
|
+
},
|
|
2499
2625
|
{
|
|
2500
2626
|
"prop": "value",
|
|
2501
2627
|
"type": "`string`",
|
|
@@ -2610,6 +2736,12 @@
|
|
|
2610
2736
|
"default": "false",
|
|
2611
2737
|
"description": "Show the formatted value next to the label"
|
|
2612
2738
|
},
|
|
2739
|
+
{
|
|
2740
|
+
"prop": "showBubble",
|
|
2741
|
+
"type": "`boolean`",
|
|
2742
|
+
"default": "true",
|
|
2743
|
+
"description": "Show a value bubble (formatted with `formatValue`) above the thumb while it is dragged; hidden otherwise"
|
|
2744
|
+
},
|
|
2613
2745
|
{
|
|
2614
2746
|
"prop": "formatValue",
|
|
2615
2747
|
"type": "`function`",
|
|
@@ -2650,7 +2782,7 @@
|
|
|
2650
2782
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
2651
2783
|
"peerDependencies": [],
|
|
2652
2784
|
"hasDoc": true,
|
|
2653
|
-
"description": "Anchored, click-triggered panel for **interactive** content — filter forms, share menus, quick editors. Unlike `Tooltip` (hover/focus, read-only text), a Popover takes focus, can hold buttons and inputs, and stays open until dismissed. Positioned in a portal
|
|
2785
|
+
"description": "Anchored, click-triggered panel for **interactive** content — filter forms, share menus, quick editors. Unlike `Tooltip` (hover/focus, read-only text), a Popover takes focus, can hold buttons and inputs, and stays open until dismissed. Positioned in a portal by the shared floating engine (flip to the opposite side, shift to stay in view). On phones it becomes a bottom sheet (see **Presentation**).",
|
|
2654
2786
|
"props": [
|
|
2655
2787
|
{
|
|
2656
2788
|
"prop": "trigger",
|
|
@@ -2712,6 +2844,12 @@
|
|
|
2712
2844
|
"default": "false",
|
|
2713
2845
|
"description": "Disable the trigger"
|
|
2714
2846
|
},
|
|
2847
|
+
{
|
|
2848
|
+
"prop": "presentation",
|
|
2849
|
+
"type": "`\"auto\"`, `\"popover\"` or `\"sheet\"`",
|
|
2850
|
+
"default": "\"auto\"",
|
|
2851
|
+
"description": "`auto` shows a bottom [`Sheet`](Sheet.md) on small touch screens (`(pointer: coarse) and (max-width: 639px)`) and the anchored panel elsewhere; `popover` / `sheet` force one. `auto` renders the panel on the server and while hydrating"
|
|
2852
|
+
},
|
|
2715
2853
|
{
|
|
2716
2854
|
"prop": "id",
|
|
2717
2855
|
"type": "`string`",
|
|
@@ -2722,133 +2860,523 @@
|
|
|
2722
2860
|
"prop": "className",
|
|
2723
2861
|
"type": "`string`",
|
|
2724
2862
|
"default": "",
|
|
2725
|
-
"description": "Extra classes for the panel (merged with `cn`)"
|
|
2863
|
+
"description": "Extra classes for the panel, or for the sheet panel in sheet mode (merged with `cn`)"
|
|
2726
2864
|
},
|
|
2727
2865
|
{
|
|
2728
2866
|
"prop": "...rest",
|
|
2729
2867
|
"type": "`HTMLAttributes`",
|
|
2730
2868
|
"default": "",
|
|
2731
|
-
"description": "Forwarded to the panel (`aria-label`, `data-*`, …); `ref` points at the panel"
|
|
2869
|
+
"description": "Forwarded to the panel (`aria-label`, `data-*`, …); `ref` points at the panel (the sheet panel in sheet mode). The panel carries `data-presentation=\"popover\"` or `\"sheet\"`"
|
|
2732
2870
|
}
|
|
2733
2871
|
],
|
|
2734
|
-
"examples": "### Basic\n\n```jsx\n<Popover trigger={<Button variant=\"outline\">Filters</Button>} title=\"Filter tasks\">\n {({ close }) => (\n <div className=\"space-y-3\">\n <Checkbox label=\"Assigned to me\" />\n <Button size=\"sm\" onClick={close}>Apply</Button>\n </div>\n )}\n</Popover>\n```\n\n### Controlled\n\n```jsx\nconst [open, setOpen] = useState(false);\n\n<Popover open={open} onOpenChange={setOpen} placement=\"top-end\" aria-label=\"Share\" trigger={<Button>Share</Button>}>\n <p>Anyone with the link can view.</p>\n</Popover>\n```"
|
|
2872
|
+
"examples": "### Basic\n\n```jsx\n<Popover trigger={<Button variant=\"outline\">Filters</Button>} title=\"Filter tasks\">\n {({ close }) => (\n <div className=\"space-y-3\">\n <Checkbox label=\"Assigned to me\" />\n <Button size=\"sm\" onClick={close}>Apply</Button>\n </div>\n )}\n</Popover>\n```\n\n### Controlled\n\n```jsx\nconst [open, setOpen] = useState(false);\n\n<Popover open={open} onOpenChange={setOpen} placement=\"top-end\" aria-label=\"Share\" trigger={<Button>Share</Button>}>\n <p>Anyone with the link can view.</p>\n</Popover>\n```\n\n### Presentation (phones)\n\n```jsx\n// Default: anchored panel on desktop, bottom sheet on phones\n<Popover trigger={<Button>Sort</Button>} title=\"Sort by\">…</Popover>\n\n// Keep the anchored panel everywhere\n<Popover presentation=\"popover\" trigger={<Button>Sort</Button>}>…</Popover>\n```\n\nIn sheet mode the content renders inside `<Sheet.Body>` of a bottom `Sheet`: `title` becomes the sheet title (with its close button), and the sheet adds a grab handle, drag-to-dismiss, a backdrop and the safe-area padding. `open` / `onOpenChange`, the `({ close })` render prop, `initialFocusRef`, `className`, `id` and `ref` work the same way."
|
|
2735
2873
|
},
|
|
2736
2874
|
{
|
|
2737
|
-
"name": "
|
|
2738
|
-
"group": "
|
|
2875
|
+
"name": "SegmentedControl",
|
|
2876
|
+
"group": "core",
|
|
2739
2877
|
"familyExports": [],
|
|
2740
2878
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
2741
2879
|
"peerDependencies": [],
|
|
2742
2880
|
"hasDoc": true,
|
|
2743
|
-
"description": "
|
|
2881
|
+
"description": "iOS-style segmented control for switching a **view or value** (list / board, day / week / month). It is a `role=\"radiogroup\"` with one `role=\"radio\"` per option and a sliding thumb under the selected option. Controlled or uncontrolled. When the choice switches **panels of content**, use `Tabs variant=\"segmented\"` instead: it looks the same but has tab / tabpanel semantics.",
|
|
2744
2882
|
"props": [
|
|
2745
2883
|
{
|
|
2746
|
-
"prop": "
|
|
2747
|
-
"type": "`Array
|
|
2884
|
+
"prop": "options",
|
|
2885
|
+
"type": "`Array<{ value, label, icon?, disabled? }>`",
|
|
2748
2886
|
"default": "[]",
|
|
2749
|
-
"description": "
|
|
2887
|
+
"description": "The segments. `value` is a string or number and must be unique; `label` is a node; `icon` is a node shown before the label (hidden from screen readers); `disabled` skips the option."
|
|
2750
2888
|
},
|
|
2751
2889
|
{
|
|
2752
|
-
"prop": "
|
|
2753
|
-
"type": "`
|
|
2754
|
-
"default": "
|
|
2755
|
-
"description": "
|
|
2890
|
+
"prop": "value",
|
|
2891
|
+
"type": "`string`",
|
|
2892
|
+
"default": "",
|
|
2893
|
+
"description": "Controlled selected value (a string or number matching an option's `value`)."
|
|
2756
2894
|
},
|
|
2757
2895
|
{
|
|
2758
|
-
"prop": "
|
|
2759
|
-
"type": "`
|
|
2896
|
+
"prop": "defaultValue",
|
|
2897
|
+
"type": "`string`",
|
|
2760
2898
|
"default": "",
|
|
2761
|
-
"description": "
|
|
2899
|
+
"description": "Uncontrolled initial value (string or number). Without `value` or `defaultValue` nothing is selected and the first enabled option is the tab stop."
|
|
2762
2900
|
},
|
|
2763
2901
|
{
|
|
2764
|
-
"prop": "
|
|
2765
|
-
"type": "`
|
|
2902
|
+
"prop": "onChange",
|
|
2903
|
+
"type": "`(value) => void`",
|
|
2766
2904
|
"default": "",
|
|
2767
|
-
"description": "
|
|
2905
|
+
"description": "Receives the newly selected option's `value` (not a DOM event). Not called when the selected option is clicked again."
|
|
2768
2906
|
},
|
|
2769
2907
|
{
|
|
2770
|
-
"prop": "
|
|
2908
|
+
"prop": "size",
|
|
2771
2909
|
"type": "`string`",
|
|
2772
|
-
"default": "",
|
|
2773
|
-
"description": "
|
|
2910
|
+
"default": "\"md\"",
|
|
2911
|
+
"description": "`sm`, `md` or `lg`. Same padding and type scale as `Tabs` segments. Every size grows to 44px tall on coarse pointers (touch screens)."
|
|
2774
2912
|
},
|
|
2775
2913
|
{
|
|
2776
|
-
"prop": "
|
|
2777
|
-
"type": "`
|
|
2778
|
-
"default": "
|
|
2779
|
-
"description": "
|
|
2914
|
+
"prop": "fullWidth",
|
|
2915
|
+
"type": "`boolean`",
|
|
2916
|
+
"default": "false",
|
|
2917
|
+
"description": "Stretch to the container's width. Segments are equal width either way."
|
|
2780
2918
|
},
|
|
2781
2919
|
{
|
|
2782
|
-
"prop": "
|
|
2783
|
-
"type": "`string
|
|
2920
|
+
"prop": "name",
|
|
2921
|
+
"type": "`string`",
|
|
2784
2922
|
"default": "",
|
|
2785
|
-
"description": "
|
|
2923
|
+
"description": "Submits the selected value with a form through a hidden input."
|
|
2786
2924
|
},
|
|
2787
2925
|
{
|
|
2788
|
-
"prop": "
|
|
2789
|
-
"type": "`
|
|
2790
|
-
"default": "
|
|
2791
|
-
"description": "
|
|
2926
|
+
"prop": "disabled",
|
|
2927
|
+
"type": "`boolean`",
|
|
2928
|
+
"default": "false",
|
|
2929
|
+
"description": "Disable the whole group (dimmed, out of the tab order, `aria-disabled` on the root)."
|
|
2792
2930
|
},
|
|
2793
2931
|
{
|
|
2794
|
-
"prop": "
|
|
2795
|
-
"type": "`
|
|
2932
|
+
"prop": "aria-label",
|
|
2933
|
+
"type": "`string`",
|
|
2796
2934
|
"default": "",
|
|
2797
|
-
"description": "
|
|
2935
|
+
"description": "Accessible name of the group. Pass this or `aria-labelledby`."
|
|
2798
2936
|
},
|
|
2799
2937
|
{
|
|
2800
|
-
"prop": "
|
|
2801
|
-
"type": "`
|
|
2938
|
+
"prop": "aria-labelledby",
|
|
2939
|
+
"type": "`string`",
|
|
2802
2940
|
"default": "",
|
|
2803
|
-
"description": "
|
|
2804
|
-
},
|
|
2805
|
-
{
|
|
2806
|
-
"prop": "footer",
|
|
2807
|
-
"type": "`ReactNode` ",
|
|
2808
|
-
"default": "({ collapsed, isMobile, closeMobile }) => ReactNode",
|
|
2809
|
-
"description": "—"
|
|
2941
|
+
"description": "Id of a visible label for the group."
|
|
2810
2942
|
},
|
|
2811
2943
|
{
|
|
2812
|
-
"prop": "
|
|
2813
|
-
"type": "`
|
|
2944
|
+
"prop": "className",
|
|
2945
|
+
"type": "`string`",
|
|
2814
2946
|
"default": "",
|
|
2815
|
-
"description": "
|
|
2816
|
-
}
|
|
2947
|
+
"description": "Merged onto the root with `cn`, so your classes win on conflict."
|
|
2948
|
+
}
|
|
2949
|
+
],
|
|
2950
|
+
"examples": "```jsx\nconst [view, setView] = useState('list');\n\n<SegmentedControl\n aria-label=\"View\"\n value={view}\n onChange={setView}\n options={[\n { value: 'list', label: 'List', icon: <List className=\"h-4 w-4\" /> },\n { value: 'board', label: 'Board' },\n { value: 'calendar', label: 'Calendar', disabled: true },\n ]}\n/>\n\n<SegmentedControl aria-label=\"Period\" fullWidth size=\"lg\" defaultValue=\"week\" options={[\n { value: 'day', label: 'Day' },\n { value: 'week', label: 'Week' },\n { value: 'month', label: 'Month' },\n]} />\n```\n\n[← Component overview](README.md)"
|
|
2951
|
+
},
|
|
2952
|
+
{
|
|
2953
|
+
"name": "SwipeableListItem",
|
|
2954
|
+
"group": "core",
|
|
2955
|
+
"familyExports": [],
|
|
2956
|
+
"importFrom": "@dreamtree-org/twreact-ui",
|
|
2957
|
+
"peerDependencies": [],
|
|
2958
|
+
"hasDoc": true,
|
|
2959
|
+
"description": "A list row with swipe actions, the iOS Mail / Gmail pattern. Swipe right to reveal the **leading** actions, left to reveal the **trailing** ones; release to snap open or closed, or swipe all the way to commit the first action on that side. Past the actions the row rubber-bands. Every action is also reachable without a gesture through a **More actions** button that opens them as a menu (an action sheet on phones).",
|
|
2960
|
+
"props": [
|
|
2817
2961
|
{
|
|
2818
|
-
"prop": "
|
|
2819
|
-
"type": "`
|
|
2820
|
-
"default": "",
|
|
2821
|
-
"description": "
|
|
2962
|
+
"prop": "leadingActions",
|
|
2963
|
+
"type": "`SwipeAction[]`",
|
|
2964
|
+
"default": "[]",
|
|
2965
|
+
"description": "Revealed by swiping right. The first one sits at the left edge and is the full-swipe action"
|
|
2822
2966
|
},
|
|
2823
2967
|
{
|
|
2824
|
-
"prop": "
|
|
2825
|
-
"type": "
|
|
2826
|
-
"default": "
|
|
2827
|
-
"description": "
|
|
2968
|
+
"prop": "trailingActions",
|
|
2969
|
+
"type": "`SwipeAction[]`",
|
|
2970
|
+
"default": "[]",
|
|
2971
|
+
"description": "Revealed by swiping left. The first one sits at the right edge and is the full-swipe action"
|
|
2828
2972
|
},
|
|
2829
2973
|
{
|
|
2830
|
-
"prop": "
|
|
2831
|
-
"type": "`boolean`",
|
|
2832
|
-
"default": "",
|
|
2833
|
-
"description": "
|
|
2974
|
+
"prop": "fullSwipe",
|
|
2975
|
+
"type": "`boolean` | `{ leading?, trailing? }`",
|
|
2976
|
+
"default": "true",
|
|
2977
|
+
"description": "A swipe past `fullSwipeThreshold` commits the first action of that side. Pass an object to allow it on one side only"
|
|
2834
2978
|
},
|
|
2835
2979
|
{
|
|
2836
|
-
"prop": "
|
|
2837
|
-
"type": "`
|
|
2838
|
-
"default": "",
|
|
2839
|
-
"description": "
|
|
2980
|
+
"prop": "fullSwipeThreshold",
|
|
2981
|
+
"type": "`number`",
|
|
2982
|
+
"default": "0.6",
|
|
2983
|
+
"description": "Fraction of the row width a full swipe must travel (and at least 24px past the revealed actions)"
|
|
2840
2984
|
},
|
|
2841
2985
|
{
|
|
2842
|
-
"prop": "
|
|
2843
|
-
"type": "`
|
|
2844
|
-
"default": "
|
|
2845
|
-
"description": "
|
|
2986
|
+
"prop": "actionWidth",
|
|
2987
|
+
"type": "`number`",
|
|
2988
|
+
"default": "80",
|
|
2989
|
+
"description": "Width in px of each revealed action"
|
|
2846
2990
|
},
|
|
2847
2991
|
{
|
|
2848
|
-
"prop": "
|
|
2849
|
-
"type": "`
|
|
2992
|
+
"prop": "open",
|
|
2993
|
+
"type": "`'leading'` | `'trailing'` | `null`",
|
|
2850
2994
|
"default": "",
|
|
2851
|
-
"description": "
|
|
2995
|
+
"description": "Controlled open side"
|
|
2996
|
+
},
|
|
2997
|
+
{
|
|
2998
|
+
"prop": "defaultOpen",
|
|
2999
|
+
"type": "`'leading'` | `'trailing'` | `null`",
|
|
3000
|
+
"default": "null",
|
|
3001
|
+
"description": "Uncontrolled initial open side"
|
|
3002
|
+
},
|
|
3003
|
+
{
|
|
3004
|
+
"prop": "onOpenChange",
|
|
3005
|
+
"type": "`(side) => void`",
|
|
3006
|
+
"default": "",
|
|
3007
|
+
"description": "Called with `'leading'`, `'trailing'` or `null` when the row opens or closes"
|
|
3008
|
+
},
|
|
3009
|
+
{
|
|
3010
|
+
"prop": "disabled",
|
|
3011
|
+
"type": "`boolean`",
|
|
3012
|
+
"default": "false",
|
|
3013
|
+
"description": "Turns off the gesture and the More actions button"
|
|
3014
|
+
},
|
|
3015
|
+
{
|
|
3016
|
+
"prop": "moreActions",
|
|
3017
|
+
"type": "`'focus'` | `'always'` | `false`",
|
|
3018
|
+
"default": "'focus'",
|
|
3019
|
+
"description": "The gesture-free button. `focus`: shown on keyboard focus and mouse hover. `always`: always visible. `false`: removed (only when you provide another way to reach the actions)"
|
|
3020
|
+
},
|
|
3021
|
+
{
|
|
3022
|
+
"prop": "moreActionsLabel",
|
|
3023
|
+
"type": "`string`",
|
|
3024
|
+
"default": "\"More actions\"",
|
|
3025
|
+
"description": "Accessible name of the button and its menu"
|
|
3026
|
+
},
|
|
3027
|
+
{
|
|
3028
|
+
"prop": "menuPresentation",
|
|
3029
|
+
"type": "`'auto'` | `'popover'` | `'sheet'`",
|
|
3030
|
+
"default": "'auto'",
|
|
3031
|
+
"description": "How the More actions menu opens. `auto` uses an action sheet on small touch screens"
|
|
3032
|
+
},
|
|
3033
|
+
{
|
|
3034
|
+
"prop": "as",
|
|
3035
|
+
"type": "`ElementType`",
|
|
3036
|
+
"default": "\"div\"",
|
|
3037
|
+
"description": "Root element, e.g. `\"li\"` inside a `<ul>`"
|
|
3038
|
+
},
|
|
3039
|
+
{
|
|
3040
|
+
"prop": "contentClassName",
|
|
3041
|
+
"type": "`string`",
|
|
3042
|
+
"default": "",
|
|
3043
|
+
"description": "Classes for the sliding content layer (default background `bg-surface`)"
|
|
3044
|
+
},
|
|
3045
|
+
{
|
|
3046
|
+
"prop": "className",
|
|
3047
|
+
"type": "`string`",
|
|
3048
|
+
"default": "",
|
|
3049
|
+
"description": "Root classes (merged with `cn`)"
|
|
3050
|
+
},
|
|
3051
|
+
{
|
|
3052
|
+
"prop": "children",
|
|
3053
|
+
"type": "`ReactNode`",
|
|
3054
|
+
"default": "",
|
|
3055
|
+
"description": "The row content"
|
|
3056
|
+
},
|
|
3057
|
+
{
|
|
3058
|
+
"prop": "...rest",
|
|
3059
|
+
"type": "`HTMLAttributes`",
|
|
3060
|
+
"default": "",
|
|
3061
|
+
"description": "Forwarded to the root; `ref` points at the root"
|
|
3062
|
+
},
|
|
3063
|
+
{
|
|
3064
|
+
"prop": "Field",
|
|
3065
|
+
"type": "Type",
|
|
3066
|
+
"default": "Description",
|
|
3067
|
+
"description": ""
|
|
3068
|
+
},
|
|
3069
|
+
{
|
|
3070
|
+
"prop": "key",
|
|
3071
|
+
"type": "`string`",
|
|
3072
|
+
"default": "Unique key",
|
|
3073
|
+
"description": ""
|
|
3074
|
+
},
|
|
3075
|
+
{
|
|
3076
|
+
"prop": "label",
|
|
3077
|
+
"type": "`ReactNode`",
|
|
3078
|
+
"default": "Visible text (also the menu item text)",
|
|
3079
|
+
"description": ""
|
|
3080
|
+
},
|
|
3081
|
+
{
|
|
3082
|
+
"prop": "icon",
|
|
3083
|
+
"type": "`ReactNode` | `Component`",
|
|
3084
|
+
"default": "Shown above the label (<Trash2 /> or Trash2)",
|
|
3085
|
+
"description": ""
|
|
3086
|
+
},
|
|
3087
|
+
{
|
|
3088
|
+
"prop": "tone",
|
|
3089
|
+
"type": "`string`",
|
|
3090
|
+
"default": "default, primary, secondary, success, warning, info, error or destructive. Sets the action's background; error / destructive also mark it destructive in the menu",
|
|
3091
|
+
"description": ""
|
|
3092
|
+
},
|
|
3093
|
+
{
|
|
3094
|
+
"prop": "disabled",
|
|
3095
|
+
"type": "`boolean`",
|
|
3096
|
+
"default": "Not selectable",
|
|
3097
|
+
"description": ""
|
|
3098
|
+
},
|
|
3099
|
+
{
|
|
3100
|
+
"prop": "onSelect",
|
|
3101
|
+
"type": "`({ action, source }) => void`",
|
|
3102
|
+
"default": "Runs the action. source is 'button' (a revealed action was tapped), 'full-swipe' or 'menu'. The row closes afterwards",
|
|
3103
|
+
"description": ""
|
|
3104
|
+
}
|
|
3105
|
+
],
|
|
3106
|
+
"examples": "### Inbox row\n\n```jsx\nimport { Archive, Trash2, Flag, MailOpen } from 'lucide-react';\n\n<ul className=\"divide-y divide-line\">\n {messages.map((m) => (\n <SwipeableListItem\n key={m.id}\n as=\"li\"\n leadingActions={[{ key: 'read', label: 'Read', icon: MailOpen, tone: 'primary', onSelect: () => markRead(m) }]}\n trailingActions={[\n { key: 'delete', label: 'Delete', icon: Trash2, tone: 'destructive', onSelect: () => remove(m) },\n { key: 'flag', label: 'Flag', icon: Flag, tone: 'warning', onSelect: () => flag(m) },\n { key: 'archive', label: 'Archive', icon: Archive, tone: 'success', onSelect: () => archive(m) },\n ]}\n >\n <div className=\"px-4 py-3\">{m.subject}</div>\n </SwipeableListItem>\n ))}\n</ul>\n```\n\n### One row open at a time\n\n```jsx\nconst [open, setOpen] = useState(null); // { id, side }\n\n<SwipeableListItem\n open={open?.id === m.id ? open.side : null}\n onOpenChange={(side) => setOpen(side ? { id: m.id, side } : null)}\n trailingActions={actions}\n>\n …\n</SwipeableListItem>\n```\n\n### In a Table\n\n`Table` builds its mobile cards on this component through the `swipeActions` prop; see [Table](Table.md#mobile-cards-filters-and-swipe-actions).\n\n[← Component overview](README.md)"
|
|
3107
|
+
},
|
|
3108
|
+
{
|
|
3109
|
+
"name": "SearchField",
|
|
3110
|
+
"group": "core",
|
|
3111
|
+
"familyExports": [],
|
|
3112
|
+
"importFrom": "@dreamtree-org/twreact-ui",
|
|
3113
|
+
"peerDependencies": [],
|
|
3114
|
+
"hasDoc": true,
|
|
3115
|
+
"description": "iOS-style search input: a `type=\"search\"` field with a leading search icon, a clear button, and a **Cancel** text button on phones while the field is focused. Optionally shows results: in a full-height `Sheet` with the field pinned at the top on phones, and in a popover beneath the field elsewhere.",
|
|
3116
|
+
"props": [
|
|
3117
|
+
{
|
|
3118
|
+
"prop": "value",
|
|
3119
|
+
"type": "`string`",
|
|
3120
|
+
"default": "",
|
|
3121
|
+
"description": "Controlled value. Pair with `onValueChange` (or `onChange`)"
|
|
3122
|
+
},
|
|
3123
|
+
{
|
|
3124
|
+
"prop": "defaultValue",
|
|
3125
|
+
"type": "`string`",
|
|
3126
|
+
"default": "\"\"",
|
|
3127
|
+
"description": "Uncontrolled initial value"
|
|
3128
|
+
},
|
|
3129
|
+
{
|
|
3130
|
+
"prop": "onValueChange",
|
|
3131
|
+
"type": "`(value: string) => void`",
|
|
3132
|
+
"default": "",
|
|
3133
|
+
"description": "Called with the new text on every change: typing, the clear button, Escape and Cancel. Recommended for controlled use"
|
|
3134
|
+
},
|
|
3135
|
+
{
|
|
3136
|
+
"prop": "onChange",
|
|
3137
|
+
"type": "`(event) => void`",
|
|
3138
|
+
"default": "",
|
|
3139
|
+
"description": "The input's change event while typing. Clear / Escape / Cancel call it with `{ target: { value: \"\" } }` (same convention as `Input`)"
|
|
3140
|
+
},
|
|
3141
|
+
{
|
|
3142
|
+
"prop": "onSearch",
|
|
3143
|
+
"type": "`(value: string) => void`",
|
|
3144
|
+
"default": "",
|
|
3145
|
+
"description": "Called with the current text on Enter (not during IME composition)"
|
|
3146
|
+
},
|
|
3147
|
+
{
|
|
3148
|
+
"prop": "onClear",
|
|
3149
|
+
"type": "`() => void`",
|
|
3150
|
+
"default": "",
|
|
3151
|
+
"description": "Called after the clear button (or Escape) empties the field"
|
|
3152
|
+
},
|
|
3153
|
+
{
|
|
3154
|
+
"prop": "onCancel",
|
|
3155
|
+
"type": "`() => void`",
|
|
3156
|
+
"default": "",
|
|
3157
|
+
"description": "Called after Cancel clears and blurs the field (and closes the results)"
|
|
3158
|
+
},
|
|
3159
|
+
{
|
|
3160
|
+
"prop": "showCancel",
|
|
3161
|
+
"type": "`boolean` | `\"auto\"`",
|
|
3162
|
+
"default": "\"auto\"",
|
|
3163
|
+
"description": "`\"auto\"`: Cancel shows on phones while the field is focused. `true`: always. `false`: never"
|
|
3164
|
+
},
|
|
3165
|
+
{
|
|
3166
|
+
"prop": "cancelLabel",
|
|
3167
|
+
"type": "`string`",
|
|
3168
|
+
"default": "\"Cancel\"",
|
|
3169
|
+
"description": "Cancel button text"
|
|
3170
|
+
},
|
|
3171
|
+
{
|
|
3172
|
+
"prop": "clearLabel",
|
|
3173
|
+
"type": "`string`",
|
|
3174
|
+
"default": "\"Clear search\"",
|
|
3175
|
+
"description": "Accessible name of the clear button"
|
|
3176
|
+
},
|
|
3177
|
+
{
|
|
3178
|
+
"prop": "placeholder",
|
|
3179
|
+
"type": "`string`",
|
|
3180
|
+
"default": "\"Search\"",
|
|
3181
|
+
"description": "Placeholder; also the default accessible name"
|
|
3182
|
+
},
|
|
3183
|
+
{
|
|
3184
|
+
"prop": "results",
|
|
3185
|
+
"type": "`ReactNode`",
|
|
3186
|
+
"default": "",
|
|
3187
|
+
"description": "Results content. Passing it (or `renderResults`) turns on the results surface"
|
|
3188
|
+
},
|
|
3189
|
+
{
|
|
3190
|
+
"prop": "renderResults",
|
|
3191
|
+
"type": "`({ value, close }) => ReactNode`",
|
|
3192
|
+
"default": "",
|
|
3193
|
+
"description": "Render function for results; `close()` hides them. Return `null` to show nothing (e.g. while `value` is empty)"
|
|
3194
|
+
},
|
|
3195
|
+
{
|
|
3196
|
+
"prop": "resultsOpen",
|
|
3197
|
+
"type": "`boolean`",
|
|
3198
|
+
"default": "",
|
|
3199
|
+
"description": "Controlled results visibility. Uncontrolled by default: opens on focus / typing (popover) or on tap / typing (sheet)"
|
|
3200
|
+
},
|
|
3201
|
+
{
|
|
3202
|
+
"prop": "onResultsOpenChange",
|
|
3203
|
+
"type": "`(open: boolean) => void`",
|
|
3204
|
+
"default": "",
|
|
3205
|
+
"description": "Called when the results want to open or close"
|
|
3206
|
+
},
|
|
3207
|
+
{
|
|
3208
|
+
"prop": "resultsLabel",
|
|
3209
|
+
"type": "`string`",
|
|
3210
|
+
"default": "\"Search results\"",
|
|
3211
|
+
"description": "Accessible name of the results region and of the results sheet"
|
|
3212
|
+
},
|
|
3213
|
+
{
|
|
3214
|
+
"prop": "presentation",
|
|
3215
|
+
"type": "`\"auto\"` | `\"popover\"` | `\"sheet\"`",
|
|
3216
|
+
"default": "\"auto\"",
|
|
3217
|
+
"description": "How results present. `\"auto\"`: a sheet on coarse-pointer screens under 640px, a popover elsewhere (popover on the server). Also decides whether `showCancel=\"auto\"` shows Cancel"
|
|
3218
|
+
},
|
|
3219
|
+
{
|
|
3220
|
+
"prop": "disabled",
|
|
3221
|
+
"type": "`boolean`",
|
|
3222
|
+
"default": "false",
|
|
3223
|
+
"description": "Disables the field; no clear or Cancel button"
|
|
3224
|
+
},
|
|
3225
|
+
{
|
|
3226
|
+
"prop": "readOnly",
|
|
3227
|
+
"type": "`boolean`",
|
|
3228
|
+
"default": "false",
|
|
3229
|
+
"description": "Read-only; no clear button"
|
|
3230
|
+
},
|
|
3231
|
+
{
|
|
3232
|
+
"prop": "className",
|
|
3233
|
+
"type": "`string`",
|
|
3234
|
+
"default": "",
|
|
3235
|
+
"description": "Classes for the root wrapper (merged with `cn`), e.g. a width"
|
|
3236
|
+
},
|
|
3237
|
+
{
|
|
3238
|
+
"prop": "inputClassName",
|
|
3239
|
+
"type": "`string`",
|
|
3240
|
+
"default": "",
|
|
3241
|
+
"description": "Classes for the `<input>` (merged with `cn`)"
|
|
3242
|
+
},
|
|
3243
|
+
{
|
|
3244
|
+
"prop": "aria-label",
|
|
3245
|
+
"type": "`string`",
|
|
3246
|
+
"default": "placeholder",
|
|
3247
|
+
"description": "Accessible name. Omitted when you pass `aria-labelledby`"
|
|
3248
|
+
},
|
|
3249
|
+
{
|
|
3250
|
+
"prop": "...rest",
|
|
3251
|
+
"type": "`InputHTMLAttributes`",
|
|
3252
|
+
"default": "",
|
|
3253
|
+
"description": "Forwarded to the `<input>` (`name`, `id`, `autoFocus`, `aria-*`, `data-*`, handlers)"
|
|
3254
|
+
}
|
|
3255
|
+
],
|
|
3256
|
+
"examples": "### Plain search\n\n```jsx\n<SearchField placeholder=\"Search invoices\" onSearch={(q) => router.push(`/invoices?q=${q}`)} className=\"max-w-sm\" />\n```\n\n### Always show Cancel (e.g. inside a search screen)\n\n```jsx\n<SearchField showCancel autoFocus onCancel={() => navigate(-1)} />\n```\n\n[← Component overview](README.md)"
|
|
3257
|
+
},
|
|
3258
|
+
{
|
|
3259
|
+
"name": "Sidebar",
|
|
3260
|
+
"group": "navigation",
|
|
3261
|
+
"familyExports": [],
|
|
3262
|
+
"importFrom": "@dreamtree-org/twreact-ui",
|
|
3263
|
+
"peerDependencies": [],
|
|
3264
|
+
"hasDoc": true,
|
|
3265
|
+
"description": "Collapsible side navigation with optional nested items, logo, and user block. Supports desktop collapsed state and mobile drawer. The content renders **once**: from `md` up it is a sticky in-flow column, below `md` the same tree is an off-canvas drawer. Tooltips show when collapsed; optional `drawerPosition` for mobile. The panel uses the semantic `bg-surface` / `border-line` / `text-foreground` tokens (section labels use `text-muted`), so it follows dark mode.",
|
|
3266
|
+
"props": [
|
|
3267
|
+
{
|
|
3268
|
+
"prop": "items",
|
|
3269
|
+
"type": "`Array`",
|
|
3270
|
+
"default": "[]",
|
|
3271
|
+
"description": "Nav items: `{ id, label, icon, href?, target?, rel?, children?, onClick?, active?, disabled?, badge?, badgeLabel?, group? }`. `children` = nested items."
|
|
3272
|
+
},
|
|
3273
|
+
{
|
|
3274
|
+
"prop": "collapsed",
|
|
3275
|
+
"type": "`boolean`",
|
|
3276
|
+
"default": "false",
|
|
3277
|
+
"description": "Desktop collapsed state (icon-only)."
|
|
3278
|
+
},
|
|
3279
|
+
{
|
|
3280
|
+
"prop": "onToggle",
|
|
3281
|
+
"type": "`() => void`",
|
|
3282
|
+
"default": "",
|
|
3283
|
+
"description": "Toggle collapse (e.g. chevron click)."
|
|
3284
|
+
},
|
|
3285
|
+
{
|
|
3286
|
+
"prop": "className",
|
|
3287
|
+
"type": "`string`",
|
|
3288
|
+
"default": "",
|
|
3289
|
+
"description": "Classes for the sidebar container inside the `<aside>` (the element `ref` points to). Applies to both the column and the drawer."
|
|
3290
|
+
},
|
|
3291
|
+
{
|
|
3292
|
+
"prop": "containerClassName",
|
|
3293
|
+
"type": "`string`",
|
|
3294
|
+
"default": "",
|
|
3295
|
+
"description": "Classes for the outer desktop sticky wrapper (default `sticky top-0 h-[100dvh]`; `display: contents` below `md`), merged with `tailwind-merge` — e.g. `\"h-full static\"` to size it from its parent."
|
|
3296
|
+
},
|
|
3297
|
+
{
|
|
3298
|
+
"prop": "stickyOffset",
|
|
3299
|
+
"type": "`number` | `string`",
|
|
3300
|
+
"default": "",
|
|
3301
|
+
"description": "Height of a top bar above the sidebar (number = px, or any CSS length such as `\"4rem\"`). The desktop sidebar sticks at that offset and its height becomes `calc(100dvh - offset)`, so a footer / user block stays on screen. Omit for the default full-viewport `100dvh`."
|
|
3302
|
+
},
|
|
3303
|
+
{
|
|
3304
|
+
"prop": "expandedIds",
|
|
3305
|
+
"type": "`string[]`",
|
|
3306
|
+
"default": "",
|
|
3307
|
+
"description": "Controlled open groups (parent item ids)."
|
|
3308
|
+
},
|
|
3309
|
+
{
|
|
3310
|
+
"prop": "defaultExpandedIds",
|
|
3311
|
+
"type": "`string[]`",
|
|
3312
|
+
"default": "[]",
|
|
3313
|
+
"description": "Initially open groups (uncontrolled)."
|
|
3314
|
+
},
|
|
3315
|
+
{
|
|
3316
|
+
"prop": "onExpandedChange",
|
|
3317
|
+
"type": "`(ids: string[]) => void`",
|
|
3318
|
+
"default": "",
|
|
3319
|
+
"description": "Called with the next open ids when a group is opened/closed (and, when controlled, to request opening the active item's ancestors)."
|
|
3320
|
+
},
|
|
3321
|
+
{
|
|
3322
|
+
"prop": "logo",
|
|
3323
|
+
"type": "`ReactNode`",
|
|
3324
|
+
"default": "",
|
|
3325
|
+
"description": "Logo at top."
|
|
3326
|
+
},
|
|
3327
|
+
{
|
|
3328
|
+
"prop": "footer",
|
|
3329
|
+
"type": "`ReactNode` | `({ collapsed, isMobile, closeMobile }) => ReactNode`",
|
|
3330
|
+
"default": "",
|
|
3331
|
+
"description": "Pinned region at the bottom, above the user block (e.g. Settings / Help). The function form lets it adapt to the collapsed rail and close the mobile drawer on click."
|
|
3332
|
+
},
|
|
3333
|
+
{
|
|
3334
|
+
"prop": "user",
|
|
3335
|
+
"type": "`object`",
|
|
3336
|
+
"default": "",
|
|
3337
|
+
"description": "User block at bottom: `{ name, email, avatar? }`."
|
|
3338
|
+
},
|
|
3339
|
+
{
|
|
3340
|
+
"prop": "onUserClick",
|
|
3341
|
+
"type": "`(user) => void`",
|
|
3342
|
+
"default": "",
|
|
3343
|
+
"description": "When set, the user block is a real `<button type=\"button\">` (Enter/Space work natively). Without it the block is static content."
|
|
3344
|
+
},
|
|
3345
|
+
{
|
|
3346
|
+
"prop": "drawerPosition",
|
|
3347
|
+
"type": "`\"left\"` | `\"right\"`",
|
|
3348
|
+
"default": "\"left\"",
|
|
3349
|
+
"description": "Mobile drawer side."
|
|
3350
|
+
},
|
|
3351
|
+
{
|
|
3352
|
+
"prop": "isMobileOpen",
|
|
3353
|
+
"type": "`boolean`",
|
|
3354
|
+
"default": "",
|
|
3355
|
+
"description": "Controlled mobile open."
|
|
3356
|
+
},
|
|
3357
|
+
{
|
|
3358
|
+
"prop": "setIsMobileOpen",
|
|
3359
|
+
"type": "`(open: boolean) => void`",
|
|
3360
|
+
"default": "",
|
|
3361
|
+
"description": "Set mobile open (when controlled)."
|
|
3362
|
+
},
|
|
3363
|
+
{
|
|
3364
|
+
"prop": "showCollapsedTooltips",
|
|
3365
|
+
"type": "`boolean`",
|
|
3366
|
+
"default": "true",
|
|
3367
|
+
"description": "Show tooltips when collapsed."
|
|
3368
|
+
},
|
|
3369
|
+
{
|
|
3370
|
+
"prop": "swipeable",
|
|
3371
|
+
"type": "`boolean`",
|
|
3372
|
+
"default": "true",
|
|
3373
|
+
"description": "Phones: swipe in from the screen edge (20px, the `drawerPosition` side) to open the drawer, and swipe the open drawer back toward its edge to close it. Needs `setIsMobileOpen`. `false` turns both gestures off."
|
|
3374
|
+
},
|
|
3375
|
+
{
|
|
3376
|
+
"prop": "tooltipOptions",
|
|
3377
|
+
"type": "`object`",
|
|
3378
|
+
"default": "",
|
|
3379
|
+
"description": "Options for internal Tooltip."
|
|
2852
3380
|
},
|
|
2853
3381
|
{
|
|
2854
3382
|
"prop": "linkAs",
|
|
@@ -2908,19 +3436,31 @@
|
|
|
2908
3436
|
"prop": "searchable",
|
|
2909
3437
|
"type": "`boolean`",
|
|
2910
3438
|
"default": "false",
|
|
2911
|
-
"description": "Show search input (
|
|
3439
|
+
"description": "Show the search input from `sm` up. Below `sm` a **Search** icon button opens a top `Sheet` holding a [`SearchField`](SearchField.md) instead (same term, same `onSearch`)."
|
|
2912
3440
|
},
|
|
2913
3441
|
{
|
|
2914
3442
|
"prop": "onSearch",
|
|
2915
3443
|
"type": "`(term: string) => void`",
|
|
2916
3444
|
"default": "",
|
|
2917
|
-
"description": "Called on search form submit
|
|
3445
|
+
"description": "Called with the current term on search form submit (desktop) or Enter in the phone search sheet (the sheet then closes)."
|
|
2918
3446
|
},
|
|
2919
3447
|
{
|
|
2920
|
-
"prop": "
|
|
2921
|
-
"type": "`
|
|
3448
|
+
"prop": "sticky",
|
|
3449
|
+
"type": "`boolean`",
|
|
2922
3450
|
"default": "false",
|
|
2923
|
-
"description": "
|
|
3451
|
+
"description": "Sticks the bar to the top (`sticky top-0 z-sticky`), pads it with the top safe area (`pt-safe`) and makes it translucent (`bg-surface/80 backdrop-blur` where `backdrop-filter` is supported; opaque `bg-surface` otherwise)."
|
|
3452
|
+
},
|
|
3453
|
+
{
|
|
3454
|
+
"prop": "largeTitle",
|
|
3455
|
+
"type": "`ReactNode`",
|
|
3456
|
+
"default": "",
|
|
3457
|
+
"description": "iOS large title: an `<h1>` under the bar that collapses into a compact centred title in the bar once the window scrolls past 44px (and expands again under 8px). Passive scroll listener, SSR-safe (expanded on the server), instant under `prefers-reduced-motion`."
|
|
3458
|
+
},
|
|
3459
|
+
{
|
|
3460
|
+
"prop": "leftIcon",
|
|
3461
|
+
"type": "`ReactNode` | `false`",
|
|
3462
|
+
"default": "<ChevronLeft />",
|
|
3463
|
+
"description": "Icon shown on mobile (e.g. back). Set `false` to hide."
|
|
2924
3464
|
},
|
|
2925
3465
|
{
|
|
2926
3466
|
"prop": "onLeftIconClick",
|
|
@@ -2974,13 +3514,37 @@
|
|
|
2974
3514
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
2975
3515
|
"peerDependencies": [],
|
|
2976
3516
|
"hasDoc": true,
|
|
2977
|
-
"description": "Fixed bottom navigation for mobile (hidden on `md` and up via `md:hidden`). Renders a row of items with icon and label; optional badge per item. Items
|
|
3517
|
+
"description": "Fixed bottom navigation for mobile (hidden on `md` and up via `md:hidden`): a native-feeling tab bar. Renders a row of items with icon and label; optional badge per item. Items are buttons (`onClick`) or links (`href` / `to`, rendered as `<a>` or your router `Link` via `linkAs`). The bar sits on the `z-sticky` layer, pads the bottom safe area (`pb-safe`, for the home indicator), and is a translucent `bg-surface/80` with backdrop blur where supported (solid `bg-surface` otherwise); borders use `border-line`, so it is dark-mode aware. Items share the width equally, long labels truncate (five tabs fit a 375px phone), and shrink slightly while pressed (off under reduced motion). Put a `<FootNav.Spacer />` at the end of the page content so nothing scrolls under the bar.",
|
|
2978
3518
|
"props": [
|
|
2979
3519
|
{
|
|
2980
3520
|
"prop": "items",
|
|
2981
3521
|
"type": "`Array`",
|
|
2982
3522
|
"default": "[]",
|
|
2983
|
-
"description": "Each item: `{ id, label, icon, onClick?, active?, badge?, variant?, 'aria-*'?, 'data-*'? }`. `variant: 'action'` renders a raised circular primary button (FAB); `aria-*` / `data-*` keys are passed to the item's button."
|
|
3523
|
+
"description": "Each item: `{ id, label, icon, onClick?, active?, badge?, href?, to?, variant?, 'aria-*'?, 'data-*'? }`. `variant: 'action'` renders a raised circular primary button (FAB); `href` / `to` render the item as a link; `aria-*` / `data-*` keys are passed to the item's button or link."
|
|
3524
|
+
},
|
|
3525
|
+
{
|
|
3526
|
+
"prop": "linkAs",
|
|
3527
|
+
"type": "`ElementType`",
|
|
3528
|
+
"default": "\"a\"",
|
|
3529
|
+
"description": "Component for items with `href` / `to`, e.g. a router `Link`. It receives `href` and `to` (`item.to ?? item.href`). Items without `href`/`to` stay `<button type=\"button\">`."
|
|
3530
|
+
},
|
|
3531
|
+
{
|
|
3532
|
+
"prop": "hideOnScroll",
|
|
3533
|
+
"type": "`boolean`",
|
|
3534
|
+
"default": "false",
|
|
3535
|
+
"description": "Slide the bar away while scrolling down and back on scroll up (passive listener, SSR-safe; instant under `prefers-reduced-motion`). Never hides within 64px of the top; keyboard focus entering the bar shows it. Sets `data-hidden` on the `<nav>`."
|
|
3536
|
+
},
|
|
3537
|
+
{
|
|
3538
|
+
"prop": "badgeMax",
|
|
3539
|
+
"type": "`number`",
|
|
3540
|
+
"default": "99",
|
|
3541
|
+
"description": "Numeric badges above this read `${badgeMax}+` (e.g. `99+`)."
|
|
3542
|
+
},
|
|
3543
|
+
{
|
|
3544
|
+
"prop": "aria-label",
|
|
3545
|
+
"type": "`string`",
|
|
3546
|
+
"default": "\"Primary\"",
|
|
3547
|
+
"description": "Accessible name of the `<nav>` landmark."
|
|
2984
3548
|
},
|
|
2985
3549
|
{
|
|
2986
3550
|
"prop": "className",
|
|
@@ -2992,7 +3556,7 @@
|
|
|
2992
3556
|
"prop": "...rest",
|
|
2993
3557
|
"type": "—",
|
|
2994
3558
|
"default": "",
|
|
2995
|
-
"description": "Passed to `<nav>`."
|
|
3559
|
+
"description": "Passed to `<nav>`. The ref is forwarded to it."
|
|
2996
3560
|
}
|
|
2997
3561
|
],
|
|
2998
3562
|
"examples": "```jsx\n<FootNav\n items={[\n { id: 'home', label: 'Home', icon: <Home />, onClick: () => navigate('/'), active: path === '/' },\n { id: 'search', label: 'Search', icon: <Search />, onClick: () => openSearch() },\n { id: 'notifications', label: 'Alerts', icon: <Bell />, badge: 3, onClick: () => openNotifications() },\n { id: 'profile', label: 'Profile', icon: <User />, onClick: () => navigate('/profile') },\n ]}\n/>\n```\n\n[← Component overview](README.md)"
|
|
@@ -3036,6 +3600,12 @@
|
|
|
3036
3600
|
"default": "",
|
|
3037
3601
|
"description": "Home click handler"
|
|
3038
3602
|
},
|
|
3603
|
+
{
|
|
3604
|
+
"prop": "maxItems",
|
|
3605
|
+
"type": "`number`",
|
|
3606
|
+
"default": "— (no collapse)",
|
|
3607
|
+
"description": "When the trail (Home included) has more crumbs than this, show the first crumb, an \"…\" button, and the last `maxItems - 1` crumbs. Clicking \"…\" expands the full trail and moves focus to the first revealed crumb. Values below 2 are treated as 2."
|
|
3608
|
+
},
|
|
3039
3609
|
{
|
|
3040
3610
|
"prop": "className",
|
|
3041
3611
|
"type": "`string`",
|
|
@@ -3043,7 +3613,7 @@
|
|
|
3043
3613
|
"description": "Extra CSS classes"
|
|
3044
3614
|
}
|
|
3045
3615
|
],
|
|
3046
|
-
"examples": "```jsx\n<Breadcrumbs\n items={[\n { id: '1', label: 'Products', href: '/products' },\n { id: '2', label: 'Category', href: '/products/cat' },\n { id: '3', label: 'Current Page' },\n ]}\n showHome\n/>\n```\n\n[← Component overview](README.md)"
|
|
3616
|
+
"examples": "```jsx\n<Breadcrumbs\n items={[\n { id: '1', label: 'Products', href: '/products' },\n { id: '2', label: 'Category', href: '/products/cat' },\n { id: '3', label: 'Current Page' },\n ]}\n showHome\n/>\n```\n\nCollapsed trail (Home / … / Board / Card):\n\n```jsx\n<Breadcrumbs maxItems={3} items={[\n { id: 'org', label: 'Org', href: '/o' },\n { id: 'team', label: 'Team', href: '/o/t' },\n { id: 'board', label: 'Board', href: '/o/t/b' },\n { id: 'card', label: 'Card' },\n]} />\n```\n\n[← Component overview](README.md)"
|
|
3047
3617
|
},
|
|
3048
3618
|
{
|
|
3049
3619
|
"name": "Dialog",
|
|
@@ -3052,7 +3622,7 @@
|
|
|
3052
3622
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
3053
3623
|
"peerDependencies": [],
|
|
3054
3624
|
"hasDoc": true,
|
|
3055
|
-
"description": "Modal dialog with title, body, optional footer, and configurable size. Supports escape key and backdrop click to close when dismissible.",
|
|
3625
|
+
"description": "Modal dialog with title, body, optional footer, and configurable size. Supports escape key and backdrop click to close when dismissible. On phones it presents as a bottom [Sheet](Sheet.md) by default (`presentation=\"auto\"`).",
|
|
3056
3626
|
"props": [
|
|
3057
3627
|
{
|
|
3058
3628
|
"prop": "isOpen",
|
|
@@ -3112,10 +3682,22 @@
|
|
|
3112
3682
|
"prop": "onOpen",
|
|
3113
3683
|
"type": "`function`",
|
|
3114
3684
|
"default": "",
|
|
3115
|
-
"description": "Called when dialog opens (
|
|
3685
|
+
"description": "Called when dialog opens, with `{ dialogRef }` (the panel node)"
|
|
3686
|
+
},
|
|
3687
|
+
{
|
|
3688
|
+
"prop": "presentation",
|
|
3689
|
+
"type": "`string`",
|
|
3690
|
+
"default": "\"auto\"",
|
|
3691
|
+
"description": "`auto`, `popover`, `sheet`. `popover` = the centred modal; `sheet` = a bottom sheet; `auto` = sheet on coarse-pointer screens under 640px, centred modal elsewhere (and on the server)"
|
|
3692
|
+
},
|
|
3693
|
+
{
|
|
3694
|
+
"prop": "ref",
|
|
3695
|
+
"type": "`Ref<HTMLDivElement>`",
|
|
3696
|
+
"default": "",
|
|
3697
|
+
"description": "Forwarded to the panel (`role=\"dialog\"`) in both presentations"
|
|
3116
3698
|
}
|
|
3117
3699
|
],
|
|
3118
|
-
"examples": "### Basic\n\n```jsx\nconst [open, setOpen] = useState(false);\n\n<Button onClick={() => setOpen(true)}>Open</Button>\n<Dialog\n isOpen={open}\n onClose={() => setOpen(false)}\n title=\"Confirm\"\n>\n Are you sure you want to continue?\n</Dialog>\n```\n\n### With footer\n\n```jsx\n<Dialog\n isOpen={open}\n onClose={() => setOpen(false)}\n title=\"Edit\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={() => setOpen(false)}>Cancel</Button>\n <Button onClick={handleSave}>Save</Button>\n </>\n }\n>\n <Input label=\"Name\" value={name} onChange={e => setName(e.target.value)} />\n</Dialog>\n```"
|
|
3700
|
+
"examples": "### Basic\n\n```jsx\nconst [open, setOpen] = useState(false);\n\n<Button onClick={() => setOpen(true)}>Open</Button>\n<Dialog\n isOpen={open}\n onClose={() => setOpen(false)}\n title=\"Confirm\"\n>\n Are you sure you want to continue?\n</Dialog>\n```\n\n### Always centred / always a sheet\n\n```jsx\n<Dialog presentation=\"popover\" isOpen={open} onClose={close} title=\"Centred everywhere\">\n Same centred modal on phones too.\n</Dialog>\n\n<Dialog presentation=\"sheet\" isOpen={open} onClose={close} title=\"Sheet everywhere\">\n A bottom sheet on desktop too.\n</Dialog>\n```\n\n### With footer\n\n```jsx\n<Dialog\n isOpen={open}\n onClose={() => setOpen(false)}\n title=\"Edit\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={() => setOpen(false)}>Cancel</Button>\n <Button onClick={handleSave}>Save</Button>\n </>\n }\n>\n <Input label=\"Name\" value={name} onChange={e => setName(e.target.value)} />\n</Dialog>\n```"
|
|
3119
3701
|
},
|
|
3120
3702
|
{
|
|
3121
3703
|
"name": "Toast",
|
|
@@ -3128,7 +3710,7 @@
|
|
|
3128
3710
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
3129
3711
|
"peerDependencies": [],
|
|
3130
3712
|
"hasDoc": true,
|
|
3131
|
-
"description": "Toast notification component, container and app-level provider. Wrap your app in `ToastProvider` and call `useToast()` anywhere below it — every caller shares one queue and the provider renders it. Without a provider, `useToast` keeps a component-local queue that you render with `ToastContainer` (the original behavior). Toasts have a status
|
|
3713
|
+
"description": "Toast notification component, container and app-level provider. Wrap your app in `ToastProvider` and call `useToast()` anywhere below it — every caller shares one queue and the provider renders it. Without a provider, `useToast` keeps a component-local queue that you render with `ToastContainer` (the original behavior). Toasts have a status icon, message body, action buttons and a bottom progress bar; they swipe away horizontally, pause while hovered, focused or touched, sit bottom-center above the FootNav on phones, and collapse into a Sonner-style stack past three.",
|
|
3132
3714
|
"props": [
|
|
3133
3715
|
{
|
|
3134
3716
|
"prop": "id",
|
|
@@ -3140,7 +3722,7 @@
|
|
|
3140
3722
|
"prop": "title",
|
|
3141
3723
|
"type": "`string`",
|
|
3142
3724
|
"default": "",
|
|
3143
|
-
"description": "Toast title
|
|
3725
|
+
"description": "Toast title"
|
|
3144
3726
|
},
|
|
3145
3727
|
{
|
|
3146
3728
|
"prop": "message",
|
|
@@ -3166,29 +3748,23 @@
|
|
|
3166
3748
|
"default": "5000",
|
|
3167
3749
|
"description": "Auto-close timer in ms (0 = no auto-close)"
|
|
3168
3750
|
},
|
|
3169
|
-
{
|
|
3170
|
-
"prop": "appIcon",
|
|
3171
|
-
"type": "`ReactNode`",
|
|
3172
|
-
"default": "<Sparkles />",
|
|
3173
|
-
"description": "Icon displayed in header next to status icon badge"
|
|
3174
|
-
},
|
|
3175
3751
|
{
|
|
3176
3752
|
"prop": "statusIcon",
|
|
3177
3753
|
"type": "`ReactNode`",
|
|
3178
3754
|
"default": "",
|
|
3179
|
-
"description": "Custom status icon
|
|
3755
|
+
"description": "Custom status icon (overrides the icon for `type`)"
|
|
3180
3756
|
},
|
|
3181
3757
|
{
|
|
3182
3758
|
"prop": "actions",
|
|
3183
3759
|
"type": "`Array`",
|
|
3184
3760
|
"default": "[]",
|
|
3185
|
-
"description": "Action button definitions `[{ label, onClick, variant, dismiss }]`"
|
|
3761
|
+
"description": "Action button definitions `[{ label, onClick, variant: 'primary' or 'secondary', dismiss, className }]`"
|
|
3186
3762
|
},
|
|
3187
3763
|
{
|
|
3188
3764
|
"prop": "onClose",
|
|
3189
3765
|
"type": "`function`",
|
|
3190
3766
|
"default": "",
|
|
3191
|
-
"description": "Called
|
|
3767
|
+
"description": "Called with `id` once the toast has closed (after its exit animation)"
|
|
3192
3768
|
},
|
|
3193
3769
|
{
|
|
3194
3770
|
"prop": "position",
|
|
@@ -3200,10 +3776,64 @@
|
|
|
3200
3776
|
"prop": "portal",
|
|
3201
3777
|
"type": "`boolean`",
|
|
3202
3778
|
"default": "true",
|
|
3203
|
-
"description": "
|
|
3779
|
+
"description": "Render through a portal at a fixed position (`ToastContainer` sets `false`)"
|
|
3780
|
+
},
|
|
3781
|
+
{
|
|
3782
|
+
"prop": "paused",
|
|
3783
|
+
"type": "`boolean`",
|
|
3784
|
+
"default": "false",
|
|
3785
|
+
"description": "Pause the auto-dismiss timer from outside (the stack does this while the user has it expanded)"
|
|
3786
|
+
},
|
|
3787
|
+
{
|
|
3788
|
+
"prop": "swipeToDismiss",
|
|
3789
|
+
"type": "`boolean`",
|
|
3790
|
+
"default": "true",
|
|
3791
|
+
"description": "A horizontal swipe past 64px, or a quick flick, dismisses the toast. Vertical page scroll is untouched (`touch-action: pan-y`)"
|
|
3792
|
+
},
|
|
3793
|
+
{
|
|
3794
|
+
"prop": "mobilePosition",
|
|
3795
|
+
"type": "`string` or `false`",
|
|
3796
|
+
"default": "\"bottom-center\"",
|
|
3797
|
+
"description": "`bottom-center` or `top-center` on phones (coarse pointer, under 640px): full width inside a 1rem gutter. `false` keeps `position`. Applies to a portaled toast"
|
|
3798
|
+
},
|
|
3799
|
+
{
|
|
3800
|
+
"prop": "mobileOffset",
|
|
3801
|
+
"type": "`number` or `string`",
|
|
3802
|
+
"default": "\"5rem\"",
|
|
3803
|
+
"description": "Gap above the bottom edge on phones (px or any CSS length), plus the safe area. Sets `--dt-toast-mobile-offset`; the default clears the FootNav"
|
|
3804
|
+
},
|
|
3805
|
+
{
|
|
3806
|
+
"prop": "closeLabel",
|
|
3807
|
+
"type": "`string`",
|
|
3808
|
+
"default": "\"Close notification\"",
|
|
3809
|
+
"description": "Accessible name of the close button"
|
|
3810
|
+
},
|
|
3811
|
+
{
|
|
3812
|
+
"prop": "color",
|
|
3813
|
+
"type": "`string`",
|
|
3814
|
+
"default": "",
|
|
3815
|
+
"description": "Custom progress-bar colour (any CSS colour)"
|
|
3816
|
+
},
|
|
3817
|
+
{
|
|
3818
|
+
"prop": "footerNote",
|
|
3819
|
+
"type": "`ReactNode`",
|
|
3820
|
+
"default": "",
|
|
3821
|
+
"description": "Footer text under the body (shown while `duration > 0`)"
|
|
3822
|
+
},
|
|
3823
|
+
{
|
|
3824
|
+
"prop": "showFooterNote",
|
|
3825
|
+
"type": "`boolean`",
|
|
3826
|
+
"default": "false",
|
|
3827
|
+
"description": "Show the default \"This message will close in N seconds. Click to stop.\" footer"
|
|
3828
|
+
},
|
|
3829
|
+
{
|
|
3830
|
+
"prop": "className",
|
|
3831
|
+
"type": "`string`",
|
|
3832
|
+
"default": "",
|
|
3833
|
+
"description": "Classes merged onto the toast root"
|
|
3204
3834
|
}
|
|
3205
3835
|
],
|
|
3206
|
-
"examples": "### App-level provider (recommended)\n\n```jsx\n// main.jsx\n<ToastProvider position=\"top-right\">\n <App />\n</ToastProvider>\n\n// anywhere below it\nfunction SaveButton() {\n const { toast } = useToast();\n return <Button onClick={() => toast.success('Saved', 'Your changes are saved.')}>Save</Button>;\n}\n```\n\n### Standalone (no provider)\n\n```jsx\nfunction App() {\n const { toast, toasts, removeToast } = useToast();\n\n const handleShowToast = () => {\n toast.info('System Update', 'A new update is available for download.', {\n duration: 6000,\n actions: [\n { label: 'Update Now', variant: 'primary', onClick: () => alert('Updating...') },\n { label: 'Later', variant: 'secondary', onClick: () => alert('Postponed') },\n ],\n });\n };\n\n return (\n <>\n <Button onClick={handleShowToast}>\n Show Notification\n </Button>\n <ToastContainer toasts={toasts} onRemove={removeToast} position=\"top-right\" />\n </>\n );\n}\n```\n\n[← Component overview](README.md)"
|
|
3836
|
+
"examples": "### App-level provider (recommended)\n\n```jsx\n// main.jsx\n<ToastProvider position=\"top-right\">\n <App />\n</ToastProvider>\n\n// anywhere below it\nfunction SaveButton() {\n const { toast } = useToast();\n return <Button onClick={() => toast.success('Saved', 'Your changes are saved.')}>Save</Button>;\n}\n```\n\n### Stack and phone options\n\n```jsx\n// Always list every toast; on phones sit 1rem above the bottom (no FootNav).\n<ToastProvider position=\"bottom-right\" expand mobileOffset={16}>\n <App />\n</ToastProvider>\n\n// Collapse as soon as two toasts are showing, and keep the desktop corner on phones.\n<ToastProvider expand={false} mobilePosition={false}>\n <App />\n</ToastProvider>\n```\n\n### Standalone (no provider)\n\n```jsx\nfunction App() {\n const { toast, toasts, removeToast } = useToast();\n\n const handleShowToast = () => {\n toast.info('System Update', 'A new update is available for download.', {\n duration: 6000,\n actions: [\n { label: 'Update Now', variant: 'primary', onClick: () => alert('Updating...') },\n { label: 'Later', variant: 'secondary', onClick: () => alert('Postponed') },\n ],\n });\n };\n\n return (\n <>\n <Button onClick={handleShowToast}>\n Show Notification\n </Button>\n <ToastContainer toasts={toasts} onRemove={removeToast} position=\"top-right\" />\n </>\n );\n}\n```\n\n[← Component overview](README.md)"
|
|
3207
3837
|
},
|
|
3208
3838
|
{
|
|
3209
3839
|
"name": "Alert",
|
|
@@ -3212,13 +3842,13 @@
|
|
|
3212
3842
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
3213
3843
|
"peerDependencies": [],
|
|
3214
3844
|
"hasDoc": true,
|
|
3215
|
-
"description": "Inline alert box for success, error, warning, or
|
|
3845
|
+
"description": "Inline alert box for success, error, warning, info or neutral messages. Optional title, an actions slot after the description, and a dismiss button. Every variant has a dark tone, and the alert fades in (off under `prefers-reduced-motion`).",
|
|
3216
3846
|
"props": [
|
|
3217
3847
|
{
|
|
3218
3848
|
"prop": "variant",
|
|
3219
3849
|
"type": "`string`",
|
|
3220
3850
|
"default": "\"info\"",
|
|
3221
|
-
"description": "`success`, `error`, `warning`, `info`"
|
|
3851
|
+
"description": "`success`, `error`, `warning`, `info`, `neutral` (role tokens: surface, line, muted). Unknown values fall back to `info`"
|
|
3222
3852
|
},
|
|
3223
3853
|
{
|
|
3224
3854
|
"prop": "title",
|
|
@@ -3230,7 +3860,13 @@
|
|
|
3230
3860
|
"prop": "children",
|
|
3231
3861
|
"type": "`ReactNode`",
|
|
3232
3862
|
"default": "",
|
|
3233
|
-
"description": "Alert body content"
|
|
3863
|
+
"description": "Alert body content (the description)"
|
|
3864
|
+
},
|
|
3865
|
+
{
|
|
3866
|
+
"prop": "actions",
|
|
3867
|
+
"type": "`ReactNode`",
|
|
3868
|
+
"default": "",
|
|
3869
|
+
"description": "Rendered after the description in a wrapping row (buttons, links)"
|
|
3234
3870
|
},
|
|
3235
3871
|
{
|
|
3236
3872
|
"prop": "dismissible",
|
|
@@ -3244,6 +3880,12 @@
|
|
|
3244
3880
|
"default": "",
|
|
3245
3881
|
"description": "Called when dismissed"
|
|
3246
3882
|
},
|
|
3883
|
+
{
|
|
3884
|
+
"prop": "dismissLabel",
|
|
3885
|
+
"type": "`string`",
|
|
3886
|
+
"default": "\"Dismiss\"",
|
|
3887
|
+
"description": "Accessible name of the dismiss button"
|
|
3888
|
+
},
|
|
3247
3889
|
{
|
|
3248
3890
|
"prop": "className",
|
|
3249
3891
|
"type": "`string`",
|
|
@@ -3251,7 +3893,7 @@
|
|
|
3251
3893
|
"description": "Extra CSS classes"
|
|
3252
3894
|
}
|
|
3253
3895
|
],
|
|
3254
|
-
"examples": "```jsx\n<Alert variant=\"info\" title=\"Note\">Your session will expire in 5 minutes.</Alert>\n<Alert variant=\"success\">Saved successfully.</Alert>\n<Alert variant=\"error\" title=\"Error\">Something went wrong.</Alert>\n<Alert variant=\"warning\" dismissible onDismiss={() => {}}>\n Please review your input.\n</Alert>\n```\n\n[← Component overview](README.md)"
|
|
3896
|
+
"examples": "```jsx\n<Alert variant=\"info\" title=\"Note\">Your session will expire in 5 minutes.</Alert>\n<Alert variant=\"success\">Saved successfully.</Alert>\n<Alert variant=\"error\" title=\"Error\">Something went wrong.</Alert>\n<Alert variant=\"warning\" dismissible onDismiss={() => {}}>\n Please review your input.\n</Alert>\n<Alert variant=\"neutral\" title=\"Heads up\">Billing moves to the 1st of each month.</Alert>\n\n<Alert\n variant=\"error\"\n title=\"Upload failed\"\n actions={\n <>\n <Button size=\"sm\" onClick={retry}>Retry</Button>\n <Button size=\"sm\" variant=\"ghost\" onClick={openHelp}>Get help</Button>\n </>\n }\n>\n The file is larger than 10 MB.\n</Alert>\n```\n\n[← Component overview](README.md)"
|
|
3255
3897
|
},
|
|
3256
3898
|
{
|
|
3257
3899
|
"name": "Drawer",
|
|
@@ -3272,7 +3914,7 @@
|
|
|
3272
3914
|
"prop": "onClose",
|
|
3273
3915
|
"type": "`function`",
|
|
3274
3916
|
"default": "",
|
|
3275
|
-
"description": "Called on Escape, backdrop click
|
|
3917
|
+
"description": "Called with no arguments on Escape, backdrop click, the close button or a swipe (required)"
|
|
3276
3918
|
},
|
|
3277
3919
|
{
|
|
3278
3920
|
"prop": "title",
|
|
@@ -3302,58 +3944,250 @@
|
|
|
3302
3944
|
"prop": "size",
|
|
3303
3945
|
"type": "`string`",
|
|
3304
3946
|
"default": "\"md\"",
|
|
3305
|
-
"description": "`sm`, `md`, `lg`, `xl`, `full` — width for left/right
|
|
3947
|
+
"description": "`sm`, `md`, `lg`, `xl`, `full` — max width for left/right (`max-w-xs` … `max-w-full`); for top/bottom a max height in dynamic viewport units: `30dvh`, `50dvh`, `75dvh`, `90dvh`, `full` = `100dvh`"
|
|
3948
|
+
},
|
|
3949
|
+
{
|
|
3950
|
+
"prop": "dismissible",
|
|
3951
|
+
"type": "`boolean`",
|
|
3952
|
+
"default": "true",
|
|
3953
|
+
"description": "Allow close via Escape and backdrop click"
|
|
3954
|
+
},
|
|
3955
|
+
{
|
|
3956
|
+
"prop": "showCloseButton",
|
|
3957
|
+
"type": "`boolean`",
|
|
3958
|
+
"default": "true",
|
|
3959
|
+
"description": "Show the X button (44×44px touch target on coarse pointers)"
|
|
3960
|
+
},
|
|
3961
|
+
{
|
|
3962
|
+
"prop": "footer",
|
|
3963
|
+
"type": "`ReactNode`",
|
|
3964
|
+
"default": "",
|
|
3965
|
+
"description": "Footer content (e.g. buttons), right-aligned"
|
|
3966
|
+
},
|
|
3967
|
+
{
|
|
3968
|
+
"prop": "initialFocusRef",
|
|
3969
|
+
"type": "`RefObject`",
|
|
3970
|
+
"default": "",
|
|
3971
|
+
"description": "Element to focus on open (default: first focusable element)"
|
|
3306
3972
|
},
|
|
3307
3973
|
{
|
|
3308
|
-
"prop": "
|
|
3974
|
+
"prop": "handle",
|
|
3975
|
+
"type": "`boolean`",
|
|
3976
|
+
"default": "true for placement=\"bottom\", else false",
|
|
3977
|
+
"description": "Draws a decorative, `aria-hidden` grab handle (at the bottom edge for `placement=\"top\"`, else at the top)"
|
|
3978
|
+
},
|
|
3979
|
+
{
|
|
3980
|
+
"prop": "swipeToClose",
|
|
3981
|
+
"type": "`boolean`",
|
|
3982
|
+
"default": "false",
|
|
3983
|
+
"description": "Any placement: dragging the handle or header toward the drawer's own edge (down for bottom, up for top, right for right, left for left) past 80px (or half the panel, if smaller), or a quick flick, calls `onClose`. A shorter drag springs back. Ignored when `dismissible` is `false`. The scrolling body never starts a swipe"
|
|
3984
|
+
},
|
|
3985
|
+
{
|
|
3986
|
+
"prop": "className",
|
|
3987
|
+
"type": "`string`",
|
|
3988
|
+
"default": "",
|
|
3989
|
+
"description": "Extra classes for the panel (merged with `cn`)"
|
|
3990
|
+
},
|
|
3991
|
+
{
|
|
3992
|
+
"prop": "...rest",
|
|
3993
|
+
"type": "`HTMLAttributes`",
|
|
3994
|
+
"default": "",
|
|
3995
|
+
"description": "Forwarded to the panel (`aria-label`, `data-*`, …); `ref` points at the panel"
|
|
3996
|
+
}
|
|
3997
|
+
],
|
|
3998
|
+
"examples": "### Basic\n\n```jsx\nconst [open, setOpen] = useState(false);\n\n<Button onClick={() => setOpen(true)}>Edit task</Button>\n<Drawer\n isOpen={open}\n onClose={() => setOpen(false)}\n title=\"Edit task\"\n description=\"Changes are saved when you press Save.\"\n footer={\n <>\n <Button variant=\"outline\" onClick={() => setOpen(false)}>Cancel</Button>\n <Button onClick={save}>Save</Button>\n </>\n }\n>\n <Input label=\"Title\" value={title} onChange={(e) => setTitle(e.target.value)} />\n</Drawer>\n```\n\n### Bottom sheet (phone)\n\n`placement=\"bottom\"` renders a phone-style sheet: rounded top corners, a grab handle, and bottom padding for the safe area (`env(safe-area-inset-bottom)`, so add `viewport-fit=cover` to your viewport meta). Add `swipeToClose` to let users drag it down to dismiss. Escape, backdrop click and the focus trap work as usual.\n\n```jsx\n<Drawer isOpen={open} onClose={close} title=\"Collect payment\" placement=\"bottom\" size=\"lg\" swipeToClose>\n …\n</Drawer>\n```\n\n### Composed (task peek)\n\n```jsx\n<Drawer isOpen={open} onClose={close} size=\"lg\" aria-label=\"Task peek\">\n <Drawer.Header><h2 className=\"text-lg font-semibold\">{task.title}</h2></Drawer.Header>\n <Drawer.Body>{task.description}</Drawer.Body>\n <Drawer.Footer><Button onClick={close}>Done</Button></Drawer.Footer>\n</Drawer>\n```"
|
|
3999
|
+
},
|
|
4000
|
+
{
|
|
4001
|
+
"name": "Sheet",
|
|
4002
|
+
"group": "feedback",
|
|
4003
|
+
"familyExports": [],
|
|
4004
|
+
"importFrom": "@dreamtree-org/twreact-ui",
|
|
4005
|
+
"peerDependencies": [],
|
|
4006
|
+
"hasDoc": true,
|
|
4007
|
+
"description": "Phone-native sheet that slides in from an edge: bottom (default), top, left or right. Bottom and top sheets can rest on **snap points** (e.g. half and full height), and every sheet can be **dragged to dismiss** with a rubber band past its limits. On phones the bottom sheet stays above the on-screen keyboard. Use `Sheet` for phone sheets (filters, pickers, quick forms, activity feeds); keep `Drawer` for desktop record peeks and editors.",
|
|
4008
|
+
"props": [
|
|
4009
|
+
{
|
|
4010
|
+
"prop": "open",
|
|
4011
|
+
"type": "`boolean`",
|
|
4012
|
+
"default": "",
|
|
4013
|
+
"description": "Controls visibility"
|
|
4014
|
+
},
|
|
4015
|
+
{
|
|
4016
|
+
"prop": "isOpen",
|
|
4017
|
+
"type": "`boolean`",
|
|
4018
|
+
"default": "",
|
|
4019
|
+
"description": "Alias of `open` (same name as `Drawer` / `Dialog`)"
|
|
4020
|
+
},
|
|
4021
|
+
{
|
|
4022
|
+
"prop": "onClose",
|
|
4023
|
+
"type": "`(reason) => void`",
|
|
4024
|
+
"default": "",
|
|
4025
|
+
"description": "Called when the sheet asks to close; `reason` is `'escape'`, `'backdrop'`, `'drag'` or `'close-button'`"
|
|
4026
|
+
},
|
|
4027
|
+
{
|
|
4028
|
+
"prop": "placement",
|
|
4029
|
+
"type": "`string`",
|
|
4030
|
+
"default": "\"bottom\"",
|
|
4031
|
+
"description": "`bottom`, `top`, `left`, `right`"
|
|
4032
|
+
},
|
|
4033
|
+
{
|
|
4034
|
+
"prop": "snapPoints",
|
|
4035
|
+
"type": "`number[]`",
|
|
4036
|
+
"default": "",
|
|
4037
|
+
"description": "Bottom/top only. Heights as fractions of the visual viewport height, each in `(0, 1]`, e.g. `[0.5, 1]`. Without them the sheet takes its content height (capped below the top safe area)"
|
|
4038
|
+
},
|
|
4039
|
+
{
|
|
4040
|
+
"prop": "activeSnap",
|
|
4041
|
+
"type": "`number`",
|
|
4042
|
+
"default": "",
|
|
4043
|
+
"description": "Controlled snap index into `snapPoints`"
|
|
4044
|
+
},
|
|
4045
|
+
{
|
|
4046
|
+
"prop": "defaultSnap",
|
|
4047
|
+
"type": "`number`",
|
|
4048
|
+
"default": "0",
|
|
4049
|
+
"description": "Uncontrolled initial snap index; restored each time the sheet opens"
|
|
4050
|
+
},
|
|
4051
|
+
{
|
|
4052
|
+
"prop": "onSnapChange",
|
|
4053
|
+
"type": "`(index) => void`",
|
|
4054
|
+
"default": "",
|
|
4055
|
+
"description": "Called with the new snap index after a drag settles or an arrow key on the handle"
|
|
4056
|
+
},
|
|
4057
|
+
{
|
|
4058
|
+
"prop": "haptics",
|
|
4059
|
+
"type": "`boolean`",
|
|
4060
|
+
"default": "false",
|
|
4061
|
+
"description": "Opt-in: a `selection` vibration (10ms) each time a drag or arrow key settles on a new snap point (not when you change `activeSnap` yourself). No-op without `navigator.vibrate` (iOS Safari) and under reduced motion. See [useHaptics](useHaptics.md)"
|
|
4062
|
+
},
|
|
4063
|
+
{
|
|
4064
|
+
"prop": "dismissible",
|
|
4065
|
+
"type": "`boolean`",
|
|
4066
|
+
"default": "true",
|
|
4067
|
+
"description": "`false` disables Escape, backdrop click and drag dismissal; a drag rubber-bands back"
|
|
4068
|
+
},
|
|
4069
|
+
{
|
|
4070
|
+
"prop": "handle",
|
|
4071
|
+
"type": "`boolean`",
|
|
4072
|
+
"default": "true for placement=\"bottom\", else false",
|
|
4073
|
+
"description": "Grab handle. With `snapPoints` it is a focusable \"Resize sheet\" button (ArrowUp / ArrowDown); otherwise decorative and `aria-hidden`"
|
|
4074
|
+
},
|
|
4075
|
+
{
|
|
4076
|
+
"prop": "scaleBackground",
|
|
4077
|
+
"type": "`boolean`",
|
|
4078
|
+
"default": "false",
|
|
4079
|
+
"description": "While open, scales and rounds the element marked `data-sheet-background` (your app wrapper). Does nothing under reduced motion"
|
|
4080
|
+
},
|
|
4081
|
+
{
|
|
4082
|
+
"prop": "title",
|
|
4083
|
+
"type": "`ReactNode`",
|
|
4084
|
+
"default": "",
|
|
4085
|
+
"description": "Rendered in an automatic `Sheet.Header`; labels the dialog (`aria-labelledby`)"
|
|
4086
|
+
},
|
|
4087
|
+
{
|
|
4088
|
+
"prop": "description",
|
|
4089
|
+
"type": "`ReactNode`",
|
|
4090
|
+
"default": "",
|
|
4091
|
+
"description": "Rendered under the title; wired to `aria-describedby`"
|
|
4092
|
+
},
|
|
4093
|
+
{
|
|
4094
|
+
"prop": "showCloseButton",
|
|
4095
|
+
"type": "`boolean`",
|
|
4096
|
+
"default": "true",
|
|
4097
|
+
"description": "Close (X) button in the automatic header (only rendered with `title` / `description`)"
|
|
4098
|
+
},
|
|
4099
|
+
{
|
|
4100
|
+
"prop": "initialFocus",
|
|
4101
|
+
"type": "`RefObject`",
|
|
4102
|
+
"default": "",
|
|
4103
|
+
"description": "Element to focus on open (default: first focusable element, skipping the handle)"
|
|
4104
|
+
},
|
|
4105
|
+
{
|
|
4106
|
+
"prop": "children",
|
|
4107
|
+
"type": "`ReactNode`",
|
|
4108
|
+
"default": "",
|
|
4109
|
+
"description": "Usually `Sheet.Header` / `Sheet.Body` / `Sheet.Footer`"
|
|
4110
|
+
},
|
|
4111
|
+
{
|
|
4112
|
+
"prop": "className",
|
|
4113
|
+
"type": "`string`",
|
|
4114
|
+
"default": "",
|
|
4115
|
+
"description": "Extra classes for the panel (merged with `cn`)"
|
|
4116
|
+
},
|
|
4117
|
+
{
|
|
4118
|
+
"prop": "...rest",
|
|
4119
|
+
"type": "`HTMLAttributes`",
|
|
4120
|
+
"default": "",
|
|
4121
|
+
"description": "Forwarded to the panel (`aria-label`, `data-*`, …); `ref` points at the panel"
|
|
4122
|
+
}
|
|
4123
|
+
],
|
|
4124
|
+
"examples": "### Bottom sheet\n\n```jsx\nconst [open, setOpen] = useState(false);\n\n<Button onClick={() => setOpen(true)}>Filters</Button>\n<Sheet open={open} onClose={() => setOpen(false)} title=\"Filters\" description=\"Narrow the task list\">\n <Sheet.Body>…</Sheet.Body>\n <Sheet.Footer>\n <Button variant=\"outline\" onClick={reset}>Reset</Button>\n <Button onClick={apply}>Apply</Button>\n </Sheet.Footer>\n</Sheet>\n```\n\n### Snap points\n\n```jsx\n// Uncontrolled: opens at half height.\n<Sheet open={open} onClose={close} title=\"Activity\" snapPoints={[0.5, 1]} defaultSnap={0}>\n <Sheet.Body>{comments}</Sheet.Body>\n</Sheet>\n\n// Controlled.\nconst [snap, setSnap] = useState(0);\n<Sheet open={open} onClose={close} title=\"Activity\" snapPoints={[0.5, 1]} activeSnap={snap} onSnapChange={setSnap}>\n …\n</Sheet>\n```\n\nA drag settles on the nearest snap point. A fast downward flick (faster than 0.5 px/ms) or a drag well below the lowest point dismisses the sheet when `dismissible` is on.\n\n### Side sheet\n\n```jsx\n<Sheet open={open} onClose={close} placement=\"left\" title=\"Navigation\">\n <Sheet.Body>…</Sheet.Body>\n</Sheet>\n```\n\nSide sheets are full height (`h-dvh`), `min(88vw, 24rem)` wide, and drag horizontally toward their own edge.\n\n### Form inside (keyboard-aware)\n\n```jsx\n<Sheet open={open} onClose={close} title=\"Collect payment\">\n <Sheet.Body>\n <Input label=\"Amount\" inputMode=\"decimal\" />\n </Sheet.Body>\n <Sheet.Footer><Button onClick={save}>Record payment</Button></Sheet.Footer>\n</Sheet>\n```\n\nWhen `window.visualViewport` is available, the bottom sheet listens to its `resize` and sits above the on-screen keyboard (its max height shrinks to the visible area), and a focused field stays scrolled into view.\n\n### Background scale\n\n```jsx\n<div data-sheet-background>\n <App />\n</div>\n\n<Sheet open={open} onClose={close} title=\"Share\" scaleBackground>…</Sheet>\n```\n\nOpt-in, because scaling the wrapper breaks `position: fixed` descendants inside it."
|
|
4125
|
+
},
|
|
4126
|
+
{
|
|
4127
|
+
"name": "ActionSheet",
|
|
4128
|
+
"group": "feedback",
|
|
4129
|
+
"familyExports": [],
|
|
4130
|
+
"importFrom": "@dreamtree-org/twreact-ui",
|
|
4131
|
+
"peerDependencies": [],
|
|
4132
|
+
"hasDoc": true,
|
|
4133
|
+
"description": "iOS-style action sheet (the `UIAlertController` action-sheet pattern) on the bottom `Sheet`: an optional title and message, a card of actions — destructive ones in the error tone — and a separate Cancel button. Use it for \"what do you want to do with this?\" choices on phones; it is the same implementation `ThreeDotPopover` and `Table`'s row menu use for their phone presentation.",
|
|
4134
|
+
"props": [
|
|
4135
|
+
{
|
|
4136
|
+
"prop": "open",
|
|
3309
4137
|
"type": "`boolean`",
|
|
3310
|
-
"default": "
|
|
3311
|
-
"description": "
|
|
4138
|
+
"default": "",
|
|
4139
|
+
"description": "Controls visibility"
|
|
3312
4140
|
},
|
|
3313
4141
|
{
|
|
3314
|
-
"prop": "
|
|
4142
|
+
"prop": "isOpen",
|
|
3315
4143
|
"type": "`boolean`",
|
|
3316
|
-
"default": "
|
|
3317
|
-
"description": "
|
|
4144
|
+
"default": "",
|
|
4145
|
+
"description": "Alias of `open`"
|
|
3318
4146
|
},
|
|
3319
4147
|
{
|
|
3320
|
-
"prop": "
|
|
4148
|
+
"prop": "onClose",
|
|
4149
|
+
"type": "`(reason) => void`",
|
|
4150
|
+
"default": "",
|
|
4151
|
+
"description": "Called when the sheet should close; `reason` is `'select'` (after an action's `onSelect`), `'cancel'`, `'escape'`, `'backdrop'` or `'drag'`"
|
|
4152
|
+
},
|
|
4153
|
+
{
|
|
4154
|
+
"prop": "title",
|
|
3321
4155
|
"type": "`ReactNode`",
|
|
3322
4156
|
"default": "",
|
|
3323
|
-
"description": "
|
|
4157
|
+
"description": "Small heading above the actions; names the sheet (`aria-labelledby`) unless `aria-label` is set"
|
|
3324
4158
|
},
|
|
3325
4159
|
{
|
|
3326
|
-
"prop": "
|
|
3327
|
-
"type": "`
|
|
4160
|
+
"prop": "message",
|
|
4161
|
+
"type": "`ReactNode`",
|
|
3328
4162
|
"default": "",
|
|
3329
|
-
"description": "
|
|
4163
|
+
"description": "Muted text under the title; wired to `aria-describedby`"
|
|
3330
4164
|
},
|
|
3331
4165
|
{
|
|
3332
|
-
"prop": "
|
|
3333
|
-
"type": "`
|
|
3334
|
-
"default": "
|
|
3335
|
-
"description": "
|
|
4166
|
+
"prop": "actions",
|
|
4167
|
+
"type": "`Array`",
|
|
4168
|
+
"default": "[]",
|
|
4169
|
+
"description": "`[{ key?, label, icon?, tone?: 'default' or 'destructive', onSelect, disabled? }]`. `icon` is a node (`<Trash2 />`) or a component (`Trash2`)"
|
|
3336
4170
|
},
|
|
3337
4171
|
{
|
|
3338
|
-
"prop": "
|
|
4172
|
+
"prop": "cancelLabel",
|
|
4173
|
+
"type": "`ReactNode`",
|
|
4174
|
+
"default": "\"Cancel\"",
|
|
4175
|
+
"description": "Text of the separate Cancel button (always shown)"
|
|
4176
|
+
},
|
|
4177
|
+
{
|
|
4178
|
+
"prop": "dismissible",
|
|
3339
4179
|
"type": "`boolean`",
|
|
3340
|
-
"default": "
|
|
3341
|
-
"description": "
|
|
4180
|
+
"default": "true",
|
|
4181
|
+
"description": "`false` disables Escape, backdrop click and drag dismissal"
|
|
3342
4182
|
},
|
|
3343
4183
|
{
|
|
3344
4184
|
"prop": "className",
|
|
3345
4185
|
"type": "`string`",
|
|
3346
4186
|
"default": "",
|
|
3347
|
-
"description": "
|
|
3348
|
-
},
|
|
3349
|
-
{
|
|
3350
|
-
"prop": "...rest",
|
|
3351
|
-
"type": "`HTMLAttributes`",
|
|
3352
|
-
"default": "",
|
|
3353
|
-
"description": "Forwarded to the panel (`aria-label`, `data-*`, …); `ref` points at the panel"
|
|
4187
|
+
"description": "Classes merged onto the sheet panel"
|
|
3354
4188
|
}
|
|
3355
4189
|
],
|
|
3356
|
-
"examples": "
|
|
4190
|
+
"examples": "```jsx\nfunction PhotoMenu({ photo }) {\n const [open, setOpen] = useState(false);\n return (\n <>\n <Button onClick={() => setOpen(true)}>Options</Button>\n <ActionSheet\n open={open}\n onClose={() => setOpen(false)}\n title=\"Photo options\"\n message=\"Choose what to do with this photo.\"\n actions={[\n { key: 'share', label: 'Share', icon: Share2, onSelect: () => share(photo) },\n { key: 'archive', label: 'Archive', onSelect: () => archive(photo), disabled: photo.archived },\n { key: 'delete', label: 'Delete', icon: Trash2, tone: 'destructive', onSelect: () => remove(photo) },\n ]}\n />\n </>\n );\n}\n```\n\n```jsx\n// Custom cancel text, no title (named by aria-label)\n<ActionSheet\n open={open}\n onClose={() => setOpen(false)}\n aria-label=\"Sort by\"\n cancelLabel=\"Never mind\"\n actions={[\n { label: 'Newest first', onSelect: () => setSort('new') },\n { label: 'Oldest first', onSelect: () => setSort('old') },\n ]}\n/>\n```\n\n[← Component overview](README.md)"
|
|
3357
4191
|
},
|
|
3358
4192
|
{
|
|
3359
4193
|
"name": "Tour",
|
|
@@ -3362,7 +4196,7 @@
|
|
|
3362
4196
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
3363
4197
|
"peerDependencies": [],
|
|
3364
4198
|
"hasDoc": true,
|
|
3365
|
-
"description": "Step-based onboarding tour (coach marks): dims the page, spotlights each step's target element and shows a step card beside it with Back / Next / Skip / Finish. Generalises the `SpotlightTour` that TaskPro and ProcurementPro each hand-rolled. A step without a target — or whose target never renders — shows as a centred card, so the tour never stalls.",
|
|
4199
|
+
"description": "Step-based onboarding tour (coach marks): dims the page, spotlights each step's target element and shows a step card beside it with Back / Next / Skip / Finish. Generalises the `SpotlightTour` that TaskPro and ProcurementPro each hand-rolled. A step without a target — or whose target never renders — shows as a centred card, so the tour never stalls. On phones the card docks at the bottom of the screen and steps can be swiped.",
|
|
3366
4200
|
"props": [
|
|
3367
4201
|
{
|
|
3368
4202
|
"prop": "open",
|
|
@@ -3424,6 +4258,12 @@
|
|
|
3424
4258
|
"default": "6",
|
|
3425
4259
|
"description": "Space in px between the target and the spotlight ring"
|
|
3426
4260
|
},
|
|
4261
|
+
{
|
|
4262
|
+
"prop": "presentation",
|
|
4263
|
+
"type": "`\"auto\"`, `\"popover\"` or `\"sheet\"`",
|
|
4264
|
+
"default": "\"auto\"",
|
|
4265
|
+
"description": "`sheet` docks the step card at the bottom of the screen; `popover` anchors it beside the target; `auto` docks on small touch screens (`(pointer: coarse) and (max-width: 639px)`) and anchors elsewhere (anchored on the server and while hydrating)"
|
|
4266
|
+
},
|
|
3427
4267
|
{
|
|
3428
4268
|
"prop": "className",
|
|
3429
4269
|
"type": "`string`",
|
|
@@ -3434,10 +4274,10 @@
|
|
|
3434
4274
|
"prop": "...rest",
|
|
3435
4275
|
"type": "`HTMLAttributes`",
|
|
3436
4276
|
"default": "",
|
|
3437
|
-
"description": "Forwarded to the step card; `ref` points at it"
|
|
4277
|
+
"description": "Forwarded to the step card; `ref` points at it. Your `onPointerDown` / `onPointerUp` / `onPointerCancel` / `onClickCapture` run before the swipe handling"
|
|
3438
4278
|
}
|
|
3439
4279
|
],
|
|
3440
|
-
"examples": "### Basic\n\n```jsx\nconst steps = [\n { title: 'Welcome', content: 'A 30-second tour of your workspace.' },\n { target: '[data-tour=\"new-task\"]', title: 'Capture work', content: 'New task opens a quick form.', placement: 'bottom' },\n { target: searchRef, title: 'Jump anywhere', content: 'Search tasks and projects.', placement: 'right' },\n];\n\nconst [open, setOpen] = useState(false);\n\n<Button onClick={() => setOpen(true)}>Take the tour</Button>\n<Tour open={open} steps={steps} onClose={() => setOpen(false)} onFinish={() => markTourSeen()} />\n```\n\n### Route-aware (react-router)\n\n```jsx\n<Tour\n open={open}\n steps={steps}\n onStepChange={(i, step) => step.route && navigate(step.route)}\n onClose={() => setOpen(false)}\n/>\n```\n\nExtra fields on a step (like `route`) are passed back to you untouched. The tour waits up to `targetTimeout` for the next screen's target to render."
|
|
4280
|
+
"examples": "### Basic\n\n```jsx\nconst steps = [\n { title: 'Welcome', content: 'A 30-second tour of your workspace.' },\n { target: '[data-tour=\"new-task\"]', title: 'Capture work', content: 'New task opens a quick form.', placement: 'bottom' },\n { target: searchRef, title: 'Jump anywhere', content: 'Search tasks and projects.', placement: 'right' },\n];\n\nconst [open, setOpen] = useState(false);\n\n<Button onClick={() => setOpen(true)}>Take the tour</Button>\n<Tour open={open} steps={steps} onClose={() => setOpen(false)} onFinish={() => markTourSeen()} />\n```\n\n### Route-aware (react-router)\n\n```jsx\n<Tour\n open={open}\n steps={steps}\n onStepChange={(i, step) => step.route && navigate(step.route)}\n onClose={() => setOpen(false)}\n/>\n```\n\nExtra fields on a step (like `route`) are passed back to you untouched. The tour waits up to `targetTimeout` for the next screen's target to render.\n\n### Phones (docked card)\n\n```jsx\n// Default: docked on phones, anchored on desktop\n<Tour open={open} steps={steps} onClose={() => setOpen(false)} />\n\n// Always docked (e.g. a mobile-only shell)\n<Tour presentation=\"sheet\" open={open} steps={steps} onClose={() => setOpen(false)} />\n```\n\nWhile docked:\n\n- the card spans the screen width minus a 1rem gutter, sits 1rem above the bottom edge with `pb-safe` for the home indicator, and scrolls inside `max-h-[60dvh]`;\n- the spotlight stays on the target; the card carries `data-placement=\"docked\"` and `data-presentation=\"sheet\"`;\n- swiping the card **left goes to the next step, right goes back** (at least 50px, clearly more horizontal than vertical; vertical drags still scroll the card). A swipe that ends on a button does not also press it;\n- the close button is a 44 × 44px touch target on coarse pointers.\n\nThe anchored card is capped at `calc(100dvh - 2rem)` (dynamic viewport units, so the mobile browser toolbar never clips it)."
|
|
3441
4281
|
},
|
|
3442
4282
|
{
|
|
3443
4283
|
"name": "Badge",
|
|
@@ -3452,7 +4292,7 @@
|
|
|
3452
4292
|
"prop": "variant",
|
|
3453
4293
|
"type": "`string`",
|
|
3454
4294
|
"default": "\"default\"",
|
|
3455
|
-
"description": "`default`, `primary`, `secondary`, `success`, `warning`, `error`, `info`, `outline`. `info` uses the `info` palette (`bg-info-100 text-info-800`)"
|
|
4295
|
+
"description": "`default`, `primary`, `secondary`, `success`, `warning`, `error`, `info`, `outline`. `info` uses the `info` palette (`bg-info-100 text-info-800`). Every variant has dark-mode tones; `outline` has its own text colour (`text-gray-700`, `dark:text-gray-200`)"
|
|
3456
4296
|
},
|
|
3457
4297
|
{
|
|
3458
4298
|
"prop": "size",
|
|
@@ -3466,6 +4306,24 @@
|
|
|
3466
4306
|
"default": "true",
|
|
3467
4307
|
"description": "Rounded pill when true"
|
|
3468
4308
|
},
|
|
4309
|
+
{
|
|
4310
|
+
"prop": "dot",
|
|
4311
|
+
"type": "`boolean`",
|
|
4312
|
+
"default": "false",
|
|
4313
|
+
"description": "Leading decorative dot in the current text colour (e.g. a \"Live\" badge)"
|
|
4314
|
+
},
|
|
4315
|
+
{
|
|
4316
|
+
"prop": "icon",
|
|
4317
|
+
"type": "`ReactNode`",
|
|
4318
|
+
"default": "",
|
|
4319
|
+
"description": "Leading decorative icon slot (`h-3.5 w-3.5`, `aria-hidden`)"
|
|
4320
|
+
},
|
|
4321
|
+
{
|
|
4322
|
+
"prop": "max",
|
|
4323
|
+
"type": "`number`",
|
|
4324
|
+
"default": "",
|
|
4325
|
+
"description": "When `children` is a number above `max`, renders `${max}+` (e.g. `max={99}` shows `99+`). Ignored for non-numeric children"
|
|
4326
|
+
},
|
|
3469
4327
|
{
|
|
3470
4328
|
"prop": "className",
|
|
3471
4329
|
"type": "`string`",
|
|
@@ -3479,7 +4337,7 @@
|
|
|
3479
4337
|
"description": "Badge content"
|
|
3480
4338
|
}
|
|
3481
4339
|
],
|
|
3482
|
-
"examples": "```jsx\n<Badge>New</Badge>\n<Badge variant=\"success\">Active</Badge>\n<Badge variant=\"error\" size=\"sm\">3</Badge>\n```\n\n[← Component overview](README.md)"
|
|
4340
|
+
"examples": "```jsx\n<Badge>New</Badge>\n<Badge variant=\"success\">Active</Badge>\n<Badge variant=\"error\" size=\"sm\">3</Badge>\n<Badge dot variant=\"success\">Live</Badge>\n<Badge icon={<Star />} variant=\"warning\">Featured</Badge>\n<Badge variant=\"error\" max={99}>{120}</Badge> {/* 99+ */}\n```\n\n[← Component overview](README.md)"
|
|
3483
4341
|
},
|
|
3484
4342
|
{
|
|
3485
4343
|
"name": "Avatar",
|
|
@@ -3524,7 +4382,7 @@
|
|
|
3524
4382
|
"prop": "status",
|
|
3525
4383
|
"type": "`string`",
|
|
3526
4384
|
"default": "",
|
|
3527
|
-
"description": "`online`, `offline`, `away`, `busy`"
|
|
4385
|
+
"description": "`online`, `offline`, `away`, `busy`. The dot is ringed in the `surface` colour, so it reads correctly in dark mode"
|
|
3528
4386
|
},
|
|
3529
4387
|
{
|
|
3530
4388
|
"prop": "className",
|
|
@@ -3533,7 +4391,49 @@
|
|
|
3533
4391
|
"description": "Extra CSS classes"
|
|
3534
4392
|
}
|
|
3535
4393
|
],
|
|
3536
|
-
"examples": "```jsx\n<Avatar name=\"Alice Smith\" />\n<Avatar src=\"/photo.jpg\" alt=\"User\" />\n<Avatar name=\"Bob\" status=\"online\" size=\"lg\" />\n```\n\n[← Component overview](README.md)"
|
|
4394
|
+
"examples": "```jsx\n<Avatar name=\"Alice Smith\" />\n<Avatar src=\"/photo.jpg\" alt=\"User\" />\n<Avatar name=\"Bob\" status=\"online\" size=\"lg\" />\n```\n\nTo stack several avatars with a \"+N\" overflow, use [AvatarGroup](AvatarGroup.md).\n\n[← Component overview](README.md)"
|
|
4395
|
+
},
|
|
4396
|
+
{
|
|
4397
|
+
"name": "AvatarGroup",
|
|
4398
|
+
"group": "utility",
|
|
4399
|
+
"familyExports": [],
|
|
4400
|
+
"importFrom": "@dreamtree-org/twreact-ui",
|
|
4401
|
+
"peerDependencies": [],
|
|
4402
|
+
"hasDoc": true,
|
|
4403
|
+
"description": "Overlapping stack of `Avatar`s (assignees, participants, members). Each avatar is ringed in the `surface` colour so it reads as cut out of the one behind it, in light and dark mode. With `max`, extra avatars collapse into a \"+N\" tile.",
|
|
4404
|
+
"props": [
|
|
4405
|
+
{
|
|
4406
|
+
"prop": "children",
|
|
4407
|
+
"type": "`ReactNode`",
|
|
4408
|
+
"default": "",
|
|
4409
|
+
"description": "`Avatar` elements. Non-element children (`null`, `false`) are ignored"
|
|
4410
|
+
},
|
|
4411
|
+
{
|
|
4412
|
+
"prop": "max",
|
|
4413
|
+
"type": "`number`",
|
|
4414
|
+
"default": "",
|
|
4415
|
+
"description": "Show at most this many avatars, then a \"+N\" overflow tile (`role=\"img\"`, `aria-label=\"N more\"`). Omit to show all"
|
|
4416
|
+
},
|
|
4417
|
+
{
|
|
4418
|
+
"prop": "size",
|
|
4419
|
+
"type": "`string`",
|
|
4420
|
+
"default": "\"md\"",
|
|
4421
|
+
"description": "`xs`, `sm`, `md`, `lg`, `xl`, `2xl`. Passed to every child that doesn't set its own `size`; also sizes the overflow tile and the overlap"
|
|
4422
|
+
},
|
|
4423
|
+
{
|
|
4424
|
+
"prop": "className",
|
|
4425
|
+
"type": "`string`",
|
|
4426
|
+
"default": "",
|
|
4427
|
+
"description": "Extra CSS classes on the root (`role=\"group\"`)"
|
|
4428
|
+
},
|
|
4429
|
+
{
|
|
4430
|
+
"prop": "...rest",
|
|
4431
|
+
"type": "—",
|
|
4432
|
+
"default": "",
|
|
4433
|
+
"description": "Passed to the root `<div>` (e.g. `aria-label=\"Assignees\"`). The ref is forwarded to it"
|
|
4434
|
+
}
|
|
4435
|
+
],
|
|
4436
|
+
"examples": "```jsx\n<AvatarGroup max={3} size=\"sm\" aria-label=\"Assignees\">\n <Avatar name=\"Asha Rao\" />\n <Avatar name=\"Bilal Khan\" src=\"/bilal.jpg\" />\n <Avatar name=\"Chen Li\" />\n <Avatar name=\"Divya Nair\" />\n <Avatar name=\"Esha Patel\" />\n</AvatarGroup>\n{/* Asha, Bilal, Chen, then a \"+2\" tile */}\n```\n\n[← Component overview](README.md)"
|
|
3537
4437
|
},
|
|
3538
4438
|
{
|
|
3539
4439
|
"name": "Card",
|
|
@@ -3542,7 +4442,7 @@
|
|
|
3542
4442
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
3543
4443
|
"peerDependencies": [],
|
|
3544
4444
|
"hasDoc": true,
|
|
3545
|
-
"description": "Card container with optional hover shadow. Subcomponents: `Card.Header`, `Card.Title`, `Card.Description`, `Card.Content`, `Card.Footer` for structure. The card and its header/footer dividers use the semantic `bg-surface` / `border-line` tokens, so they follow dark mode (re-theme via `--dt-surface` / `--dt-line`).",
|
|
4445
|
+
"description": "Card container with optional hover shadow or a fully interactive (pressable) style. Subcomponents: `Card.Header`, `Card.Title`, `Card.Description`, `Card.Content`, `Card.Footer` for structure. The card and its header/footer dividers use the semantic `bg-surface` / `border-line` tokens, so they follow dark mode (re-theme via `--dt-surface` / `--dt-line`).",
|
|
3546
4446
|
"props": [
|
|
3547
4447
|
{
|
|
3548
4448
|
"prop": "children",
|
|
@@ -3562,6 +4462,12 @@
|
|
|
3562
4462
|
"default": "false",
|
|
3563
4463
|
"description": "If true, add hover:shadow-md transition."
|
|
3564
4464
|
},
|
|
4465
|
+
{
|
|
4466
|
+
"prop": "interactive",
|
|
4467
|
+
"type": "`boolean`",
|
|
4468
|
+
"default": "false",
|
|
4469
|
+
"description": "Pressable card: hover shadow, a slight press shrink (off under reduced motion), a `focus-visible` ring and `cursor-pointer`. With an `onClick` (and no `role` of your own) the card also gets `role=\"button\"` and `tabIndex=0`, and Enter / Space call `onClick`. Without `interactive` the card renders exactly as before."
|
|
4470
|
+
},
|
|
3565
4471
|
{
|
|
3566
4472
|
"prop": "...rest",
|
|
3567
4473
|
"type": "—",
|
|
@@ -3654,15 +4560,15 @@
|
|
|
3654
4560
|
},
|
|
3655
4561
|
{
|
|
3656
4562
|
"prop": "orientation",
|
|
3657
|
-
"type": "`\"horizontal\"` ",
|
|
3658
|
-
"default": "\"
|
|
3659
|
-
"description": "`\"
|
|
4563
|
+
"type": "`\"horizontal\"` | `\"vertical\"` | `\"auto\"`",
|
|
4564
|
+
"default": "\"horizontal\"",
|
|
4565
|
+
"description": "Layout. `\"auto\"` stacks vertically below the `sm` breakpoint (640 px) and lays out horizontally from `sm` up, using CSS only."
|
|
3660
4566
|
},
|
|
3661
4567
|
{
|
|
3662
4568
|
"prop": "variant",
|
|
3663
|
-
"type": "`
|
|
4569
|
+
"type": "`\"default\"` | `\"dots\"`",
|
|
3664
4570
|
"default": "\"default\"",
|
|
3665
|
-
"description": "
|
|
4571
|
+
"description": "`\"dots\"` renders compact progress dots: one labelled dot per step, the current step as a wider pill, and no titles or descriptions. Clicks work as with `onStepClick`. Unknown values render the default stepper."
|
|
3666
4572
|
},
|
|
3667
4573
|
{
|
|
3668
4574
|
"prop": "onStepClick",
|
|
@@ -3734,7 +4640,13 @@
|
|
|
3734
4640
|
"prop": "disabled",
|
|
3735
4641
|
"type": "`boolean`",
|
|
3736
4642
|
"default": "false",
|
|
3737
|
-
"description": "Disable the zone and remove buttons."
|
|
4643
|
+
"description": "Disable the zone, pick buttons and remove buttons."
|
|
4644
|
+
},
|
|
4645
|
+
{
|
|
4646
|
+
"prop": "capture",
|
|
4647
|
+
"type": "`boolean` | `\"user\"` | `\"environment\"`",
|
|
4648
|
+
"default": "",
|
|
4649
|
+
"description": "When set, touch screens also get a **Take photo** button, wired to a second hidden input that carries `capture` (and `accept`, defaulting to `\"image/*\"`). The **Choose file** input never has `capture`, so picking from the library still works."
|
|
3738
4650
|
},
|
|
3739
4651
|
{
|
|
3740
4652
|
"prop": "className",
|
|
@@ -3788,7 +4700,7 @@
|
|
|
3788
4700
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
3789
4701
|
"peerDependencies": [],
|
|
3790
4702
|
"hasDoc": true,
|
|
3791
|
-
"description": "Horizontal carousel of slides. Renders `children` as slides (one per child).
|
|
4703
|
+
"description": "Horizontal carousel of slides built on native CSS scroll-snap. Renders `children` as slides (one per child). Native momentum swipe on touch, optional auto-play, always-visible previous/next buttons, and dot indicators. Auto-play pauses on hover, touch and keyboard focus, and is off under reduced motion.",
|
|
3792
4704
|
"props": [
|
|
3793
4705
|
{
|
|
3794
4706
|
"prop": "children",
|
|
@@ -3830,7 +4742,7 @@
|
|
|
3830
4742
|
"prop": "...rest",
|
|
3831
4743
|
"type": "`HTMLAttributes<HTMLDivElement>`",
|
|
3832
4744
|
"default": "",
|
|
3833
|
-
"description": "Forwarded to the root `<div>` (`aria-*`, `data-*`, `id`, …); `onMouseEnter` / `onMouseLeave` are composed with the autoplay pause."
|
|
4745
|
+
"description": "Forwarded to the root `<div>` (`aria-*`, `data-*`, `id`, …); `onMouseEnter` / `onMouseLeave` / `onFocus` / `onBlur` / `onTouchStart` / `onTouchEnd` are composed with the autoplay pause. Pass `aria-label` to name the carousel (default `\"Carousel\"`)."
|
|
3834
4746
|
}
|
|
3835
4747
|
],
|
|
3836
4748
|
"examples": "```jsx\n<Carousel autoPlay interval={5000} showDots>\n <div className=\"h-64 bg-gray-100 flex items-center justify-center\">Slide 1</div>\n <div className=\"h-64 bg-gray-200 flex items-center justify-center\">Slide 2</div>\n <div className=\"h-64 bg-gray-300 flex items-center justify-center\">Slide 3</div>\n</Carousel>\n```\n\n[← Component overview](README.md)"
|
|
@@ -3884,7 +4796,7 @@
|
|
|
3884
4796
|
"prop": "handle",
|
|
3885
4797
|
"type": "`boolean`",
|
|
3886
4798
|
"default": "false",
|
|
3887
|
-
"description": "Drag only from the element you spread `handleProps` on
|
|
4799
|
+
"description": "Drag only from the element you spread `handleProps` on. The handle has `touch-none`, so a touch drag on it starts immediately; without a handle, touch drags start after a long-press"
|
|
3888
4800
|
},
|
|
3889
4801
|
{
|
|
3890
4802
|
"prop": "orientation",
|
|
@@ -3925,6 +4837,78 @@
|
|
|
3925
4837
|
],
|
|
3926
4838
|
"examples": "### Basic\n\n```jsx\nconst [tasks, setTasks] = useState(initialTasks);\n\n<Sortable\n aria-label=\"Backlog\"\n items={tasks}\n onReorder={(next) => setTasks(next)}\n getItemLabel={(t) => t.title}\n renderItem={(t) => <div className=\"rounded-md border p-3\">{t.title}</div>}\n/>\n```\n\n### Drag handle + buttons\n\n```jsx\n<Sortable\n handle\n aria-label=\"Backlog\"\n items={tasks}\n onReorder={(next, { from, to }) => saveOrder(next)}\n renderItem={(t, { handleProps, moveUp, moveDown }) => (\n <div className=\"flex items-center gap-2\">\n <span {...handleProps} aria-hidden=\"true\"><GripVertical /></span>\n <span className=\"flex-1\">{t.title}</span>\n <Button size=\"sm\" variant=\"ghost\" onClick={moveUp} aria-label={`Move ${t.title} up`}>↑</Button>\n <Button size=\"sm\" variant=\"ghost\" onClick={moveDown} aria-label={`Move ${t.title} down`}>↓</Button>\n </div>\n )}\n/>\n```"
|
|
3927
4839
|
},
|
|
4840
|
+
{
|
|
4841
|
+
"name": "PullToRefresh",
|
|
4842
|
+
"group": "utility",
|
|
4843
|
+
"familyExports": [],
|
|
4844
|
+
"importFrom": "@dreamtree-org/twreact-ui",
|
|
4845
|
+
"peerDependencies": [],
|
|
4846
|
+
"hasDoc": true,
|
|
4847
|
+
"description": "Touch pull-to-refresh around a scroll container. Pull down while the list is scrolled to the top: a spinner follows the finger (rubber-band resistance, rotation and opacity tracking the pull). Release past `threshold` and `onRefresh()` runs; the spinner keeps spinning until its promise settles, then retracts. Normal scrolling is never intercepted.",
|
|
4848
|
+
"props": [
|
|
4849
|
+
{
|
|
4850
|
+
"prop": "onRefresh",
|
|
4851
|
+
"type": "`() => Promise | void`",
|
|
4852
|
+
"default": "",
|
|
4853
|
+
"description": "Called on release past `threshold`. The indicator stays in the refreshing state until the returned promise settles (resolve or reject); a synchronous return settles on the next tick. Handle your own errors inside it"
|
|
4854
|
+
},
|
|
4855
|
+
{
|
|
4856
|
+
"prop": "threshold",
|
|
4857
|
+
"type": "`number`",
|
|
4858
|
+
"default": "64",
|
|
4859
|
+
"description": "Pull distance in px (after resistance) that arms a refresh; the refreshing indicator rests at this offset"
|
|
4860
|
+
},
|
|
4861
|
+
{
|
|
4862
|
+
"prop": "maxPull",
|
|
4863
|
+
"type": "`number`",
|
|
4864
|
+
"default": "threshold * 2",
|
|
4865
|
+
"description": "Ceiling of the resistance-scaled pull in px"
|
|
4866
|
+
},
|
|
4867
|
+
{
|
|
4868
|
+
"prop": "disabled",
|
|
4869
|
+
"type": "`boolean`",
|
|
4870
|
+
"default": "false",
|
|
4871
|
+
"description": "Ignores pulls (a refresh already in flight still finishes)"
|
|
4872
|
+
},
|
|
4873
|
+
{
|
|
4874
|
+
"prop": "refreshingLabel",
|
|
4875
|
+
"type": "`string`",
|
|
4876
|
+
"default": "\"Refreshing…\"",
|
|
4877
|
+
"description": "Text of the polite live region (`role=\"status\"`, `aria-live=\"polite\"`) while refreshing"
|
|
4878
|
+
},
|
|
4879
|
+
{
|
|
4880
|
+
"prop": "scrollRef",
|
|
4881
|
+
"type": "`RefObject<HTMLElement>`",
|
|
4882
|
+
"default": "",
|
|
4883
|
+
"description": "Your own scroll container inside `children`. Omit it and PullToRefresh renders the scroller (`h-full overflow-y-auto overscroll-y-contain`)"
|
|
4884
|
+
},
|
|
4885
|
+
{
|
|
4886
|
+
"prop": "contentClassName",
|
|
4887
|
+
"type": "`string`",
|
|
4888
|
+
"default": "",
|
|
4889
|
+
"description": "Extra classes for the inner content element (the scroller when `scrollRef` is omitted)"
|
|
4890
|
+
},
|
|
4891
|
+
{
|
|
4892
|
+
"prop": "children",
|
|
4893
|
+
"type": "`ReactNode`",
|
|
4894
|
+
"default": "",
|
|
4895
|
+
"description": "The list / feed"
|
|
4896
|
+
},
|
|
4897
|
+
{
|
|
4898
|
+
"prop": "className",
|
|
4899
|
+
"type": "`string`",
|
|
4900
|
+
"default": "",
|
|
4901
|
+
"description": "Root classes (merged with `cn`). Give the root a height (`h-96`, `flex-1`, …) so the inner scroller can scroll"
|
|
4902
|
+
},
|
|
4903
|
+
{
|
|
4904
|
+
"prop": "...rest",
|
|
4905
|
+
"type": "`HTMLAttributes`",
|
|
4906
|
+
"default": "",
|
|
4907
|
+
"description": "Forwarded to the root `div`; `ref` points at the root"
|
|
4908
|
+
}
|
|
4909
|
+
],
|
|
4910
|
+
"examples": "```jsx\n<PullToRefresh onRefresh={() => refetchOrders()} className=\"h-[calc(100dvh-4rem)]\">\n <ul>{orders.map((o) => <OrderRow key={o.id} order={o} />)}</ul>\n</PullToRefresh>\n```\n\n```jsx\n// Your own scroll container\nconst listRef = useRef(null);\n\n<PullToRefresh scrollRef={listRef} onRefresh={refetch} refreshingLabel=\"Loading new orders…\" className=\"h-96\">\n <ul ref={listRef} className=\"h-full overflow-y-auto\">{rows}</ul>\n</PullToRefresh>\n```\n\n[← Component overview](README.md)"
|
|
4911
|
+
},
|
|
3928
4912
|
{
|
|
3929
4913
|
"name": "AppShell",
|
|
3930
4914
|
"group": "blocks",
|
|
@@ -3932,7 +4916,7 @@
|
|
|
3932
4916
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
3933
4917
|
"peerDependencies": [],
|
|
3934
4918
|
"hasDoc": true,
|
|
3935
|
-
"description": "Page-level block: the product app shell — a sidebar on large screens
|
|
4919
|
+
"description": "Page-level block: the product app shell — a sidebar on large screens, a top bar with a navigation drawer below `lg`, and an optional bottom nav on phones. Replaces the shell 12 of 13 products hand-rolled (extracted from procurementpro `components/app-shell.tsx`). Colours come from the `shell` and role tokens (`bg-background`, `bg-surface`, `text-foreground`, `text-muted`, `border-line`), so a brand file restyles it and dark mode needs no extra classes.",
|
|
3936
4920
|
"props": [
|
|
3937
4921
|
{
|
|
3938
4922
|
"prop": "brand",
|
|
@@ -3964,6 +4948,12 @@
|
|
|
3964
4948
|
"default": "",
|
|
3965
4949
|
"description": "Title shown in the mobile top bar"
|
|
3966
4950
|
},
|
|
4951
|
+
{
|
|
4952
|
+
"prop": "bottomNav",
|
|
4953
|
+
"type": "`ReactNode`",
|
|
4954
|
+
"default": "",
|
|
4955
|
+
"description": "Bottom navigation for phones, usually a [`FootNav`](FootNav.md). Rendered below `md` only (hidden from `md` up, where the sidebar / drawer take over). When set, a `FootNav.Spacer` is added after `<main>` so content doesn't scroll under the bar"
|
|
4956
|
+
},
|
|
3967
4957
|
{
|
|
3968
4958
|
"prop": "linkAs",
|
|
3969
4959
|
"type": "`ElementType`",
|
|
@@ -3974,16 +4964,22 @@
|
|
|
3974
4964
|
"prop": "className",
|
|
3975
4965
|
"type": "`string`",
|
|
3976
4966
|
"default": "",
|
|
3977
|
-
"description": "Extra CSS classes"
|
|
4967
|
+
"description": "Extra CSS classes for the root (merged with `cn`)"
|
|
3978
4968
|
},
|
|
3979
4969
|
{
|
|
3980
4970
|
"prop": "children",
|
|
3981
4971
|
"type": "`ReactNode`",
|
|
3982
4972
|
"default": "",
|
|
3983
4973
|
"description": "Page content, rendered in `<main id=\"main\">`"
|
|
4974
|
+
},
|
|
4975
|
+
{
|
|
4976
|
+
"prop": "...rest",
|
|
4977
|
+
"type": "`HTMLAttributes<HTMLDivElement>`",
|
|
4978
|
+
"default": "",
|
|
4979
|
+
"description": "Forwarded to the root `<div>`; the ref is forwarded to it"
|
|
3984
4980
|
}
|
|
3985
4981
|
],
|
|
3986
|
-
"examples": "```jsx\n<AppShell\n brand={<span className=\"text-lg font-bold\">PracticePro</span>}\n nav={[\n { id: 'overview', label: 'Overview', href: '/overview', icon: <Home className=\"h-5 w-5\" /> },\n { id: 'compliance', label: 'Compliance', href: '/compliance', active: true },\n { id: 'approvals', label: 'Approvals', href: '/approvals', count: 4 },\n ]}\n topbar={<Avatar name=\"Ananya Rao\" size=\"sm\" />}\n linkAs={Link}\n>\n <PageHeader title=\"Compliance calendar\" />\n</AppShell>\n```"
|
|
4982
|
+
"examples": "```jsx\n<AppShell\n brand={<span className=\"text-lg font-bold\">PracticePro</span>}\n nav={[\n { id: 'overview', label: 'Overview', href: '/overview', icon: <Home className=\"h-5 w-5\" /> },\n { id: 'compliance', label: 'Compliance', href: '/compliance', active: true },\n { id: 'approvals', label: 'Approvals', href: '/approvals', count: 4 },\n ]}\n topbar={<Avatar name=\"Ananya Rao\" size=\"sm\" />}\n linkAs={Link}\n>\n <PageHeader title=\"Compliance calendar\" />\n</AppShell>\n```\n\n### Bottom nav on phones\n\n```jsx\n<AppShell\n brand={<Logo />}\n nav={nav}\n linkAs={Link}\n bottomNav={\n <FootNav\n linkAs={Link}\n items={[\n { id: 'home', label: 'Home', icon: <Home />, href: '/', active: true },\n { id: 'tasks', label: 'Tasks', icon: <CheckSquare />, href: '/tasks', badge: 3 },\n { id: 'inbox', label: 'Inbox', icon: <Inbox />, href: '/inbox' },\n ]}\n />\n }\n>\n {page}\n</AppShell>\n```\n\nKeep the bottom nav to the 3–5 most used destinations; the full `nav` stays in the drawer (menu button) below `lg` and in the sidebar on desktop."
|
|
3987
4983
|
},
|
|
3988
4984
|
{
|
|
3989
4985
|
"name": "PageHeader",
|
|
@@ -4012,11 +5008,17 @@
|
|
|
4012
5008
|
"default": "",
|
|
4013
5009
|
"description": "Buttons on the right; keep one primary action"
|
|
4014
5010
|
},
|
|
5011
|
+
{
|
|
5012
|
+
"prop": "actionsPlacement",
|
|
5013
|
+
"type": "`'inline' | 'bottom'`",
|
|
5014
|
+
"default": "\"inline\"",
|
|
5015
|
+
"description": "`inline`: actions next to the title. `bottom`: on phones (coarse pointer and ≤639px) the actions are pinned in a [`BottomActionBar`](BottomActionBar.md) (`fullWidth`) at the bottom of the screen; on desktop, and on the server until hydration, they stay inline"
|
|
5016
|
+
},
|
|
4015
5017
|
{
|
|
4016
5018
|
"prop": "back",
|
|
4017
5019
|
"type": "`{ href: string; label: string }`",
|
|
4018
5020
|
"default": "",
|
|
4019
|
-
"description": "Back link for detail pages"
|
|
5021
|
+
"description": "Back link for detail pages. Grows to a 44px touch target on coarse pointers; `focus-visible` ring"
|
|
4020
5022
|
},
|
|
4021
5023
|
{
|
|
4022
5024
|
"prop": "linkAs",
|
|
@@ -4028,10 +5030,16 @@
|
|
|
4028
5030
|
"prop": "className",
|
|
4029
5031
|
"type": "`string`",
|
|
4030
5032
|
"default": "",
|
|
4031
|
-
"description": "Extra CSS classes"
|
|
5033
|
+
"description": "Extra CSS classes (merged with `cn`)"
|
|
5034
|
+
},
|
|
5035
|
+
{
|
|
5036
|
+
"prop": "...rest",
|
|
5037
|
+
"type": "`HTMLAttributes<HTMLDivElement>`",
|
|
5038
|
+
"default": "",
|
|
5039
|
+
"description": "Forwarded to the root `<div>`; the ref is forwarded to it"
|
|
4032
5040
|
}
|
|
4033
5041
|
],
|
|
4034
|
-
"examples": "```jsx\n<PageHeader\n title=\"Compliance calendar\"\n subtitle=\"Track deadlines, ownership and filing readiness across clients\"\n actions={<Button leftIcon={<Plus className=\"h-4 w-4\" />}>Add obligation</Button>}\n/>\n```\n\n[← Component overview](README.md)"
|
|
5042
|
+
"examples": "```jsx\n<PageHeader\n title=\"Compliance calendar\"\n subtitle=\"Track deadlines, ownership and filing readiness across clients\"\n actions={<Button leftIcon={<Plus className=\"h-4 w-4\" />}>Add obligation</Button>}\n/>\n```\n\n### Actions at the bottom on phones\n\n```jsx\n<PageHeader\n title=\"New invoice\"\n back={{ href: '/invoices', label: 'Invoices' }}\n actionsPlacement=\"bottom\"\n actions={\n <>\n <Button variant=\"outline\">Save draft</Button>\n <Button>Send invoice</Button>\n </>\n }\n/>\n{form}\n<BottomActionBar.Spacer className=\"sm:hidden\" />\n```\n\nWith `actionsPlacement=\"bottom\"`, put a `BottomActionBar.Spacer` at the end of the page so the bar doesn't cover the last fields. The actions render once (never duplicated), so ids and test queries stay unique.\n\n[← Component overview](README.md)"
|
|
4035
5043
|
},
|
|
4036
5044
|
{
|
|
4037
5045
|
"name": "StatCard",
|
|
@@ -4062,15 +5070,15 @@
|
|
|
4062
5070
|
},
|
|
4063
5071
|
{
|
|
4064
5072
|
"prop": "tone",
|
|
4065
|
-
"type": "`primary` ",
|
|
4066
|
-
"default": "
|
|
4067
|
-
"description": "
|
|
5073
|
+
"type": "`primary` | `secondary` | `success` | `warning` | `error` | `info`",
|
|
5074
|
+
"default": "\"primary\"",
|
|
5075
|
+
"description": "Chip colour (light tint in light mode, a deep translucent tint with a lighter icon in dark mode)"
|
|
4068
5076
|
},
|
|
4069
5077
|
{
|
|
4070
5078
|
"prop": "change",
|
|
4071
|
-
"type": "`{ value: number; period?: string; unit?: '%' ",
|
|
4072
|
-
"default": "
|
|
4073
|
-
"description": "
|
|
5079
|
+
"type": "`{ value: number; period?: string; unit?: '%' | ''; goodWhenUp?: boolean }`",
|
|
5080
|
+
"default": "",
|
|
5081
|
+
"description": "Change indicator; `goodWhenUp: false` makes a rise red. Drawn in `success-700` / `error-700` (at least 4.5:1 on white), `-400` in dark mode"
|
|
4074
5082
|
},
|
|
4075
5083
|
{
|
|
4076
5084
|
"prop": "loading",
|
|
@@ -4118,7 +5126,7 @@
|
|
|
4118
5126
|
"prop": "icon",
|
|
4119
5127
|
"type": "`ReactNode`",
|
|
4120
5128
|
"default": "",
|
|
4121
|
-
"description": "Small leading icon"
|
|
5129
|
+
"description": "Small leading icon (rendered in Badge's decorative `icon` slot)"
|
|
4122
5130
|
},
|
|
4123
5131
|
{
|
|
4124
5132
|
"prop": "children",
|
|
@@ -4160,7 +5168,7 @@
|
|
|
4160
5168
|
"prop": "icon",
|
|
4161
5169
|
"type": "`ReactNode`",
|
|
4162
5170
|
"default": "Inbox icon",
|
|
4163
|
-
"description": "Muted icon"
|
|
5171
|
+
"description": "Muted icon, in a `bg-secondary-100` circle (`dark:bg-gray-800` in dark mode)"
|
|
4164
5172
|
},
|
|
4165
5173
|
{
|
|
4166
5174
|
"prop": "action",
|
|
@@ -4236,9 +5244,9 @@
|
|
|
4236
5244
|
"props": [
|
|
4237
5245
|
{
|
|
4238
5246
|
"prop": "variant",
|
|
4239
|
-
"type": "`table` ",
|
|
4240
|
-
"default": "
|
|
4241
|
-
"description": "
|
|
5247
|
+
"type": "`table` | `cards` | `stats` | `detail`",
|
|
5248
|
+
"default": "\"table\"",
|
|
5249
|
+
"description": "Shape of the skeleton"
|
|
4242
5250
|
},
|
|
4243
5251
|
{
|
|
4244
5252
|
"prop": "rows",
|
|
@@ -4323,9 +5331,81 @@
|
|
|
4323
5331
|
"type": "`() => void`",
|
|
4324
5332
|
"default": "",
|
|
4325
5333
|
"description": "Called on cancel, Esc or backdrop click"
|
|
5334
|
+
},
|
|
5335
|
+
{
|
|
5336
|
+
"prop": "presentation",
|
|
5337
|
+
"type": "`string`",
|
|
5338
|
+
"default": "\"auto\"",
|
|
5339
|
+
"description": "`auto`, `popover`, `sheet`. As [Dialog](Dialog.md#presentation): bottom sheet on coarse-pointer phones, centred modal elsewhere"
|
|
5340
|
+
},
|
|
5341
|
+
{
|
|
5342
|
+
"prop": "haptics",
|
|
5343
|
+
"type": "`boolean`",
|
|
5344
|
+
"default": "false",
|
|
5345
|
+
"description": "Opt-in vibration: `warning` once when it opens with `destructive`, `success` when the confirm button is pressed. Cancel stays silent. No-op without `navigator.vibrate` (iOS Safari) and under reduced motion. See [useHaptics](useHaptics.md)"
|
|
5346
|
+
}
|
|
5347
|
+
],
|
|
5348
|
+
"examples": "```jsx\n<ConfirmDialog\n isOpen={open}\n title=\"Remove Kiran M from Diwali buyers?\"\n description=\"They stop receiving this list's campaigns.\"\n confirmLabel=\"Remove\"\n destructive\n onConfirm={remove}\n onCancel={() => setOpen(false)}\n/>\n\n// Keep the centred modal on phones too:\n<ConfirmDialog\n presentation=\"popover\"\n isOpen={open}\n title=\"Archive this list?\"\n confirmLabel=\"Archive\"\n onConfirm={archive}\n onCancel={() => setOpen(false)}\n/>\n```\n\n[← Component overview](README.md)"
|
|
5349
|
+
},
|
|
5350
|
+
{
|
|
5351
|
+
"name": "BottomActionBar",
|
|
5352
|
+
"group": "blocks",
|
|
5353
|
+
"familyExports": [],
|
|
5354
|
+
"importFrom": "@dreamtree-org/twreact-ui",
|
|
5355
|
+
"peerDependencies": [],
|
|
5356
|
+
"hasDoc": true,
|
|
5357
|
+
"description": "Page-level block: a bar pinned to the bottom of the screen that holds the page's primary action on forms and checkout pages (\"Save\", \"Place order\", \"Pay now\"). It sits on the `z-sticky` layer, pads the home indicator (`pb-safe`) and the side notches (`px-safe`), and uses role tokens (`bg-surface`, a top `border-line`, a soft upward shadow), so it is dark-mode aware. On phones it **rides above the on-screen keyboard**: it follows `window.visualViewport` (resize and scroll) and lifts itself by the height the keyboard covers. This is SSR-safe: on the server and without `visualViewport` it rests at the bottom.",
|
|
5358
|
+
"props": [
|
|
5359
|
+
{
|
|
5360
|
+
"prop": "children",
|
|
5361
|
+
"type": "`ReactNode`",
|
|
5362
|
+
"default": "",
|
|
5363
|
+
"description": "The actions, usually one or two `Button`s. Put the primary action last"
|
|
5364
|
+
},
|
|
5365
|
+
{
|
|
5366
|
+
"prop": "position",
|
|
5367
|
+
"type": "`'fixed' | 'sticky'`",
|
|
5368
|
+
"default": "\"fixed\"",
|
|
5369
|
+
"description": "`fixed`: pinned to the bottom of the screen (`fixed inset-x-0 bottom-0`); pair it with `BottomActionBar.Spacer`. `sticky`: `sticky bottom-0` inside its scroll container; it keeps its place in the flow and needs no Spacer"
|
|
5370
|
+
},
|
|
5371
|
+
{
|
|
5372
|
+
"prop": "align",
|
|
5373
|
+
"type": "`'start' | 'center' | 'end' | 'between'`",
|
|
5374
|
+
"default": "\"end\"",
|
|
5375
|
+
"description": "Horizontal alignment of the actions. `between` puts e.g. a total on the left and the action on the right"
|
|
5376
|
+
},
|
|
5377
|
+
{
|
|
5378
|
+
"prop": "fullWidth",
|
|
5379
|
+
"type": "`boolean`",
|
|
5380
|
+
"default": "false",
|
|
5381
|
+
"description": "Below `sm` (phones), the actions share the row's width equally"
|
|
5382
|
+
},
|
|
5383
|
+
{
|
|
5384
|
+
"prop": "stackOnMobile",
|
|
5385
|
+
"type": "`boolean`",
|
|
5386
|
+
"default": "false",
|
|
5387
|
+
"description": "Below `sm`, the actions stack full-width with the last (primary) one on top"
|
|
5388
|
+
},
|
|
5389
|
+
{
|
|
5390
|
+
"prop": "keyboardAware",
|
|
5391
|
+
"type": "`boolean`",
|
|
5392
|
+
"default": "true",
|
|
5393
|
+
"description": "Rise above the on-screen keyboard via `window.visualViewport`. While lifted the bar drops its `pb-safe` pad (the keyboard covers the home indicator) and sets `data-keyboard=\"open\"`"
|
|
5394
|
+
},
|
|
5395
|
+
{
|
|
5396
|
+
"prop": "className",
|
|
5397
|
+
"type": "`string`",
|
|
5398
|
+
"default": "",
|
|
5399
|
+
"description": "Extra classes for the bar (merged with `cn`, so yours win)"
|
|
5400
|
+
},
|
|
5401
|
+
{
|
|
5402
|
+
"prop": "...rest",
|
|
5403
|
+
"type": "`HTMLAttributes<HTMLDivElement>`",
|
|
5404
|
+
"default": "",
|
|
5405
|
+
"description": "Forwarded to the bar (`aria-label`, `role`, `data-*`, …). The ref is forwarded to it"
|
|
4326
5406
|
}
|
|
4327
5407
|
],
|
|
4328
|
-
"examples": "```jsx\n<
|
|
5408
|
+
"examples": "### Form\n\n```jsx\n<form onSubmit={save}>\n <Input label=\"Customer name\" />\n <Input label=\"Amount\" inputMode=\"decimal\" />\n <BottomActionBar.Spacer />\n <BottomActionBar fullWidth>\n <Button variant=\"outline\" onClick={cancel}>Cancel</Button>\n <Button type=\"submit\">Save payment</Button>\n </BottomActionBar>\n</form>\n```\n\n### Checkout\n\n```jsx\n<BottomActionBar align=\"between\" aria-label=\"Checkout\">\n <div>\n <p className=\"text-xs text-muted\">Total</p>\n <p className=\"text-lg font-semibold\">₹4,820.00</p>\n </div>\n <Button>Pay now</Button>\n</BottomActionBar>\n```\n\n### Stacked on phones\n\n```jsx\n<BottomActionBar stackOnMobile>\n <Button variant=\"outline\">Save draft</Button>\n <Button>Send invoice</Button>\n</BottomActionBar>\n```\n\n### Inside a scrolling panel\n\n```jsx\n<div className=\"h-96 overflow-y-auto\">\n {steps}\n <BottomActionBar position=\"sticky\">\n <Button variant=\"outline\">Back</Button>\n <Button>Continue</Button>\n </BottomActionBar>\n</div>\n```\n\n`PageHeader` uses this bar for `actionsPlacement=\"bottom\"`."
|
|
4329
5409
|
},
|
|
4330
5410
|
{
|
|
4331
5411
|
"name": "LineChart",
|
|
@@ -4395,8 +5475,8 @@
|
|
|
4395
5475
|
{
|
|
4396
5476
|
"prop": "yAxisWidth",
|
|
4397
5477
|
"type": "`number`",
|
|
4398
|
-
"default": "48",
|
|
4399
|
-
"description": "Width reserved for value-axis labels, px."
|
|
5478
|
+
"default": "48 (36 when the chart is narrower than 400 px)",
|
|
5479
|
+
"description": "Width reserved for value-axis labels, px. When set, it is used at every width."
|
|
4400
5480
|
},
|
|
4401
5481
|
{
|
|
4402
5482
|
"prop": "curveType",
|
|
@@ -4428,6 +5508,12 @@
|
|
|
4428
5508
|
"default": "300",
|
|
4429
5509
|
"description": "Chart height (px number or any CSS length). The width is always 100% of the parent (responsive)."
|
|
4430
5510
|
},
|
|
5511
|
+
{
|
|
5512
|
+
"prop": "aspectRatio",
|
|
5513
|
+
"type": "`number`",
|
|
5514
|
+
"default": "",
|
|
5515
|
+
"description": "Width / height ratio, e.g. `16 / 9`. When provided it replaces `height`, so the chart scales with its container (handy on phones); the empty state uses it too."
|
|
5516
|
+
},
|
|
4431
5517
|
{
|
|
4432
5518
|
"prop": "title",
|
|
4433
5519
|
"type": "`string`",
|
|
@@ -4462,7 +5548,7 @@
|
|
|
4462
5548
|
"prop": "showTooltip",
|
|
4463
5549
|
"type": "`boolean`",
|
|
4464
5550
|
"default": "true",
|
|
4465
|
-
"description": "
|
|
5551
|
+
"description": "Tooltip (themed, dark-mode aware): on hover with a mouse; on touch a tap pins it at the tapped point, and a second tap on the chart or a tap outside unpins it."
|
|
4466
5552
|
},
|
|
4467
5553
|
{
|
|
4468
5554
|
"prop": "className",
|
|
@@ -4553,8 +5639,8 @@
|
|
|
4553
5639
|
{
|
|
4554
5640
|
"prop": "yAxisWidth",
|
|
4555
5641
|
"type": "`number`",
|
|
4556
|
-
"default": "48",
|
|
4557
|
-
"description": "Width reserved for value-axis labels, px."
|
|
5642
|
+
"default": "48 (36 when the chart is narrower than 400 px)",
|
|
5643
|
+
"description": "Width reserved for value-axis labels, px. When set, it is used at every width."
|
|
4558
5644
|
},
|
|
4559
5645
|
{
|
|
4560
5646
|
"prop": "stacked",
|
|
@@ -4580,6 +5666,12 @@
|
|
|
4580
5666
|
"default": "300",
|
|
4581
5667
|
"description": "Chart height (px number or any CSS length). The width is always 100% of the parent (responsive)."
|
|
4582
5668
|
},
|
|
5669
|
+
{
|
|
5670
|
+
"prop": "aspectRatio",
|
|
5671
|
+
"type": "`number`",
|
|
5672
|
+
"default": "",
|
|
5673
|
+
"description": "Width / height ratio, e.g. `16 / 9`. When provided it replaces `height`, so the chart scales with its container (handy on phones); the empty state uses it too."
|
|
5674
|
+
},
|
|
4583
5675
|
{
|
|
4584
5676
|
"prop": "title",
|
|
4585
5677
|
"type": "`string`",
|
|
@@ -4614,7 +5706,7 @@
|
|
|
4614
5706
|
"prop": "showTooltip",
|
|
4615
5707
|
"type": "`boolean`",
|
|
4616
5708
|
"default": "true",
|
|
4617
|
-
"description": "
|
|
5709
|
+
"description": "Tooltip (themed, dark-mode aware): on hover with a mouse; on touch a tap pins it at the tapped point, and a second tap on the chart or a tap outside unpins it."
|
|
4618
5710
|
},
|
|
4619
5711
|
{
|
|
4620
5712
|
"prop": "className",
|
|
@@ -4705,8 +5797,8 @@
|
|
|
4705
5797
|
{
|
|
4706
5798
|
"prop": "yAxisWidth",
|
|
4707
5799
|
"type": "`number`",
|
|
4708
|
-
"default": "48",
|
|
4709
|
-
"description": "Width reserved for value-axis labels, px."
|
|
5800
|
+
"default": "48 (36 when the chart is narrower than 400 px)",
|
|
5801
|
+
"description": "Width reserved for value-axis labels, px. When set, it is used at every width."
|
|
4710
5802
|
},
|
|
4711
5803
|
{
|
|
4712
5804
|
"prop": "curveType",
|
|
@@ -4744,6 +5836,12 @@
|
|
|
4744
5836
|
"default": "300",
|
|
4745
5837
|
"description": "Chart height (px number or any CSS length). The width is always 100% of the parent (responsive)."
|
|
4746
5838
|
},
|
|
5839
|
+
{
|
|
5840
|
+
"prop": "aspectRatio",
|
|
5841
|
+
"type": "`number`",
|
|
5842
|
+
"default": "",
|
|
5843
|
+
"description": "Width / height ratio, e.g. `16 / 9`. When provided it replaces `height`, so the chart scales with its container (handy on phones); the empty state uses it too."
|
|
5844
|
+
},
|
|
4747
5845
|
{
|
|
4748
5846
|
"prop": "title",
|
|
4749
5847
|
"type": "`string`",
|
|
@@ -4778,7 +5876,7 @@
|
|
|
4778
5876
|
"prop": "showTooltip",
|
|
4779
5877
|
"type": "`boolean`",
|
|
4780
5878
|
"default": "true",
|
|
4781
|
-
"description": "
|
|
5879
|
+
"description": "Tooltip (themed, dark-mode aware): on hover with a mouse; on touch a tap pins it at the tapped point, and a second tap on the chart or a tap outside unpins it."
|
|
4782
5880
|
},
|
|
4783
5881
|
{
|
|
4784
5882
|
"prop": "className",
|
|
@@ -5136,6 +6234,12 @@
|
|
|
5136
6234
|
"default": "\"Formatting\"",
|
|
5137
6235
|
"description": "Accessible name of the `role=\"toolbar\"`."
|
|
5138
6236
|
},
|
|
6237
|
+
{
|
|
6238
|
+
"prop": "stickyToolbar",
|
|
6239
|
+
"type": "`boolean`, `number` or `string`",
|
|
6240
|
+
"default": "false",
|
|
6241
|
+
"description": "Keep the toolbar pinned (`sticky`, `z-sticky`) to the top of the scrolling page while long content scrolls. `true` sticks at the top; a number (px) or CSS length sets the offset, e.g. `\"3.5rem\"` below `AppShell`'s sticky top bar. The frame switches from `overflow-hidden` to `overflow-clip` so the toolbar can stick."
|
|
6242
|
+
},
|
|
5139
6243
|
{
|
|
5140
6244
|
"prop": "minHeight",
|
|
5141
6245
|
"type": "`number` or `string`",
|
|
@@ -5203,7 +6307,7 @@
|
|
|
5203
6307
|
"description": "Forwarded to the root `<div>`."
|
|
5204
6308
|
}
|
|
5205
6309
|
],
|
|
5206
|
-
"examples": "### Controlled\n\n```jsx\nconst [body, setBody] = useState('<p>Hello</p>');\n\n<RichTextEditor\n label=\"Description\"\n placeholder=\"Describe the task…\"\n helperText=\"Markdown shortcuts work: **bold**, # heading, - list.\"\n value={body}\n onChange={setBody}\n maxHeight={320}\n/>\n```\n\n### Compact toolbar with validation\n\n```jsx\n<RichTextEditor label=\"Comment\" required toolbar={['bold', 'italic', 'link']} minHeight={80} error={errors.comment} />\n```\n\n### Uncontrolled + ref\n\n```jsx\nconst editorRef = useRef(null);\n\n<RichTextEditor label=\"Notes\" defaultValue=\"<p>Draft</p>\" ref={editorRef} />\n<Button onClick={() => save(editorRef.current.getHTML())}>Save</Button>\n```\n\n### JSON output\n\n```jsx\n<RichTextEditor label=\"Body\" outputFormat=\"json\" onChange={(doc) => setDoc(doc)} />\n```"
|
|
6310
|
+
"examples": "### Controlled\n\n```jsx\nconst [body, setBody] = useState('<p>Hello</p>');\n\n<RichTextEditor\n label=\"Description\"\n placeholder=\"Describe the task…\"\n helperText=\"Markdown shortcuts work: **bold**, # heading, - list.\"\n value={body}\n onChange={setBody}\n maxHeight={320}\n/>\n```\n\n### Compact toolbar with validation\n\n```jsx\n<RichTextEditor label=\"Comment\" required toolbar={['bold', 'italic', 'link']} minHeight={80} error={errors.comment} />\n```\n\n### Uncontrolled + ref\n\n```jsx\nconst editorRef = useRef(null);\n\n<RichTextEditor label=\"Notes\" defaultValue=\"<p>Draft</p>\" ref={editorRef} />\n<Button onClick={() => save(editorRef.current.getHTML())}>Save</Button>\n```\n\n### Sticky toolbar for long documents\n\n```jsx\n// Inside AppShell: stick below its 56px top bar.\n<RichTextEditor label=\"Article\" stickyToolbar=\"3.5rem\" value={body} onChange={setBody} />\n```\n\n### JSON output\n\n```jsx\n<RichTextEditor label=\"Body\" outputFormat=\"json\" onChange={(doc) => setDoc(doc)} />\n```"
|
|
5207
6311
|
}
|
|
5208
6312
|
],
|
|
5209
6313
|
"hooks": [
|
|
@@ -5272,6 +6376,24 @@
|
|
|
5272
6376
|
}
|
|
5273
6377
|
],
|
|
5274
6378
|
"examples": "```jsx\nconst apiMap = {\n getUser: (id) => ({ url: `/users/${id}`, method: 'GET' }),\n updateUser: (id, body) => ({ url: `/users/${id}`, method: 'PUT', data: body }),\n};\n\nfunction Profile({ id }) {\n const { data, loading, error, fetchApi, fetchApiWith } = useApi({\n BASE_URL: 'https://api.example.com',\n DEFAULT_HEADERS: { Authorization: `Bearer ${token}` },\n apiMap,\n });\n\n useEffect(() => { fetchApi('getUser', id); }, [id]);\n\n const save = (body) => fetchApiWith('updateUser', [id, body]);\n // …\n}\n```\n\n[← Component overview](README.md)"
|
|
6379
|
+
},
|
|
6380
|
+
{
|
|
6381
|
+
"name": "useHaptics",
|
|
6382
|
+
"group": "hook",
|
|
6383
|
+
"familyExports": [],
|
|
6384
|
+
"importFrom": "@dreamtree-org/twreact-ui",
|
|
6385
|
+
"peerDependencies": [],
|
|
6386
|
+
"hasDoc": true,
|
|
6387
|
+
"description": "Thin, opt-in wrapper around `navigator.vibrate` with named patterns. It does nothing where vibration is unsupported (iOS Safari, desktop Safari), on the server, while hydrating, and while the user asks for reduced motion, so you can call it unconditionally.",
|
|
6388
|
+
"props": [
|
|
6389
|
+
{
|
|
6390
|
+
"prop": "enabled",
|
|
6391
|
+
"type": "`boolean`",
|
|
6392
|
+
"default": "true",
|
|
6393
|
+
"description": "Gates every `trigger` call; pass your own opt-in flag here"
|
|
6394
|
+
}
|
|
6395
|
+
],
|
|
6396
|
+
"examples": "```jsx\nfunction SaveButton({ onSave }) {\n const { trigger } = useHaptics();\n return (\n <Button\n onClick={async () => {\n try {\n await onSave();\n trigger('success');\n } catch {\n trigger('error');\n }\n }}\n >\n Save\n </Button>\n );\n}\n```\n\n```jsx\n// Built-in opt-in on components\n<Switch haptics rightLabel=\"Wi-Fi\" />\n<ConfirmDialog haptics destructive isOpen={open} title=\"Delete invoice?\" confirmLabel=\"Delete\" onConfirm={remove} onCancel={close} />\n```\n\n[← Component overview](README.md)"
|
|
5275
6397
|
}
|
|
5276
6398
|
],
|
|
5277
6399
|
"store": [
|
|
@@ -5332,73 +6454,84 @@
|
|
|
5332
6454
|
"rich-text"
|
|
5333
6455
|
],
|
|
5334
6456
|
"counts": {
|
|
5335
|
-
"components":
|
|
5336
|
-
"hooks":
|
|
6457
|
+
"components": 67,
|
|
6458
|
+
"hooks": 4,
|
|
5337
6459
|
"utils": 3,
|
|
5338
6460
|
"store": 1
|
|
5339
6461
|
}
|
|
5340
6462
|
},
|
|
5341
6463
|
"docs": {
|
|
5342
6464
|
"Accordion": "# Accordion\n\nExpandable/collapsible content panels. Only one panel open at a time by default, or multiple when `allowMultiple` is true. Supports keyboard navigation (Arrow keys, Home, End, Enter, Space).\n\n## Import\n\n```jsx\nimport { Accordion } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `items` | `Array` | `[]` | Array of `{ id?, title, subtitle?, content, disabled?, leftIcon? }`. `leftIcon` can be ReactNode or `(index, item) => ReactNode`. |\n| `allowMultiple` | `boolean` | `false` | If true, multiple panels can be open at once. |\n| `defaultOpenIndexes` | `number[]` | `[]` | Initial open panel index(es). |\n| `onChange` | `(openIndexes: number[]) => void` | — | Called when open panels change. |\n| `className` | `string` | `\"\"` | Wrapper CSS classes. |\n| `headerClassName` | `string` | `\"\"` | Header button CSS classes. |\n| `contentClassName` | `string` | `\"\"` | Panel content CSS classes. |\n| `chevronPosition` | `\"left\"` \\| `\"right\"` | `\"right\"` | Position of chevron icon. |\n| `chevronIcon` | `ReactNode` | default SVG | Custom chevron icon. |\n| `bordered` | `boolean` | `true` | Whether to show border/background. |\n| `rounded` | `\"none\"` \\| `\"sm\"` \\| `\"md\"` \\| `\"lg\"` \\| `\"xl\"` | `\"md\"` | Border radius of wrapper. |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Header and content padding/text size. |\n| `transitionDuration` | `number` | `220` | Max-height transition duration in ms. Ignored (no animation) when the user prefers reduced motion. |\n\n## Accessibility & behavior\n\n- Header/panel ids are prefixed with a per-instance `useId()`, so several accordions on one page never collide.\n- Collapsed panels carry the `inert` attribute, so Tab never lands on focusable content inside a closed panel.\n- Header focus ring uses `focus-visible:ring-primary-500` (keyboard focus only).\n- Once an open transition finishes, the panel's `max-height` is released to `none`, so content that grows after opening is never clipped.\n- The height transition is disabled under `prefers-reduced-motion: reduce`.\n\n## Example\n\n```jsx\n<Accordion\n items={[\n { id: '1', title: 'Section 1', content: <p>Content for section 1</p> },\n { id: '2', title: 'Section 2', subtitle: 'Optional', content: <p>Content 2</p> },\n { title: 'Disabled', content: <p>Content</p>, disabled: true },\n ]}\n allowMultiple={false}\n defaultOpenIndexes={[0]}\n onChange={(indexes) => console.log('Open:', indexes)}\n size=\"md\"\n chevronPosition=\"right\"\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
5343
|
-
"
|
|
5344
|
-
"
|
|
5345
|
-
"
|
|
5346
|
-
"
|
|
5347
|
-
"
|
|
5348
|
-
"
|
|
5349
|
-
"
|
|
5350
|
-
"
|
|
5351
|
-
"
|
|
5352
|
-
"
|
|
5353
|
-
"
|
|
5354
|
-
"
|
|
6465
|
+
"ActionSheet": "# ActionSheet\n\niOS-style action sheet (the `UIAlertController` action-sheet pattern) on the bottom `Sheet`: an optional title and message, a card of actions — destructive ones in the error tone — and a separate Cancel button. Use it for \"what do you want to do with this?\" choices on phones; it is the same implementation `ThreeDotPopover` and `Table`'s row menu use for their phone presentation.\n\n## Import\n\n```jsx\nimport { ActionSheet } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `open` | `boolean` | — | Controls visibility |\n| `isOpen` | `boolean` | — | Alias of `open` |\n| `onClose` | `(reason) => void` | — | Called when the sheet should close; `reason` is `'select'` (after an action's `onSelect`), `'cancel'`, `'escape'`, `'backdrop'` or `'drag'` |\n| `title` | `ReactNode` | — | Small heading above the actions; names the sheet (`aria-labelledby`) unless `aria-label` is set |\n| `message` | `ReactNode` | — | Muted text under the title; wired to `aria-describedby` |\n| `actions` | `Array` | `[]` | `[{ key?, label, icon?, tone?: 'default' or 'destructive', onSelect, disabled? }]`. `icon` is a node (`<Trash2 />`) or a component (`Trash2`) |\n| `cancelLabel` | `ReactNode` | `\"Cancel\"` | Text of the separate Cancel button (always shown) |\n| `dismissible` | `boolean` | `true` | `false` disables Escape, backdrop click and drag dismissal |\n| `className` | `string` | — | Classes merged onto the sheet panel |\n\nOther props (`aria-label`, `data-*`, …) are forwarded to the sheet panel (`role=\"dialog\"`), and the ref points at it.\n\n### Behavior\n\n- Selecting an enabled action calls its `onSelect`, then `onClose('select')`. Disabled actions do nothing.\n- Cancel, Escape, a backdrop tap and a downward drag all call `onClose` with their reason; none of them calls an action.\n- You own the open state: set `open` to `false` in `onClose`.\n- With no `title` and no `aria-label`, the sheet is named \"Actions\".\n\n## Accessibility\n\n- The panel is a modal `role=\"dialog\"`; focus moves to the first action on open, Tab stays inside, and Escape closes.\n- Focus returns to the element that was focused before the sheet opened (usually the button that opened it).\n- Rows are at least 48px tall with a focus-visible ring; destructive rows pair the error colour with their label text.\n- The slide-in and slide-out animations are off under `prefers-reduced-motion`.\n\n## Examples\n\n```jsx\nfunction PhotoMenu({ photo }) {\n const [open, setOpen] = useState(false);\n return (\n <>\n <Button onClick={() => setOpen(true)}>Options</Button>\n <ActionSheet\n open={open}\n onClose={() => setOpen(false)}\n title=\"Photo options\"\n message=\"Choose what to do with this photo.\"\n actions={[\n { key: 'share', label: 'Share', icon: Share2, onSelect: () => share(photo) },\n { key: 'archive', label: 'Archive', onSelect: () => archive(photo), disabled: photo.archived },\n { key: 'delete', label: 'Delete', icon: Trash2, tone: 'destructive', onSelect: () => remove(photo) },\n ]}\n />\n </>\n );\n}\n```\n\n```jsx\n// Custom cancel text, no title (named by aria-label)\n<ActionSheet\n open={open}\n onClose={() => setOpen(false)}\n aria-label=\"Sort by\"\n cancelLabel=\"Never mind\"\n actions={[\n { label: 'Newest first', onSelect: () => setSort('new') },\n { label: 'Oldest first', onSelect: () => setSort('old') },\n ]}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
6466
|
+
"Alert": "# Alert\n\nInline alert box for success, error, warning, info or neutral messages. Optional title, an actions slot after the description, and a dismiss button. Every variant has a dark tone, and the alert fades in (off under `prefers-reduced-motion`).\n\n## Import\n\n```jsx\nimport { Alert } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `variant` | `string` | `\"info\"` | `success`, `error`, `warning`, `info`, `neutral` (role tokens: surface, line, muted). Unknown values fall back to `info` |\n| `title` | `string` | — | Optional title |\n| `children` | `ReactNode` | — | Alert body content (the description) |\n| `actions` | `ReactNode` | — | Rendered after the description in a wrapping row (buttons, links) |\n| `dismissible` | `boolean` | `false` | Show close button |\n| `onDismiss` | `function` | — | Called when dismissed |\n| `dismissLabel` | `string` | `\"Dismiss\"` | Accessible name of the dismiss button |\n| `className` | `string` | — | Extra CSS classes |\n\nOther props are forwarded to the root `div`; the ref points at it.\n\n## Accessibility\n\n- `error` and `warning` render with `role=\"alert\"` (announced immediately); `info`, `success` and `neutral` use `role=\"status\"` (polite), so routine messages don't interrupt a screen reader. Pass `role` to override.\n- The dismiss button is labelled \"Dismiss\" (`dismissLabel`), has a focus-visible ring and a 44px hit area on touch screens.\n- The enter animation (`animate-fade-in`) is off under `prefers-reduced-motion`.\n\n## Examples\n\n```jsx\n<Alert variant=\"info\" title=\"Note\">Your session will expire in 5 minutes.</Alert>\n<Alert variant=\"success\">Saved successfully.</Alert>\n<Alert variant=\"error\" title=\"Error\">Something went wrong.</Alert>\n<Alert variant=\"warning\" dismissible onDismiss={() => {}}>\n Please review your input.\n</Alert>\n<Alert variant=\"neutral\" title=\"Heads up\">Billing moves to the 1st of each month.</Alert>\n\n<Alert\n variant=\"error\"\n title=\"Upload failed\"\n actions={\n <>\n <Button size=\"sm\" onClick={retry}>Retry</Button>\n <Button size=\"sm\" variant=\"ghost\" onClick={openHelp}>Get help</Button>\n </>\n }\n>\n The file is larger than 10 MB.\n</Alert>\n```\n\n[← Component overview](README.md)\n",
|
|
6467
|
+
"AppShell": "# AppShell\n\nPage-level block: the product app shell — a sidebar on large screens, a top bar with a navigation drawer below `lg`, and an optional bottom nav on phones. Replaces the shell 12 of 13 products hand-rolled (extracted from procurementpro `components/app-shell.tsx`). Colours come from the `shell` and role tokens (`bg-background`, `bg-surface`, `text-foreground`, `text-muted`, `border-line`), so a brand file restyles it and dark mode needs no extra classes.\n\nThe shell fills the dynamic viewport (`min-h-dvh`; the sidebar is `h-dvh`), so mobile browser toolbars don't cut it off. It pads the safe areas: the top bar has `pt-safe` and `px-safe`, the sidebar `pt-safe pb-safe pl-safe`, the content column `px-safe`. Add `viewport-fit=cover` to your viewport meta.\n\n## Import\n\n```jsx\nimport { AppShell } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `brand` | `ReactNode` | — | Product mark at the top of the sidebar |\n| `nav` | `AppShellNavItem[]` | — | `{ id, label, href?, icon?, active?, count?, group?, onClick? }`. Items with `href` render as links; `active` sets `aria-current=\"page\"` |\n| `sidebarFooter` | `ReactNode` | — | Pinned to the bottom of the sidebar (user menu, workspace switcher) |\n| `topbar` | `ReactNode` | — | Right side of the top bar (search, notifications, avatar) |\n| `pageTitle` | `string` | — | Title shown in the mobile top bar |\n| `bottomNav` | `ReactNode` | — | Bottom navigation for phones, usually a [`FootNav`](FootNav.md). Rendered below `md` only (hidden from `md` up, where the sidebar / drawer take over). When set, a `FootNav.Spacer` is added after `<main>` so content doesn't scroll under the bar |\n| `linkAs` | `ElementType` | `\"a\"` | Link element for nav items, e.g. Next.js `Link` |\n| `className` | `string` | — | Extra CSS classes for the root (merged with `cn`) |\n| `children` | `ReactNode` | — | Page content, rendered in `<main id=\"main\">` |\n| ...rest | `HTMLAttributes<HTMLDivElement>` | — | Forwarded to the root `<div>`; the ref is forwarded to it |\n\n## Example\n\n```jsx\n<AppShell\n brand={<span className=\"text-lg font-bold\">PracticePro</span>}\n nav={[\n { id: 'overview', label: 'Overview', href: '/overview', icon: <Home className=\"h-5 w-5\" /> },\n { id: 'compliance', label: 'Compliance', href: '/compliance', active: true },\n { id: 'approvals', label: 'Approvals', href: '/approvals', count: 4 },\n ]}\n topbar={<Avatar name=\"Ananya Rao\" size=\"sm\" />}\n linkAs={Link}\n>\n <PageHeader title=\"Compliance calendar\" />\n</AppShell>\n```\n\n### Bottom nav on phones\n\n```jsx\n<AppShell\n brand={<Logo />}\n nav={nav}\n linkAs={Link}\n bottomNav={\n <FootNav\n linkAs={Link}\n items={[\n { id: 'home', label: 'Home', icon: <Home />, href: '/', active: true },\n { id: 'tasks', label: 'Tasks', icon: <CheckSquare />, href: '/tasks', badge: 3 },\n { id: 'inbox', label: 'Inbox', icon: <Inbox />, href: '/inbox' },\n ]}\n />\n }\n>\n {page}\n</AppShell>\n```\n\nKeep the bottom nav to the 3–5 most used destinations; the full `nav` stays in the drawer (menu button) below `lg` and in the sidebar on desktop.\n\n## Mobile drawer accessibility\n\nBelow `lg`, the \"Open navigation\" button (`aria-haspopup=\"dialog\"`, `aria-expanded`) opens the sidebar in a left [`Sheet`](Sheet.md) (`role=\"dialog\"`, `aria-modal=\"true\"`, `aria-label=\"Navigation\"`), coloured with the shell tokens. While it is open, focus moves into the drawer, Tab / Shift+Tab stay inside it, **Escape** closes it and returns focus to the menu button, and body scroll is locked. The lock is ref-counted and shared with `Dialog`, `Drawer`, `Sheet` and `Sidebar`, so stacked overlays release the page only when the last one closes. Clicking the backdrop, the close button or a nav item, or dragging the drawer to the left, also closes it. It slides in and out (instant under `prefers-reduced-motion`), so it stays mounted for its short exit transition. The menu, close and nav controls grow to 44px on coarse pointers and show a `focus-visible` ring.\n\n[← Component overview](README.md)\n",
|
|
6468
|
+
"AreaChart": "# AreaChart\n\nThemed, responsive area chart (optionally stacked) with token colours, an accessible data-table fallback and reduced-motion support. Wraps `recharts` (optional peer, `/charts` entry point).\n\n## Install\n\nCharts live on a separate entry point so apps that don't use them never install or bundle `recharts`:\n\n```bash\nnpm install recharts\n# React 18 apps: also pin react-is to your React major (recharts resolves it as a peer)\nnpm install react-is@18\n```\n\n## Import\n\n```jsx\nimport { AreaChart } from '@dreamtree-org/twreact-ui/charts';\n```\n\nNever import charts from the root `@dreamtree-org/twreact-ui` — they are not exported there.\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `data` | `object[]` | — | **Required.** Rows to plot. An empty array renders `emptyState`. |\n| `index` | `string` | — | **Required.** Key of the x-axis (category) value in each row, e.g. `\"month\"`. |\n| `categories` | `string[]` | — | **Required.** Keys of the numeric series, one area per key. |\n| `colors` | `ChartColor[]` | `primary, success, warning, error, info, secondary, gray` | Series colours by position (cycled). A token (`\"primary\"`, `\"success-600\"`) resolves to the `--dt-*` CSS variable and follows the theme and dark mode; any other string is used as a CSS colour. |\n| `xAxisFormatter` | `(value) => string` | — | Formats category (x-axis) tick labels and the hidden table's row headers. |\n| `showLegend` | `boolean` | `true` | Legend below the chart (real DOM text). |\n| `showXAxis` | `boolean` | `true` | Show the x-axis. |\n| `showYAxis` | `boolean` | `true` | Show the y-axis. |\n| `showGrid` | `boolean` | `true` | Dashed grid lines (`--dt-line`). |\n| `yAxisWidth` | `number` | `48` (`36` when the chart is narrower than 400 px) | Width reserved for value-axis labels, px. When set, it is used at every width. |\n| `curveType` | `\"linear\"`, `\"monotone\"`, `\"step\"` or `\"natural\"` | `\"monotone\"` | Area interpolation. |\n| `stacked` | `boolean` | `false` | Stack the series on top of each other. |\n| `showDots` | `boolean` | `false` | Draw a dot on every data point. |\n| `connectNulls` | `boolean` | `false` | Draw across `null` gaps. |\n| `valueFormatter` | `(value: number) => string` | `toLocaleString` | Formats values in the tooltip, value axis and hidden data table. |\n| `height` | `number` or `string` | `300` | Chart height (px number or any CSS length). The width is always 100% of the parent (responsive). |\n| `aspectRatio` | `number` | — | Width / height ratio, e.g. `16 / 9`. When provided it replaces `height`, so the chart scales with its container (handy on phones); the empty state uses it too. |\n| `title` | `string` | — | Accessible name: `aria-label` of the `role=\"img\"` region and caption of the hidden table. Falls back to `aria-label`, then `\"Area chart\"`. |\n| `description` | `string` | — | Longer text alternative (the takeaway), linked with `aria-describedby`. |\n| `emptyState` | `ReactNode` | `\"No data to display\"` | Shown (as `role=\"status\"`) instead of the chart when `data` is empty. |\n| `accessibleTable` | `boolean` | `true` | Repeat the numbers in a visually-hidden `<table>` for screen-reader users. |\n| `animate` | `boolean` | `true` | Animate on mount / data change. Always off when the user prefers reduced motion. |\n| `showTooltip` | `boolean` | `true` | Tooltip (themed, dark-mode aware): on hover with a mouse; on touch a tap pins it at the tapped point, and a second tap on the chart or a tap outside unpins it. |\n| `className` | `string` | — | Extra classes for the root `<div>` (merged with `cn`). |\n| `ref` | `Ref<HTMLDivElement>` | — | Forwarded to the root `<div>`. |\n| ...rest | `HTMLAttributes<HTMLDivElement>` | — | Forwarded to the root `<div>` (`id`, `data-*`, `aria-*`, handlers). |\n\n## Small screens\n\n- x-axis ticks keep a wider gap on narrow charts (`minTickGap` 32 px below 360 px wide, 16 px below 640 px, otherwise 8 px), so labels don't collide.\n- The value axis narrows to 36 px below 400 px wide unless you pass `yAxisWidth`.\n- Use `aspectRatio` instead of a fixed `height` to keep the chart proportional to its width.\n\n## Examples\n\n### Revenue vs forecast\n\n```jsx\nconst data = [\n { month: 'Jan', revenue: 4200, costs: 2600 },\n { month: 'Feb', revenue: 5100, costs: 2900 },\n { month: 'Mar', revenue: 4800, costs: 3100 },\n];\n\n<AreaChart data={data} index=\"month\" categories={['revenue', 'costs']} colors={['success', 'gray-400']} title=\"Revenue vs costs\" />\n```\n\n### Stacked\n\n```jsx\n<AreaChart data={data} index=\"month\" categories={['revenue', 'costs']} stacked title=\"Total spend\" />\n```\n\n## Accessibility\n\n| Behavior | Detail |\n|----------|--------|\n| Name | The SVG sits in a `role=\"img\"` region named by `title` (falls back to `aria-label`, then a generic name such as \"Area chart\") |\n| Description | `description` is rendered visually hidden and linked with `aria-describedby` |\n| Numbers | `accessibleTable` (default on) repeats the data in a visually-hidden `<table>` with a caption, column headers and row headers, formatted with `valueFormatter`; recharts' SVG itself has no accessible content |\n| Legend | Real DOM text, not SVG |\n| Motion | Animation is disabled when `prefers-reduced-motion: reduce` is set (and with `animate={false}`) |\n| Colour | Series colours come from the design tokens; pair the chart with the table / description so colour is never the only signal |\n| Dark mode | Axis text (`--dt-muted`), grid (`--dt-line`) and series tokens are re-read when the `dark` class or `data-theme` on `<html>` changes; tooltip and legend use `dark:` variants |\n| Empty | An empty `data` array renders `emptyState` in a `role=\"status\"` box with the same accessible name |\n\n## Server rendering\n\nSafe to render on the server (a Next.js `\"use client\"` page). The first client render matches the server's HTML: token colours stay `rgb(var(--dt-*))` and reduced motion reads as off until hydration finishes, then the live values apply. SVG ids come from `React.useId()` (`dt-chart-…`), not a module counter.\n\n[← Component overview](README.md)\n",
|
|
6469
|
+
"Avatar": "# Avatar\n\nUser avatar: image or initials fallback. Optional status indicator.\n\n## Import\n\n```jsx\nimport { Avatar } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `src` | `string` | — | Image URL |\n| `alt` | `string` | — | Image alt |\n| `name` | `string` | — | Used for initials when no src |\n| `size` | `string` | `\"md\"` | `xs`, `sm`, `md`, `lg`, `xl`, `2xl` |\n| `shape` | `string` | `\"circle\"` | `circle`, `rounded`, `square` |\n| `status` | `string` | — | `online`, `offline`, `away`, `busy`. The dot is ringed in the `surface` colour, so it reads correctly in dark mode |\n| `className` | `string` | — | Extra CSS classes |\n\nIf `src` fails to load, the avatar falls back to the initials (or the user icon) through React state; a new `src` retries the image.\n\n## Example\n\n```jsx\n<Avatar name=\"Alice Smith\" />\n<Avatar src=\"/photo.jpg\" alt=\"User\" />\n<Avatar name=\"Bob\" status=\"online\" size=\"lg\" />\n```\n\nTo stack several avatars with a \"+N\" overflow, use [AvatarGroup](AvatarGroup.md).\n\n[← Component overview](README.md)\n",
|
|
6470
|
+
"AvatarGroup": "# AvatarGroup\n\nOverlapping stack of `Avatar`s (assignees, participants, members). Each avatar is ringed in the `surface` colour so it reads as cut out of the one behind it, in light and dark mode. With `max`, extra avatars collapse into a \"+N\" tile.\n\n## Import\n\n```jsx\nimport { Avatar, AvatarGroup } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | `Avatar` elements. Non-element children (`null`, `false`) are ignored |\n| `max` | `number` | — | Show at most this many avatars, then a \"+N\" overflow tile (`role=\"img\"`, `aria-label=\"N more\"`). Omit to show all |\n| `size` | `string` | `\"md\"` | `xs`, `sm`, `md`, `lg`, `xl`, `2xl`. Passed to every child that doesn't set its own `size`; also sizes the overflow tile and the overlap |\n| `className` | `string` | — | Extra CSS classes on the root (`role=\"group\"`) |\n| ...rest | — | — | Passed to the root `<div>` (e.g. `aria-label=\"Assignees\"`). The ref is forwarded to it |\n\n## Example\n\n```jsx\n<AvatarGroup max={3} size=\"sm\" aria-label=\"Assignees\">\n <Avatar name=\"Asha Rao\" />\n <Avatar name=\"Bilal Khan\" src=\"/bilal.jpg\" />\n <Avatar name=\"Chen Li\" />\n <Avatar name=\"Divya Nair\" />\n <Avatar name=\"Esha Patel\" />\n</AvatarGroup>\n{/* Asha, Bilal, Chen, then a \"+2\" tile */}\n```\n\n[← Component overview](README.md)\n",
|
|
6471
|
+
"Badge": "# Badge\n\nBadge or pill for counts, labels, and status.\n\n## Import\n\n```jsx\nimport { Badge } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `variant` | `string` | `\"default\"` | `default`, `primary`, `secondary`, `success`, `warning`, `error`, `info`, `outline`. `info` uses the `info` palette (`bg-info-100 text-info-800`). Every variant has dark-mode tones; `outline` has its own text colour (`text-gray-700`, `dark:text-gray-200`) |\n| `size` | `string` | `\"md\"` | `xs`, `sm`, `md`, `lg`, `xl`, `2xl` (unknown values fall back to `md`) |\n| `rounded` | `boolean` | `true` | Rounded pill when true |\n| `dot` | `boolean` | `false` | Leading decorative dot in the current text colour (e.g. a \"Live\" badge) |\n| `icon` | `ReactNode` | — | Leading decorative icon slot (`h-3.5 w-3.5`, `aria-hidden`) |\n| `max` | `number` | — | When `children` is a number above `max`, renders `${max}+` (e.g. `max={99}` shows `99+`). Ignored for non-numeric children |\n| `className` | `string` | — | Extra CSS classes |\n| `children` | `ReactNode` | — | Badge content |\n\n## Example\n\n```jsx\n<Badge>New</Badge>\n<Badge variant=\"success\">Active</Badge>\n<Badge variant=\"error\" size=\"sm\">3</Badge>\n<Badge dot variant=\"success\">Live</Badge>\n<Badge icon={<Star />} variant=\"warning\">Featured</Badge>\n<Badge variant=\"error\" max={99}>{120}</Badge> {/* 99+ */}\n```\n\n[← Component overview](README.md)\n",
|
|
6472
|
+
"BarChart": "# BarChart\n\nThemed, responsive bar chart — grouped, stacked or horizontal — with token colours, an accessible data-table fallback and reduced-motion support. Wraps `recharts` (optional peer, `/charts` entry point).\n\n## Install\n\nCharts live on a separate entry point so apps that don't use them never install or bundle `recharts`:\n\n```bash\nnpm install recharts\n# React 18 apps: also pin react-is to your React major (recharts resolves it as a peer)\nnpm install react-is@18\n```\n\n## Import\n\n```jsx\nimport { BarChart } from '@dreamtree-org/twreact-ui/charts';\n```\n\nNever import charts from the root `@dreamtree-org/twreact-ui` — they are not exported there.\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `data` | `object[]` | — | **Required.** Rows to plot. An empty array renders `emptyState`. |\n| `index` | `string` | — | **Required.** Key of the x-axis (category) value in each row, e.g. `\"month\"`. |\n| `categories` | `string[]` | — | **Required.** Keys of the numeric series, one bar per key. |\n| `colors` | `ChartColor[]` | `primary, success, warning, error, info, secondary, gray` | Series colours by position (cycled). A token (`\"primary\"`, `\"success-600\"`) resolves to the `--dt-*` CSS variable and follows the theme and dark mode; any other string is used as a CSS colour. |\n| `xAxisFormatter` | `(value) => string` | — | Formats category (x-axis) tick labels and the hidden table's row headers. |\n| `showLegend` | `boolean` | `true` | Legend below the chart (real DOM text). |\n| `showXAxis` | `boolean` | `true` | Show the x-axis. |\n| `showYAxis` | `boolean` | `true` | Show the y-axis. |\n| `showGrid` | `boolean` | `true` | Dashed grid lines (`--dt-line`). |\n| `yAxisWidth` | `number` | `48` (`36` when the chart is narrower than 400 px) | Width reserved for value-axis labels, px. When set, it is used at every width. |\n| `stacked` | `boolean` | `false` | Stack the series in one bar per category. |\n| `horizontal` | `boolean` | `false` | Horizontal bars: categories on the y-axis (every label shown), values on the x-axis. |\n| `valueFormatter` | `(value: number) => string` | `toLocaleString` | Formats values in the tooltip, value axis and hidden data table. |\n| `height` | `number` or `string` | `300` | Chart height (px number or any CSS length). The width is always 100% of the parent (responsive). |\n| `aspectRatio` | `number` | — | Width / height ratio, e.g. `16 / 9`. When provided it replaces `height`, so the chart scales with its container (handy on phones); the empty state uses it too. |\n| `title` | `string` | — | Accessible name: `aria-label` of the `role=\"img\"` region and caption of the hidden table. Falls back to `aria-label`, then `\"Bar chart\"`. |\n| `description` | `string` | — | Longer text alternative (the takeaway), linked with `aria-describedby`. |\n| `emptyState` | `ReactNode` | `\"No data to display\"` | Shown (as `role=\"status\"`) instead of the chart when `data` is empty. |\n| `accessibleTable` | `boolean` | `true` | Repeat the numbers in a visually-hidden `<table>` for screen-reader users. |\n| `animate` | `boolean` | `true` | Animate on mount / data change. Always off when the user prefers reduced motion. |\n| `showTooltip` | `boolean` | `true` | Tooltip (themed, dark-mode aware): on hover with a mouse; on touch a tap pins it at the tapped point, and a second tap on the chart or a tap outside unpins it. |\n| `className` | `string` | — | Extra classes for the root `<div>` (merged with `cn`). |\n| `ref` | `Ref<HTMLDivElement>` | — | Forwarded to the root `<div>`. |\n| ...rest | `HTMLAttributes<HTMLDivElement>` | — | Forwarded to the root `<div>` (`id`, `data-*`, `aria-*`, handlers). |\n\n## Small screens\n\n- x-axis ticks keep a wider gap on narrow charts (`minTickGap` 32 px below 360 px wide, 16 px below 640 px, otherwise 8 px), so labels don't collide.\n- The value axis narrows to 36 px below 400 px wide unless you pass `yAxisWidth`.\n- Use `aspectRatio` instead of a fixed `height` to keep the chart proportional to its width.\n\n## Examples\n\n### Grouped\n\n```jsx\nconst data = [\n { month: 'Jan', revenue: 4200, costs: 2600 },\n { month: 'Feb', revenue: 5100, costs: 2900 },\n { month: 'Mar', revenue: 4800, costs: 3100 },\n];\n\n<BarChart data={data} index=\"month\" categories={['revenue', 'costs']} colors={['primary', 'warning']} title=\"Monthly revenue and costs\" />\n```\n\n### Stacked, horizontal\n\n```jsx\n<BarChart data={data} index=\"month\" categories={['revenue', 'costs']} stacked horizontal height={240} title=\"Revenue + costs\" />\n```\n\n## Accessibility\n\n| Behavior | Detail |\n|----------|--------|\n| Name | The SVG sits in a `role=\"img\"` region named by `title` (falls back to `aria-label`, then a generic name such as \"Bar chart\") |\n| Description | `description` is rendered visually hidden and linked with `aria-describedby` |\n| Numbers | `accessibleTable` (default on) repeats the data in a visually-hidden `<table>` with a caption, column headers and row headers, formatted with `valueFormatter`; recharts' SVG itself has no accessible content |\n| Legend | Real DOM text, not SVG |\n| Motion | Animation is disabled when `prefers-reduced-motion: reduce` is set (and with `animate={false}`) |\n| Colour | Series colours come from the design tokens; pair the chart with the table / description so colour is never the only signal |\n| Dark mode | Axis text (`--dt-muted`), grid (`--dt-line`) and series tokens are re-read when the `dark` class or `data-theme` on `<html>` changes; tooltip and legend use `dark:` variants |\n| Empty | An empty `data` array renders `emptyState` in a `role=\"status\"` box with the same accessible name |\n\n## Server rendering\n\nSafe to render on the server (a Next.js `\"use client\"` page). The first client render matches the server's HTML: token colours stay `rgb(var(--dt-*))` and reduced motion reads as off until hydration finishes, then the live values apply. SVG ids come from `React.useId()` (`dt-chart-…`), not a module counter.\n\n[← Component overview](README.md)\n",
|
|
6473
|
+
"BottomActionBar": "# BottomActionBar\n\nPage-level block: a bar pinned to the bottom of the screen that holds the page's primary action on forms and checkout pages (\"Save\", \"Place order\", \"Pay now\"). It sits on the `z-sticky` layer, pads the home indicator (`pb-safe`) and the side notches (`px-safe`), and uses role tokens (`bg-surface`, a top `border-line`, a soft upward shadow), so it is dark-mode aware. On phones it **rides above the on-screen keyboard**: it follows `window.visualViewport` (resize and scroll) and lifts itself by the height the keyboard covers. This is SSR-safe: on the server and without `visualViewport` it rests at the bottom.\n\nPut a `<BottomActionBar.Spacer />` at the end of the page content so the last rows aren't hidden behind the bar.\n\n## Import\n\n```jsx\nimport { BottomActionBar } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | The actions, usually one or two `Button`s. Put the primary action last |\n| `position` | `'fixed' \\| 'sticky'` | `\"fixed\"` | `fixed`: pinned to the bottom of the screen (`fixed inset-x-0 bottom-0`); pair it with `BottomActionBar.Spacer`. `sticky`: `sticky bottom-0` inside its scroll container; it keeps its place in the flow and needs no Spacer |\n| `align` | `'start' \\| 'center' \\| 'end' \\| 'between'` | `\"end\"` | Horizontal alignment of the actions. `between` puts e.g. a total on the left and the action on the right |\n| `fullWidth` | `boolean` | `false` | Below `sm` (phones), the actions share the row's width equally |\n| `stackOnMobile` | `boolean` | `false` | Below `sm`, the actions stack full-width with the last (primary) one on top |\n| `keyboardAware` | `boolean` | `true` | Rise above the on-screen keyboard via `window.visualViewport`. While lifted the bar drops its `pb-safe` pad (the keyboard covers the home indicator) and sets `data-keyboard=\"open\"` |\n| `className` | `string` | — | Extra classes for the bar (merged with `cn`, so yours win) |\n| ...rest | `HTMLAttributes<HTMLDivElement>` | — | Forwarded to the bar (`aria-label`, `role`, `data-*`, …). The ref is forwarded to it |\n\nThe bar also sets `data-position=\"fixed\" | \"sticky\"`. Its content row is centred with `max-w-7xl` and the same horizontal padding as `AppShell`'s `<main>`.\n\n## BottomActionBar.Spacer\n\n`<BottomActionBar.Spacer />` renders an `aria-hidden` block as tall as the fixed bar. The bar measures itself (with `ResizeObserver`) and publishes its height as the CSS variable `--dt-bottom-action-bar-height` on `<html>`; the Spacer reads it, so it matches stacked layouts and the safe area. Before the bar has measured, it falls back to `4.5rem` plus the bottom safe area. Accepts `className` (e.g. `sm:hidden` when the bar only shows on phones), `...rest` and a ref.\n\n## Examples\n\n### Form\n\n```jsx\n<form onSubmit={save}>\n <Input label=\"Customer name\" />\n <Input label=\"Amount\" inputMode=\"decimal\" />\n <BottomActionBar.Spacer />\n <BottomActionBar fullWidth>\n <Button variant=\"outline\" onClick={cancel}>Cancel</Button>\n <Button type=\"submit\">Save payment</Button>\n </BottomActionBar>\n</form>\n```\n\n### Checkout\n\n```jsx\n<BottomActionBar align=\"between\" aria-label=\"Checkout\">\n <div>\n <p className=\"text-xs text-muted\">Total</p>\n <p className=\"text-lg font-semibold\">₹4,820.00</p>\n </div>\n <Button>Pay now</Button>\n</BottomActionBar>\n```\n\n### Stacked on phones\n\n```jsx\n<BottomActionBar stackOnMobile>\n <Button variant=\"outline\">Save draft</Button>\n <Button>Send invoice</Button>\n</BottomActionBar>\n```\n\n### Inside a scrolling panel\n\n```jsx\n<div className=\"h-96 overflow-y-auto\">\n {steps}\n <BottomActionBar position=\"sticky\">\n <Button variant=\"outline\">Back</Button>\n <Button>Continue</Button>\n </BottomActionBar>\n</div>\n```\n\n`PageHeader` uses this bar for `actionsPlacement=\"bottom\"`.\n\n## Accessibility\n\n| Behavior | Detail |\n|----------|--------|\n| Semantics | A plain `<div>`; its actions keep their own roles and DOM order, so Tab order is the visual order (with `stackOnMobile` the primary action is shown on top but stays last in Tab order). Pass `role=\"region\"` + `aria-label` if the bar needs a landmark |\n| Keyboard | The bar lifts above the on-screen keyboard, so the action stays visible and tappable while a field is focused |\n| Touch | Use `Button`s, which are 44px tall on coarse pointers |\n| Motion | The lift animates `bottom` for `duration-fast`; off under `prefers-reduced-motion` |\n| Dark mode | `bg-surface`, `border-line` and `text-foreground` switch with the theme; the shadow deepens via `--dt-shadow-strength` |\n\n[← Component overview](README.md)\n",
|
|
6474
|
+
"Breadcrumbs": "# Breadcrumbs\n\nBreadcrumb navigation trail. Optional home link and custom separator.\n\n## Import\n\n```jsx\nimport { Breadcrumbs } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `items` | `Array` | `[]` | `[{ id?, label, href?, icon?, onClick? }]` — `id` is optional; it is the React key when present, otherwise an index-based key is used (no key warning) |\n| `separator` | `ReactNode` | `<ChevronRight />` | Separator between items |\n| `showHome` | `boolean` | `true` | Prepend Home link |\n| `homeIcon` | `ReactNode` | — | Icon for Home |\n| `onHomeClick` | `function` | — | Home click handler |\n| `maxItems` | `number` | — (no collapse) | When the trail (Home included) has more crumbs than this, show the first crumb, an \"…\" button, and the last `maxItems - 1` crumbs. Clicking \"…\" expands the full trail and moves focus to the first revealed crumb. Values below 2 are treated as 2. |\n| `className` | `string` | — | Extra CSS classes |\n\nLong labels truncate with an ellipsis (`max-w-[10rem]`, `sm:max-w-[16rem]`), and the full text stays available as a `title` tooltip when the label is a string. Links underline on hover, show a `focus-visible` ring, and have dark-mode colours. The \"…\" button grows to a 44 px touch target on coarse pointers.\n\n## Example\n\n```jsx\n<Breadcrumbs\n items={[\n { id: '1', label: 'Products', href: '/products' },\n { id: '2', label: 'Category', href: '/products/cat' },\n { id: '3', label: 'Current Page' },\n ]}\n showHome\n/>\n```\n\nCollapsed trail (Home / … / Board / Card):\n\n```jsx\n<Breadcrumbs maxItems={3} items={[\n { id: 'org', label: 'Org', href: '/o' },\n { id: 'team', label: 'Team', href: '/o/t' },\n { id: 'board', label: 'Board', href: '/o/t/b' },\n { id: 'card', label: 'Card' },\n]} />\n```\n\n[← Component overview](README.md)\n",
|
|
6475
|
+
"Button": "# Button\n\nButton with variants, sizes, loading state, and optional left/right icons.\n\n## Import\n\n```jsx\nimport { Button } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `variant` | `string` | `\"primary\"` | `primary`, `secondary`, `outline`, `ghost`, `destructive`, `success`, `warning` |\n| `size` | `string` | `\"md\"` | `xs` (h-7), `sm` (h-8), `md` (h-10), `lg` (h-12), `xl` (h-14), `2xl` (h-16) — every value of the shared `Size` type — plus the square icon-only sizes `icon-sm` (32px), `icon` (40px), `icon-lg` (48px); pair an icon size with `aria-label`. `xs`, `sm`, `icon-sm` and `icon` grow to 44px on coarse pointers (touch) |\n| `disabled` | `boolean` | — | Disable button |\n| `loading` | `boolean` | — | Disable and overlay a centred spinner. The label (and icons) stay in the layout at `opacity-0`, so the button keeps its width and its accessible name; sets `aria-busy=\"true\"` |\n| `leftIcon` | `ReactNode` | — | Icon before children |\n| `rightIcon` | `ReactNode` | — | Icon after children |\n| `fullWidth` | `boolean` | — | Full width |\n| `as` | `ElementType` | `\"button\"` | Render as another element or component with the Button's variants, sizes and focus ring, e.g. `\"a\"` or a router `Link` |\n| `href` | `string` | — | Link target. A bare `href` (no `as`) renders an `<a>` |\n| `className` | `string` | — | Extra CSS classes |\n| ...rest | — | — | Passed to the rendered element (native `<button>` by default; `to`, `target`, `rel` etc. reach a link) |\n\n### As a link\n\n```jsx\n<Button href=\"tel:+919800000000\" variant=\"outline\">Call</Button>\n<Button as=\"a\" href=\"https://wa.me/919800000000\" target=\"_blank\" rel=\"noopener noreferrer\">WhatsApp</Button>\n<Button as={Link} to=\"/move-in\">Move in</Button>\n```\n\nA link can't be `disabled`; with `disabled` or `loading` it gets `aria-disabled=\"true\"`, `tabIndex={-1}`, the disabled styling, and its click is prevented.\n\nEvery variant has `dark:` colours, and the focus ring offset matches a dark page. Buttons shrink slightly while pressed (`active:scale-[0.97]`, off under `prefers-reduced-motion`).\n\n## Examples\n\n### Basic\n\n```jsx\n<Button variant=\"primary\">Save</Button>\n<Button variant=\"outline\">Cancel</Button>\n```\n\n### Sizes and loading\n\n```jsx\n<Button size=\"xs\">Extra small</Button>\n<Button size=\"sm\">Small</Button>\n<Button size=\"lg\">Large</Button>\n<Button size=\"xl\">Extra large</Button>\n<Button loading>Saving...</Button>\n```\n\n### With icons\n\n```jsx\nimport { Download } from 'lucide-react';\n\n<Button leftIcon={<Download className=\"h-4 w-4\" />}>Download</Button>\n```\n\n### Icon-only\n\n```jsx\nimport { Plus } from 'lucide-react';\n\n<Button size=\"icon\" variant=\"outline\" aria-label=\"Add\"><Plus className=\"h-4 w-4\" /></Button>\n<Button size=\"icon-sm\" variant=\"ghost\" aria-label=\"Add\"><Plus className=\"h-4 w-4\" /></Button>\n```\n\n[← Component overview](README.md)\n",
|
|
6476
|
+
"Card": "# Card\n\nCard container with optional hover shadow or a fully interactive (pressable) style. Subcomponents: `Card.Header`, `Card.Title`, `Card.Description`, `Card.Content`, `Card.Footer` for structure. The card and its header/footer dividers use the semantic `bg-surface` / `border-line` tokens, so they follow dark mode (re-theme via `--dt-surface` / `--dt-line`).\n\n## Import\n\n```jsx\nimport { Card } from '@dreamtree-org/twreact-ui';\n```\n\n## Props (Card root)\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Card content. |\n| `className` | `string` | — | Wrapper CSS classes. |\n| `hover` | `boolean` | `false` | If true, add hover:shadow-md transition. |\n| `interactive` | `boolean` | `false` | Pressable card: hover shadow, a slight press shrink (off under reduced motion), a `focus-visible` ring and `cursor-pointer`. With an `onClick` (and no `role` of your own) the card also gets `role=\"button\"` and `tabIndex=0`, and Enter / Space call `onClick`. Without `interactive` the card renders exactly as before. |\n| ...rest | — | — | Passed to root `<div>`. |\n\nCorners use the `rounded-card` radius token (12 px; previously `rounded-lg`, 8 px). Header, Content and Footer pad `px-4` on phones and `px-6` from `sm` up.\n\n## Subcomponents\n\n- **Card.Header**: Wrapper for header (border-bottom, padding). Accepts `children`, `className`.\n- **Card.Title**: `<h3>` for title. Accepts `children`, `className`.\n- **Card.Description**: `<p>` for description. Accepts `children`, `className`.\n- **Card.Content**: Main body (padding). Accepts `children`, `className`.\n- **Card.Footer**: Footer (border-top, padding). Accepts `children`, `className`.\n\n## Example\n\n```jsx\n<Card hover>\n <Card.Header>\n <Card.Title>Card title</Card.Title>\n <Card.Description>Optional description text.</Card.Description>\n </Card.Header>\n <Card.Content>\n Main content here.\n </Card.Content>\n <Card.Footer>\n <Button>Action</Button>\n </Card.Footer>\n</Card>\n```\n\n[← Component overview](README.md)\n",
|
|
6477
|
+
"Carousel": "# Carousel\n\nHorizontal carousel of slides built on native CSS scroll-snap. Renders `children` as slides (one per child). Native momentum swipe on touch, optional auto-play, always-visible previous/next buttons, and dot indicators. Auto-play pauses on hover, touch and keyboard focus, and is off under reduced motion.\n\n## Import\n\n```jsx\nimport { Carousel } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Each child is one slide. |\n| `autoPlay` | `boolean` | `false` | Auto-advance slides. |\n| `interval` | `number` | `5000` | Auto-play interval in ms. |\n| `showDots` | `boolean` | `true` | Show dot indicators at bottom. |\n| `className` | `string` | — | Wrapper CSS classes. |\n| `itemClassName` | `string` | — | Each slide wrapper CSS classes. |\n| `...rest` | `HTMLAttributes<HTMLDivElement>` | — | Forwarded to the root `<div>` (`aria-*`, `data-*`, `id`, …); `onMouseEnter` / `onMouseLeave` / `onFocus` / `onBlur` / `onTouchStart` / `onTouchEnd` are composed with the autoplay pause. Pass `aria-label` to name the carousel (default `\"Carousel\"`). |\n\n## Behavior\n\n- Slides sit in a horizontal track with `scroll-snap-type: x mandatory` (`snap-x snap-mandatory`), so touch users get native momentum swipe; the scrollbar is hidden. The active slide is tracked with an `IntersectionObserver` (the slide at least 60% in view), so swiping updates the dots and the announcement.\n- Arrows, dots and the arrow keys (with the track focused) scroll the track with `scrollTo`; `behavior` is `'smooth'`, or `'auto'` under `prefers-reduced-motion`. Previous/Next wrap around.\n- Accessibility: the root is `role=\"region\"` with `aria-roledescription=\"carousel\"`; each slide is `role=\"group\"` with `aria-roledescription=\"slide\"` and an `\"n of m\"` label. A visually hidden polite live region announces \"Slide n of m\"; it is `aria-live=\"off\"` while auto-play is running.\n- Dots are 44 px tall tap targets with a small visual dot inside; the active dot has `aria-current=\"true\"`.\n- Previous/Next buttons (labelled \"Previous slide\" / \"Next slide\") are always visible — including on touch devices — and grow to a 44×44 px touch target on coarse pointers.\n- Styling uses the role tokens (`bg-surface`, `border-line`, `text-foreground`, `bg-muted`) and `bg-primary-600` for the active dot, so it follows light/dark theming; transitions honour `prefers-reduced-motion`.\n- Dots are clickable to jump to a slide.\n- Auto-play pauses while the pointer hovers, while a finger is on the carousel, and while focus is inside it; it resumes when that ends. Under `prefers-reduced-motion` auto-play does not run at all.\n\n## Example\n\n```jsx\n<Carousel autoPlay interval={5000} showDots>\n <div className=\"h-64 bg-gray-100 flex items-center justify-center\">Slide 1</div>\n <div className=\"h-64 bg-gray-200 flex items-center justify-center\">Slide 2</div>\n <div className=\"h-64 bg-gray-300 flex items-center justify-center\">Slide 3</div>\n</Carousel>\n```\n\n[← Component overview](README.md)\n",
|
|
6478
|
+
"Checkbox": "# Checkbox\n\nCheckbox with label, sizes, and optional indeterminate state.\n\n## Import\n\n```jsx\nimport { Checkbox } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `id` | `string` | — | Input id |\n| `name` | `string` | — | Form name |\n| `label` | `string` | — | Label text |\n| `checked` | `boolean` | — | Controlled checked |\n| `defaultChecked` | `boolean` | `false` | Uncontrolled default |\n| `onChange` | `function` | — | Change handler |\n| `disabled` | `boolean` | `false` | Disable |\n| `required` | `boolean` | `false` | Required |\n| `size` | `string` | `\"md\"` | `sm`, `md`, `lg` |\n| `labelPosition` | `string` | `\"right\"` | `left`, `right` |\n| `value` | `any` | — | Value when checked |\n| `indeterminate` | `boolean` | `false` | Mixed state: fills the box with a dash and sets `aria-checked=\"mixed\"` |\n| `color` | `string` | `\"primary\"` | Token preset (`primary`, `secondary`, `success`, `error`, `warning`; aliases `green`, `danger`, `red`) or a custom CSS colour string. The raw-palette presets `blue`, `indigo`, `purple` and the `borderColor` prop were **removed in 2.0.0** ([migration](MIGRATION-2.0.md#checkbox-and-switch-raw-colour-presets)); the box border is always the `border-gray-300` token. |\n| `iconClass` | `string` | — | Extra classes for the check icon |\n| `checkedClass` | `string` | — | Extra classes applied when checked |\n| `wrapperProps` | `object` | — | Extra props spread onto the outer wrapper element |\n| `haptics` | `boolean` | `false` | Opt-in: a `selection` vibration (10ms) on every change. No-op without `navigator.vibrate` (iOS Safari) and under reduced motion. See [useHaptics](useHaptics.md) |\n\n## Behavior\n\n- The check and the indeterminate dash scale/fade in (`duration-fast`, off under `prefers-reduced-motion`).\n- Keyboard focus draws a `focus-visible` ring on the box (no ring after a mouse click).\n- On coarse pointers the invisible input is a 44×44 hit area centred on the box.\n- Surfaces use tokens (`bg-surface`, `border-gray-300` / `dark:border-gray-600`) and follow dark mode.\n\n## Example\n\n```jsx\n<Checkbox label=\"Accept terms\" checked={checked} onChange={e => setChecked(e.target.checked)} />\n<Checkbox label=\"Remember me\" defaultChecked />\n```\n\n[← Component overview](README.md)\n",
|
|
6479
|
+
"ColorPicker": "# ColorPicker\n\nColor picker with optional swatches, hex input, HSL sliders, and alpha. Value can be hex or rgba string; `onChange` receives normalized `rgba(r,g,b,a)` string.\n\n## Import\n\n```jsx\nimport { ColorPicker } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `string` | — | Controlled value (hex or rgba string). |\n| `defaultValue` | `string` | — | Uncontrolled initial value. |\n| `onChange` | `(rgbaString: string) => void` | — | Called with normalized `rgba(r,g,b,a)` string. |\n| `swatches` | `string[]` | — | Array of hex/rgba strings for preset swatches. |\n| `showAlpha` | `boolean` | `false` | Show the alpha slider (the library `Slider`, labelled \"Alpha\", value shown as a percentage). When off (or when the alpha channel is fully opaque), the displayed hex is the 6-char `#rrggbb` form; the 8-char `#rrggbbff` form is shown only when `showAlpha` is on **and** alpha < 1. |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Picker size. Other `Size` values (`xs`, `xl`, `2xl`) fall back to `md` (they used to crash). |\n| `disabled` | `boolean` | — | Disable the picker. |\n| `className` | `string` | — | Wrapper CSS classes. |\n| `label` | `string` | `\"Pick a color\"` | Label text. Associated with the color value text input (`htmlFor`). |\n| `id` | `string` | auto (`useId`) | Id of the color value text input; the label's `htmlFor` points at it. |\n\n## Example\n\n```jsx\nconst [color, setColor] = useState('#3b82f6');\n<ColorPicker\n value={color}\n onChange={setColor}\n swatches={['#ef4444', '#22c55e', '#3b82f6']}\n showAlpha\n label=\"Background\"\n/>\n```\n\n## Notes\n\n- Forwards `ref` to the root wrapper element and spreads unknown props (`...rest`,\n e.g. `data-*`) onto it. `className` is merged via `cn()` so caller classes win\n on Tailwind conflicts.\n- The default render (no `showAlpha`) displays the 6-char `#rrggbb` value.\n- The alpha control is the core [`Slider`](Slider.md) (`role=\"slider\"`, keyboard,\n pointer and touch drag), replacing the old native range input and its fixed\n 144 px label column. When disabled the thumb is `aria-disabled=\"true\"`.\n- The swatch matching the current colour is marked with `aria-pressed=\"true\"`\n and a `ring-2 ring-primary-500` ring; the others have `aria-pressed=\"false\"`.\n Swatches sit in a `role=\"group\"` labelled \"Swatches\".\n- Layout stacks on phones: the 220 px minimum width of the value panel applies\n only from the `sm` breakpoint up. Borders and the text input use the\n `border-line` / `bg-surface` / `text-foreground` role tokens (dark-mode safe).\n\n[← Component overview](README.md)\n",
|
|
5355
6480
|
"Condition": "# Condition\n\nRenders `children` when a condition is true, otherwise renders `fallback`. Simple conditional wrapper with no extra DOM when not needed.\n\n## Import\n\n```jsx\nimport { Condition } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `condition` | `boolean` | — | When true, render `children`; when false, render `fallback`. |\n| `children` | `ReactNode` | — | Content when condition is true. |\n| `fallback` | `ReactNode` | `null` | Content when condition is false. |\n\n## Example\n\n```jsx\n<Condition condition={isLoggedIn}>\n <Dashboard />\n</Condition>\n\n<Condition condition={loading} fallback={<Loader />}>\n <Content />\n</Condition>\n\n<Condition condition={hasError} fallback={<Alert variant=\"error\">Error</Alert>}>\n <Data />\n</Condition>\n```\n\n[← Component overview](README.md)\n",
|
|
5356
|
-
"ConfirmDialog": "# ConfirmDialog\n\nPage-level block: the confirmation step for delete, remove, archive or cancel — replaces native `confirm()`. The title asks the question, one line says what happens, and the action button is named with the verb. Extracted from firepro `components/ui/confirm-dialog.tsx`.\n\n## Import\n\n```jsx\nimport { ConfirmDialog } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `isOpen` | `boolean` | — | Whether the dialog is open |\n| `title` | `string` | — | The action as a question |\n| `description` | `ReactNode` | — | One line saying what will happen |\n| `confirmLabel` | `string` | — | The verb, e.g. \"Remove\" (never \"OK\") |\n| `cancelLabel` | `string` | `\"Cancel\"` | Cancel button label |\n| `destructive` | `boolean` | `false` | Red confirm button for actions that can't be undone |\n| `loading` | `boolean` | `false` | Busy confirm button; the dialog can't be dismissed |\n| `onConfirm` | `() => void` | — | Called on confirm |\n| `onCancel` | `() => void` | — | Called on cancel, Esc or backdrop click |\n\n## Example\n\n```jsx\n<ConfirmDialog\n isOpen={open}\n title=\"Remove Kiran M from Diwali buyers?\"\n description=\"They stop receiving this list's campaigns.\"\n confirmLabel=\"Remove\"\n destructive\n onConfirm={remove}\n onCancel={() => setOpen(false)}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
5357
|
-
"DatePicker": "# DatePicker\n\nSingle date picker with calendar popup. Accepts `Date` or ISO date string; supports min/max, clear button, and optional portal rendering.\n\n## Import\n\n```jsx\nimport { DatePicker } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `Date` \\| `string` | — | Selected date (Date or ISO string). |\n| `onChange` | `(date: Date \\| null) => void` | — | Called when date changes. |\n| `placeholder` | `string` | `\"Select date...\"` | Input placeholder. |\n| `label` | `string` | — | Label text. Associated with the trigger (`<label for>` + `aria-labelledby`), so the accessible name is the label followed by the displayed value. |\n| `error` | `string` | — | Error message. Sets `aria-invalid`; replaces `helperText` while shown. |\n| `helperText` | `ReactNode` | — | Hint below the trigger, linked via `aria-describedby`. Hidden while `error` is shown. |\n| `disabled` | `boolean` | — | Disable the picker. |\n| `required` | `boolean` | — | Required field. |\n| `minDate` | `Date` | — | Minimum selectable date. |\n| `maxDate` | `Date` | — | Maximum selectable date. |\n| `className` | `string` | — | Wrapper CSS classes. |\n| `weekStartsOn` | `0` \\| `1` | `0` | 0 = Sunday, 1 = Monday. |\n| `portal` | `boolean` | `false` | Render calendar in `document.body`. |\n| `displayFormat` | `string` | `\"MMM dd, yyyy\"` | date-fns format for display. |\n| `locale` | `Locale` | — | date-fns locale. |\n| `showClear` | `boolean` | `true` | Show the clear controls: a \"Clear date\" button overlaid on the field (a sibling `<button>`, keyboard-reachable) when a value is set, plus \"Clear\" in the
|
|
5358
|
-
"DateRangePicker": "# DateRangePicker\n\nDate range picker: user selects start then end. Value is `{ start: Date, end: Date }`.
|
|
5359
|
-
"Dialog": "# Dialog\n\nModal dialog with title, body, optional footer, and configurable size. Supports escape key and backdrop click to close when dismissible.\n\n## Import\n\n```jsx\nimport { Dialog } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `isOpen` | `boolean` | — | Controls visibility |\n| `onClose` | `function` | — | Called when dialog should close |\n| `title` | `string` | — | Dialog title |\n| `children` | `ReactNode` | — | Body content |\n| `size` | `string` | `\"md\"` | `sm`, `md`, `lg`, `xl`, `full` |\n| `dismissible` | `boolean` | `true` | Allow close via backdrop or Escape |\n| `showCloseButton` | `boolean` | `true` | Show X button (44×44px touch target on coarse pointers) |\n| `footer` | `ReactNode` | — | Footer content (e.g. buttons) |\n| `className` | `string` | — | Extra CSS classes |\n| `onOpen` | `function` | — | Called when dialog opens (
|
|
6481
|
+
"ConfirmDialog": "# ConfirmDialog\n\nPage-level block: the confirmation step for delete, remove, archive or cancel — replaces native `confirm()`. The title asks the question, one line says what happens, and the action button is named with the verb. Extracted from firepro `components/ui/confirm-dialog.tsx`.\n\n## Import\n\n```jsx\nimport { ConfirmDialog } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `isOpen` | `boolean` | — | Whether the dialog is open |\n| `title` | `string` | — | The action as a question |\n| `description` | `ReactNode` | — | One line saying what will happen |\n| `confirmLabel` | `string` | — | The verb, e.g. \"Remove\" (never \"OK\") |\n| `cancelLabel` | `string` | `\"Cancel\"` | Cancel button label |\n| `destructive` | `boolean` | `false` | Red confirm button for actions that can't be undone |\n| `loading` | `boolean` | `false` | Busy confirm button; the dialog can't be dismissed |\n| `onConfirm` | `() => void` | — | Called on confirm |\n| `onCancel` | `() => void` | — | Called on cancel, Esc or backdrop click |\n| `presentation` | `string` | `\"auto\"` | `auto`, `popover`, `sheet`. As [Dialog](Dialog.md#presentation): bottom sheet on coarse-pointer phones, centred modal elsewhere |\n| `haptics` | `boolean` | `false` | Opt-in vibration: `warning` once when it opens with `destructive`, `success` when the confirm button is pressed. Cancel stays silent. No-op without `navigator.vibrate` (iOS Safari) and under reduced motion. See [useHaptics](useHaptics.md) |\n\n`ref`, `className` and other props are forwarded to the underlying [Dialog](Dialog.md) panel.\n\n## Layout\n\n- **Centred modal:** the buttons sit in a row on the right from `sm` up and stack below it.\n- **Sheet:** both buttons are full width and stacked. The confirm button is on\n top and Cancel is below it. In the DOM, and so in Tab order, Cancel comes\n first, as in the centred layout.\n\n## Example\n\n```jsx\n<ConfirmDialog\n isOpen={open}\n title=\"Remove Kiran M from Diwali buyers?\"\n description=\"They stop receiving this list's campaigns.\"\n confirmLabel=\"Remove\"\n destructive\n onConfirm={remove}\n onCancel={() => setOpen(false)}\n/>\n\n// Keep the centred modal on phones too:\n<ConfirmDialog\n presentation=\"popover\"\n isOpen={open}\n title=\"Archive this list?\"\n confirmLabel=\"Archive\"\n onConfirm={archive}\n onCancel={() => setOpen(false)}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
6482
|
+
"DatePicker": "# DatePicker\n\nSingle date picker with calendar popup. Accepts `Date` or ISO date string; supports min/max, clear button, and optional portal rendering.\n\n## Import\n\n```jsx\nimport { DatePicker } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `Date` \\| `string` | — | Selected date (Date or ISO string). |\n| `onChange` | `(date: Date \\| null) => void` | — | Called when date changes. |\n| `placeholder` | `string` | `\"Select date...\"` | Input placeholder. |\n| `label` | `string` | — | Label text. Associated with the trigger (`<label for>` + `aria-labelledby`), so the accessible name is the label followed by the displayed value. |\n| `error` | `string` | — | Error message. Sets `aria-invalid`; replaces `helperText` while shown. |\n| `helperText` | `ReactNode` | — | Hint below the trigger, linked via `aria-describedby`. Hidden while `error` is shown. |\n| `disabled` | `boolean` | — | Disable the picker. |\n| `required` | `boolean` | — | Required field. |\n| `minDate` | `Date` | — | Minimum selectable date. |\n| `maxDate` | `Date` | — | Maximum selectable date. |\n| `className` | `string` | — | Wrapper CSS classes. |\n| `weekStartsOn` | `0` \\| `1` | `0` | 0 = Sunday, 1 = Monday. |\n| `portal` | `boolean` | `false` | Render calendar in `document.body`. |\n| `displayFormat` | `string` | `\"MMM dd, yyyy\"` | date-fns format for display. |\n| `locale` | `Locale` | — | date-fns locale. |\n| `showClear` | `boolean` | `true` | Show the clear controls: a \"Clear date\" button overlaid on the field (a sibling `<button>`, keyboard-reachable) when a value is set, plus \"Clear\" in the calendar. |\n| `closeOnSelect` | `boolean` | `true` | Close popup on date select. |\n| `presentation` | `'auto'` \\| `'popover'` \\| `'sheet'` | `'auto'` | How the calendar opens. `auto` uses a bottom `Sheet` on coarse-pointer screens under 640px and an anchored popover elsewhere (popover on the server). Pass `'popover'` to keep the desktop popup everywhere. |\n\n## Presentation\n\n- **Popover** — anchored to the field by the shared floating engine (flips and\n shifts to stay on screen). Fluid width `min(20rem, 100vw - 2rem)`, so it never\n overflows a 320px screen. Stacks on the `z-popover` layer.\n- **Sheet** — the calendar opens in a bottom [`Sheet`](Sheet.md) titled with\n `label` (or \"Select date\"). Day cells are 44px tall, swiping the grid left /\n right changes month (vertical drags still scroll and drag the sheet), and\n \"Clear\" / \"Today\" sit in the sticky footer. Backdrop, Escape and drag-down\n close it.\n\n## Keyboard & accessibility\n\n- The calendar grid uses a **roving tabindex**: exactly one day is tabbable —\n the last keyboard-focused day, else the selected day, else today (when in the\n visible month and enabled), else the first enabled day. Opening the popup\n moves focus to that day.\n- `ArrowLeft` / `ArrowRight` move by one day, `ArrowUp` / `ArrowDown` by one\n week, `PageUp` / `PageDown` by one month, `Home` / `End` to the start / end of\n the week. Moving past the month edge switches the visible month. Disabled\n (out-of-range) days are never focused.\n- `Enter` / `Space` select the focused day; `Escape` closes the popup and\n returns focus to the field.\n- Previous / next month buttons grow to a 44×44 px touch target on coarse\n pointers.\n\n## Example\n\n```jsx\nconst [date, setDate] = useState(null);\n<DatePicker\n value={date}\n onChange={setDate}\n placeholder=\"Select date\"\n minDate={new Date()}\n label=\"Start date\"\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
6483
|
+
"DateRangePicker": "# DateRangePicker\n\nDate range picker: user selects start then end. Value is `{ start: Date, end: Date }`. The popover stays open on outside click / Escape until both are selected (or the user clears); the trigger always toggles it, and closing mid-pick drops the uncommitted start. Supports min/max, clear, preset chips, optional portal, and a phone-native bottom sheet.\n\n## Import\n\n```jsx\nimport { DateRangePicker } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `{ start: Date\\|string, end: Date\\|string }` | — | Selected range. |\n| `onChange` | `(range: { start, end } \\| null) => void` | — | Called when range changes. |\n| `placeholder` | `string` | `\"Select date range...\"` | Input placeholder. |\n| `label` | `string` | — | Label text. Associated with the trigger (`<label for>` + `aria-labelledby`), so the accessible name is the label followed by the displayed value. |\n| `error` | `string` | — | Error message. Sets `aria-invalid`; replaces `helperText` while shown. |\n| `helperText` | `ReactNode` | — | Hint below the trigger, linked via `aria-describedby`. Hidden while `error` is shown. |\n| `disabled` | `boolean` | — | Disable. |\n| `required` | `boolean` | — | Required. |\n| `minDate` | `Date` | — | Minimum selectable date. |\n| `maxDate` | `Date` | — | Maximum selectable date. |\n| `className` | `string` | — | Wrapper CSS classes. |\n| `weekStartsOn` | `0` \\| `1` | `0` | 0 = Sunday, 1 = Monday. |\n| `portal` | `boolean` | `false` | Render popup in document.body. |\n| `displayFormat` | `string` | `\"MMM dd, yyyy\"` | date-fns format for display. |\n| `locale` | `Locale` | — | date-fns locale. |\n| `showClear` | `boolean` | `true` | Show the clear button (`aria-label=\"Clear range\"`) once a date is picked. It sits beside the trigger, not inside it, and is hidden while `disabled`. |\n| `closeOnSelect` | `boolean` | `true` | Close when both dates selected (popover). The sheet always closes on \"Apply\". |\n| `presentation` | `'auto'` \\| `'popover'` \\| `'sheet'` | `'auto'` | How the calendar opens. `auto` uses a bottom `Sheet` on coarse-pointer screens under 640px and an anchored popover elsewhere (popover on the server). Pass `'popover'` to keep the desktop popup everywhere. |\n| `presets` | `{ label: string, range: [Date\\|string, Date\\|string] }[]` | — | Quick-pick chips above the calendar. In the popover a chip commits its range (`onChange`) at once; in the sheet it fills the draft for \"Apply\". The chip matching the current selection has `aria-pressed=\"true\"`. |\n\n## Presentation\n\n- **Popover** — anchored to the field by the shared floating engine (flips and\n shifts to stay on screen). Fluid width `min(20rem, 100vw - 2rem)`, so it never\n overflows a 320px screen. Stacks on the `z-popover` layer. Clicking the second\n date commits the range.\n- **Sheet** — the calendar opens in a bottom [`Sheet`](Sheet.md) titled with\n `label` (or \"Select date range\"). Day cells are 44px tall and swiping the grid\n left / right changes month (vertical drags still scroll and drag the sheet).\n Tap the start, then the end: the days in between are highlighted straight\n away, no hover needed. Picks are a **draft** until the sticky-footer\n **Apply** (`onChange({ start, end })`); **Clear** fires `onChange(null)`.\n Dismissing the sheet (backdrop, Escape, drag down, close button) discards the\n draft.\n\n## Keyboard & accessibility\n\n- The grid is `role=\"grid\"` (`aria-multiselectable`) with `role=\"row\"` weeks.\n Each day is a `gridcell` button labelled with its full date (for example\n \"Friday, 30 October 2026\", formatted with `locale`), `aria-selected` on the\n start, end and in-range days, and `aria-current=\"date\"` on today.\n- **Roving tabindex**: one day is tabbable — the last focused day, else the\n range end / start, else today, else the first enabled day. Opening the popover\n focuses it.\n- `ArrowLeft` / `ArrowRight` move by one day, `ArrowUp` / `ArrowDown` by one\n week, `PageUp` / `PageDown` by one month, `Home` / `End` to the start / end of\n the week; crossing the month edge switches the visible month. Disabled days\n are never focused. `Enter` / `Space` pick the focused day.\n- While picking the end, the range up to the hovered **or keyboard-focused** day\n is previewed.\n- `Escape` closes the popover (ignored while only a start is picked) and returns\n focus to the field.\n\n## Example\n\n```jsx\nconst [range, setRange] = useState({ start: null, end: null });\n<DateRangePicker value={range} onChange={setRange} label=\"Period\" />\n```\n\nWith presets:\n\n```jsx\nconst day = 24 * 60 * 60 * 1000;\n<DateRangePicker\n value={range}\n onChange={setRange}\n label=\"Report period\"\n presets={[\n { label: 'Last 7 days', range: [new Date(Date.now() - 6 * day), new Date()] },\n { label: 'Last 30 days', range: [new Date(Date.now() - 29 * day), new Date()] },\n ]}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
6484
|
+
"Dialog": "# Dialog\n\nModal dialog with title, body, optional footer, and configurable size. Supports escape key and backdrop click to close when dismissible. On phones it presents as a bottom [Sheet](Sheet.md) by default (`presentation=\"auto\"`).\n\n## Import\n\n```jsx\nimport { Dialog } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `isOpen` | `boolean` | — | Controls visibility |\n| `onClose` | `function` | — | Called when dialog should close |\n| `title` | `string` | — | Dialog title |\n| `children` | `ReactNode` | — | Body content |\n| `size` | `string` | `\"md\"` | `sm`, `md`, `lg`, `xl`, `full` |\n| `dismissible` | `boolean` | `true` | Allow close via backdrop or Escape |\n| `showCloseButton` | `boolean` | `true` | Show X button (44×44px touch target on coarse pointers) |\n| `footer` | `ReactNode` | — | Footer content (e.g. buttons) |\n| `className` | `string` | — | Extra CSS classes |\n| `onOpen` | `function` | — | Called when dialog opens, with `{ dialogRef }` (the panel node) |\n| `presentation` | `string` | `\"auto\"` | `auto`, `popover`, `sheet`. `popover` = the centred modal; `sheet` = a bottom sheet; `auto` = sheet on coarse-pointer screens under 640px, centred modal elsewhere (and on the server) |\n| `ref` | `Ref<HTMLDivElement>` | — | Forwarded to the panel (`role=\"dialog\"`) in both presentations |\n\nAny other props (`data-*`, `aria-*`, …) are spread onto the panel.\n\nThe close (X) button grows to a 44×44px touch target on coarse pointers (`@media (pointer: coarse)`); mouse layouts keep the compact 28px button.\n\n## Presentation\n\n`presentation=\"auto\"` (the default) resolves through the shared\n`(pointer: coarse) and (max-width: 639px)` query: phones get a sheet, desktops\nand tablets get the centred modal. Pass `presentation=\"popover\"` to keep the\ncentred modal everywhere (the pre-1.5 behaviour), or `\"sheet\"` to force a sheet.\nThe panel carries `data-presentation=\"popover\" | \"sheet\"`.\n\n**Centred modal (`popover`)**\n\n- The panel is capped at the viewport height minus a 1rem gutter and the safe\n areas (`max-h-[calc(100dvh-2rem-env(safe-area-inset-top)-env(safe-area-inset-bottom))]`).\n Header and footer stay fixed; the body scrolls (`overflow-y-auto overscroll-contain`).\n Because the body scrolls, an absolutely positioned dropdown you render\n *inside* the body is clipped. The library's Select, DatePicker and Popover\n portal their menus, so they are not affected.\n- The backdrop keeps a 1rem gutter plus `env(safe-area-inset-*)` on every side.\n- It animates in (`animate-scale-in` + backdrop `animate-fade-in`) and out\n (`animate-scale-out` + `animate-fade-out`). It stays mounted for the ~150ms\n exit (`data-state=\"exiting\"`, pointer events off) and then unmounts. Under\n `prefers-reduced-motion` there is no animation and it unmounts at once.\n\n**Bottom sheet (`sheet`)**\n\n- Renders the public `Sheet` at `placement=\"bottom\"`, content height, no snap\n points, with the grab handle. Drag-to-dismiss works only when `dismissible`;\n a non-dismissible sheet springs back.\n- `title` becomes the sheet title (`aria-labelledby`). With no title,\n `showCloseButton` still renders a \"Close dialog\" button in a small header row.\n With a title, the sheet's own close button is used (labelled \"Close sheet\").\n- The body scrolls inside the sheet. `size` does not apply: the sheet is full width.\n- The `footer` stacks its children full width with `flex-col-reverse`. Put the\n actions in the usual desktop order, secondary first, then primary\n (`<Cancel/><Save/>`). The primary then shows **on top**, as on iOS, while\n DOM and Tab order stay secondary → primary, as on desktop.\n- `onClose` is called with no arguments, as in the centred modal.\n\n`Escape`, backdrop click and `dismissible` behave the same in both presentations.\n\n## Examples\n\n### Basic\n\n```jsx\nconst [open, setOpen] = useState(false);\n\n<Button onClick={() => setOpen(true)}>Open</Button>\n<Dialog\n isOpen={open}\n onClose={() => setOpen(false)}\n title=\"Confirm\"\n>\n Are you sure you want to continue?\n</Dialog>\n```\n\n### Always centred / always a sheet\n\n```jsx\n<Dialog presentation=\"popover\" isOpen={open} onClose={close} title=\"Centred everywhere\">\n Same centred modal on phones too.\n</Dialog>\n\n<Dialog presentation=\"sheet\" isOpen={open} onClose={close} title=\"Sheet everywhere\">\n A bottom sheet on desktop too.\n</Dialog>\n```\n\n### With footer\n\n```jsx\n<Dialog\n isOpen={open}\n onClose={() => setOpen(false)}\n title=\"Edit\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={() => setOpen(false)}>Cancel</Button>\n <Button onClick={handleSave}>Save</Button>\n </>\n }\n>\n <Input label=\"Name\" value={name} onChange={e => setName(e.target.value)} />\n</Dialog>\n```\n\n## Accessibility & keyboard\n\n`Dialog` renders `role=\"dialog\"` + `aria-modal=\"true\"` and backs that promise\nwith real focus management (no extra dependency — a built-in focus trap):\n\n| Behavior | Detail |\n|----------|--------|\n| Focus on open | Focus moves to the first focusable element inside the panel (or the panel itself when there is none). |\n| Focus trap | `Tab` / `Shift`+`Tab` cycle only through focusable elements inside the panel; focus cannot reach the page behind the modal. |\n| Focus restore | On close, focus returns to the element that had it when the dialog opened (typically the trigger). |\n| `Escape` | Closes the dialog when `dismissible` (default); ignored when `dismissible={false}`. |\n| Backdrop click | Closes the dialog when `dismissible`. |\n| Reduced motion | Enter/exit animations are suppressed under `prefers-reduced-motion` (`motion-reduce:animate-none`) and the dialog unmounts immediately on close. |\n| Sheet mode | Same guarantees: focus moves in, `Tab` is trapped, focus is restored, `Escape`/backdrop honour `dismissible`. |\n| Labelling | When `title` is set, the panel is `aria-labelledby` the title heading. The heading id is generated per instance (`useId`), so several dialogs can be open at once without id collisions. |\n| Stacking | Scroll lock is ref-counted and shared with `Drawer`, `Sheet`, `Sidebar`, `Tour`, etc.: closing a Dialog opened over a Drawer keeps the page locked until the last overlay closes. Only the top-most overlay reacts to `Escape`. |\n\n## Theming\n\nThe panel uses role tokens (`bg-surface`, `text-foreground`, `border-line`, `text-muted`), so it follows dark mode and any `--dt-*` variable overrides. The backdrop is `bg-black/50`.\n\n[← Component overview](README.md)\n",
|
|
5360
6485
|
"DonutChart": "# DonutChart\n\nThemed donut (or pie) chart for part-to-whole data, with a DOM legend, an optional centre label, an accessible data-table fallback and reduced-motion support. Wraps `recharts` (optional peer, `/charts` entry point).\n\n## Install\n\nCharts live on a separate entry point so apps that don't use them never install or bundle `recharts`:\n\n```bash\nnpm install recharts\n# React 18 apps: also pin react-is to your React major (recharts resolves it as a peer)\nnpm install react-is@18\n```\n\n## Import\n\n```jsx\nimport { DonutChart } from '@dreamtree-org/twreact-ui/charts';\n```\n\nNever import charts from the root `@dreamtree-org/twreact-ui` — they are not exported there.\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `data` | `object[]` | — | **Required.** Rows to plot. An empty array renders `emptyState`. |\n| `index` | `string` | `\"name\"` | Key of the slice name in each row. |\n| `category` | `string` | `\"value\"` | Key of the slice value in each row. |\n| `colors` | `ChartColor[]` | `primary, success, warning, error, info, secondary, gray` | Slice colours by row position (cycled); tokens follow the theme and dark mode. |\n| `variant` | `\"donut\"` or `\"pie\"` | `\"donut\"` | `\"pie\"` fills the centre. |\n| `innerRadius` | `number` or `string` | `\"60%\"` | Inner radius of the ring (px or %). Ignored for `variant=\"pie\"`. |\n| `outerRadius` | `number` or `string` | `\"90%\"` | Outer radius (px or %). |\n| `centerLabel` | `ReactNode` | — | Content drawn in the middle of the donut (e.g. the total). Decorative (`aria-hidden`): repeat the figure in `description`. |\n| `showLegend` | `boolean` | `true` | Legend below the chart (real DOM text). |\n| `valueFormatter` | `(value: number) => string` | `toLocaleString` | Formats values in the tooltip, value axis and hidden data table. |\n| `height` | `number` or `string` | `240` | Chart height (px number or any CSS length). The width is always 100% of the parent (responsive). |\n| `title` | `string` | — | Accessible name: `aria-label` of the `role=\"img\"` region and caption of the hidden table. Falls back to `aria-label`, then `\"Donut chart\"`. |\n| `description` | `string` | — | Longer text alternative (the takeaway), linked with `aria-describedby`. |\n| `emptyState` | `ReactNode` | `\"No data to display\"` | Shown (as `role=\"status\"`) instead of the chart when `data` is empty. |\n| `accessibleTable` | `boolean` | `true` | Repeat the numbers in a visually-hidden `<table>` for screen-reader users. |\n| `animate` | `boolean` | `true` | Animate on mount / data change. Always off when the user prefers reduced motion. |\n| `showTooltip` | `boolean` | `true` | Hover tooltip (themed, dark-mode aware). |\n| `className` | `string` | — | Extra classes for the root `<div>` (merged with `cn`). |\n| `ref` | `Ref<HTMLDivElement>` | — | Forwarded to the root `<div>`. |\n| ...rest | `HTMLAttributes<HTMLDivElement>` | — | Forwarded to the root `<div>` (`id`, `data-*`, `aria-*`, handlers). |\n\n## Examples\n\n### Tasks by status\n\n```jsx\nconst status = [\n { status: 'Open', tasks: 18 },\n { status: 'Blocked', tasks: 3 },\n { status: 'Done', tasks: 42 },\n];\n\n<DonutChart\n data={status}\n index=\"status\"\n category=\"tasks\"\n colors={['info', 'error', 'success']}\n centerLabel=\"63 tasks\"\n title=\"Tasks by status\"\n description=\"63 tasks: 42 done, 18 open, 3 blocked.\"\n/>\n```\n\n### Pie\n\n```jsx\n<DonutChart data={status} index=\"status\" category=\"tasks\" variant=\"pie\" />\n```\n\n## Accessibility\n\n| Behavior | Detail |\n|----------|--------|\n| Name | The SVG sits in a `role=\"img\"` region named by `title` (falls back to `aria-label`, then a generic name such as \"Donut chart\") |\n| Description | `description` is rendered visually hidden and linked with `aria-describedby` |\n| Numbers | `accessibleTable` (default on) repeats the data in a visually-hidden `<table>` with a caption, column headers and row headers, formatted with `valueFormatter`; recharts' SVG itself has no accessible content |\n| Legend | Real DOM text, not SVG |\n| Motion | Animation is disabled when `prefers-reduced-motion: reduce` is set (and with `animate={false}`) |\n| Colour | Series colours come from the design tokens; pair the chart with the table / description so colour is never the only signal |\n| Dark mode | Axis text (`--dt-muted`), grid (`--dt-line`) and series tokens are re-read when the `dark` class or `data-theme` on `<html>` changes; tooltip and legend use `dark:` variants |\n| Empty | An empty `data` array renders `emptyState` in a `role=\"status\"` box with the same accessible name |\n\n## Server rendering\n\nSafe to render on the server (a Next.js `\"use client\"` page). The first client render matches the server's HTML: token colours stay `rgb(var(--dt-*))` and reduced motion reads as off until hydration finishes, then the live values apply. SVG ids come from `React.useId()` (`dt-chart-…`), not a module counter.\n\n[← Component overview](README.md)\n",
|
|
5361
|
-
"Drawer": "# Drawer\n\nSide panel (sheet) that slides over the page from the right, left, top or bottom. Same open/close contract as `Dialog` (`isOpen` + `onClose`), so a `Dialog size=\"lg\"` stand-in can be swapped for a `Drawer` without touching state code. Use it for record peeks, editors and filter panels.\n\n## Import\n\n```jsx\nimport { Drawer } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `isOpen` | `boolean` | — | Controls visibility (required) |\n| `onClose` | `function` | — | Called on Escape, backdrop click
|
|
5362
|
-
"EmptyState": "# EmptyState\n\nPage-level block: what a card shows when it has no data — an icon, a title, one line saying what to do, and the action that fixes it. Extracted from procurementpro `components/common/empty-state.tsx`.\n\n## Import\n\n```jsx\nimport { EmptyState } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `title` | `ReactNode` | — | What is empty |\n| `description` | `ReactNode` | — | One line of guidance |\n| `icon` | `ReactNode` | Inbox icon | Muted icon |\n| `action` | `ReactNode` | — | Primary or outlined button |\n| `compact` | `boolean` | `false` | Less vertical padding |\n| `className` | `string` | — | Extra CSS classes |\n\n## Example\n\n```jsx\n<EmptyState\n title=\"No obligations this month\"\n description=\"Add the first obligation for a client, or import your compliance register.\"\n action={<Button>Add obligation</Button>}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
5363
|
-
"ErrorState": "# ErrorState\n\nPage-level block: what a card shows when loading failed — neutral copy and a retry, rendered with `role=\"alert\"`. Never shows a raw `error.message`. Extracted from CampaignPro `components/app/page-error.tsx`.\n\n## Import\n\n```jsx\nimport { ErrorState } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `title` | `ReactNode` | `\"We couldn't load this\"` | What failed |\n| `description` | `ReactNode` | Reassurance line | What the user can do |\n| `onRetry` | `() => void` | — | Shows a Try again button |\n| `retryLabel` | `string` | `\"Try again\"` | Retry button label |\n| `className` | `string` | — | Extra CSS classes |\n\n## Example\n\n```jsx\n<ErrorState title=\"We couldn't load the compliance calendar\" onRetry={refetch} />\n```\n\n[← Component overview](README.md)\n",
|
|
5364
|
-
"FileUpload": "# FileUpload\n\nFile upload zone: drag-and-drop or click to open file dialog. Supports single/multiple, `accept`, `maxSize` validation, and list of selected files with remove. Calls `onFileSelect` with valid files and `onFileRemove` with index.\n\n## Import\n\n```jsx\nimport { FileUpload } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `accept` | `string` | — | Input accept (e.g. `\"image/*\"`, `\".pdf\"`). |\n| `multiple` | `boolean` | `false` | Allow multiple files. |\n| `maxSize` | `number` | — | Max file size in bytes; larger files trigger error and are not added. |\n| `onFileSelect` | `(files: File[]) => void` | — | Called with valid selected/dropped files. |\n| `onFileRemove` | `(fileIndex: number) => void` | — | Called when user removes a file from the list. |\n| `files` | `File[]` | `[]` | List of files to display (e.g. controlled from parent). |\n| `disabled` | `boolean` | `false` | Disable the zone and remove buttons. |\n| `className` | `string` | — | Wrapper CSS classes. |\n| ...rest | — | — | Passed to wrapper div. |\n\n## Behavior\n\n- Validates each file: if `maxSize` is set and file.size > maxSize, shows error and does not add that file.\n- File type icons: image, text/document, or generic file.\n- Displays file size in human-readable form (Bytes, KB, MB, GB).\n\n## Example\n\n```jsx\nconst [files, setFiles] = useState([]);\n<FileUpload\n accept=\"image/*\"\n multiple\n maxSize={5 * 1024 * 1024}\n files={files}\n onFileSelect={(newFiles) => setFiles(prev => [...prev, ...newFiles])}\n onFileRemove={(index) => setFiles(prev => prev.filter((_, i) => i !== index))}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
5365
|
-
"FootNav": "# FootNav\n\nFixed bottom navigation for mobile (hidden on `md` and up via `md:hidden`). Renders a row of items with icon and label; optional badge per item. Items
|
|
5366
|
-
"Form": "# Form\n\nForm built on **react-hook-form** with a `fields` config array. Renders Input, Select, DatePicker, Checkbox, Switch, textarea, etc., and handles submit/reset. Use `validationSchema` (e.g. from `@hookform/resolvers` with yup) for validation.\n\n## Import\n\n```jsx\nimport { Form } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `fields` | `Array` | `[]` | Field configs (see Field shape below). |\n| `onSubmit` | `(data: object) => void \\| Promise<void>` | — | Submit handler; receives form data. If it returns a promise, the form awaits it: the submit button shows its loading state and stays disabled until the promise settles. |\n| `defaultValues` | `object` | `{}` | Default values for each field name. |\n| `validationSchema` | `object` | — | Resolver from e.g. `yupResolver(schema)`. |\n| `className` | `string` | — | Form wrapper CSS classes. |\n| `submitText` | `string` | `\"Submit\"` | Submit button text. |\n| `resetText` | `string` | `\"Reset\"` | Reset button text. |\n| `submitButtonClass` | `string` | — | Submit button CSS classes. |\n| `resetButtonClass` | `string` | — | Reset button CSS classes. |\n| `showReset` | `boolean` | `true` | Show reset button. |\n| `submitDisabled` | `boolean` | `false` | Disable submit button. |\n| ...rest | — | — | Passed to react-hook-form `useForm` (e.g. `mode`). |\n\n## Field shape\n\nEach item in `fields` can have:\n\n- `type`: `\"text\"` \\| `\"email\"` \\| `\"password\"` \\| `\"number\"` \\| `\"tel\"` \\| `\"url\"` \\| `\"select\"` \\| `\"date\"` \\| `\"textarea\"` \\| `\"checkbox\"` \\| `\"radio\"` \\| `\"file\"` \\| `\"switch\"` \\| `\"color\"` \\| `\"custom\"`.\n- `name`: string (required).\n- `label`: string.\n- `required`: boolean.\n- `placeholder`: string.\n- `helperText`: ReactNode — hint shown under the field and linked via `aria-describedby`, for **every** built-in type. The validation error replaces it while shown.\n- `swatches`, `showAlpha`: for `type: \"color\"` — forwarded to `ColorPicker`.\n- `options`: for `select` and `radio`, an array of `{ value, label }` choices (used only by those types and never spread as props). A plain **object** `options` on any type is spread onto the rendered component as extra props, e.g. `options: { maxLength: 4 }`.\n- `rows`: for textarea.\n- Radio options share the field `name` as a native group, and each option's label is linked to its input (`htmlFor`/`id`), so clicking the label selects it. The field `label` names the `radiogroup`.\n- Any other prop passed through to the rendered component (e.g. `disabled`).\n\n## Field groups\n\nAn item with `type: \"group\"` renders a titled section whose `fields` are laid\nout in a grid:\n\n- `title`, `description`: optional heading and intro text.\n- `gridCols`: `1`–`4` (default `1`). Layouts are responsive and always one\n column below the `sm` breakpoint: `2` → 2 columns from `sm`; `3` / `4` → 2\n columns from `sm`, 3 / 4 columns from `lg`. Values outside 1–4 are clamped.\n\n```jsx\n<Form\n fields={[\n {\n type: 'group',\n name: 'contact',\n title: 'Contact',\n gridCols: 2,\n fields: [\n { type: 'text', name: 'first', label: 'First name' },\n { type: 'text', name: 'last', label: 'Last name' },\n ],\n },\n ]}\n/>\n```\n\n## Color fields\n\n`type: \"color\"` mounts the library's [`ColorPicker`](ColorPicker.md) through a\nreact-hook-form `Controller`. The stored value is the string ColorPicker emits:\n`rgb(r, g, b)` (or `rgba(r, g, b, a)` when alpha < 1). Any hex / `rgb()` /\n`rgba()` string works as a default value and is submitted unchanged until the\nuser picks a colour.\n\n```jsx\n<Form\n defaultValues={{ brand: '#2563eb' }}\n fields={[\n {\n type: 'color',\n name: 'brand',\n label: 'Brand colour',\n swatches: ['#2563eb', '#16a34a', '#dc2626'],\n helperText: 'Used for buttons and links.',\n },\n ]}\n onSubmit={(data) => save(data)} // { brand: 'rgb(37, 99, 235)' } after a pick\n/>\n```\n\n## Helper text\n\n```jsx\n<Form\n fields={[\n { type: 'email', name: 'email', label: 'Email', helperText: 'Receipts only.' },\n { type: 'textarea', name: 'bio', label: 'Bio', helperText: 'Max 200 characters.' },\n ]}\n/>\n```\n\n`Input` / `Select` / `DatePicker` fields forward `helperText` to the component;\n`textarea`, `checkbox`, `radio`, `file`, `switch` and `color` fields render it\nin Form's own markup with the same rule: the validation error replaces the\nhelper text while present.\n\n## Example\n\n```jsx\nimport { Form } from '@dreamtree-org/twreact-ui';\nimport { yupResolver } from '@hookform/resolvers/yup';\nimport * as yup from 'yup';\n\nconst schema = yup.object({\n email: yup.string().email().required(),\n password: yup.string().min(6).required(),\n});\n\n<Form\n fields={[\n { type: 'email', name: 'email', label: 'Email', required: true, placeholder: 'you@example.com' },\n { type: 'password', name: 'password', label: 'Password', required: true },\n { type: 'select', name: 'role', label: 'Role', options: [{ value: 'user', label: 'User' }, { value: 'admin', label: 'Admin' }] },\n ]}\n defaultValues={{ email: '', password: '', role: 'user' }}\n validationSchema={yupResolver(schema)}\n onSubmit={(data) => console.log(data)}\n submitText=\"Sign in\"\n showReset\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
5367
|
-
"Input": "# Input\n\nText input with optional label, error/success states, clear button, character count, and password visibility toggle.\n\n## Import\n\n```jsx\nimport { Input } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `id` | `string` | — | Input id (auto-generated if not set) |\n| `className` | `string` | — | Extra CSS classes |\n| `type` | `string` | `\"text\"` | `text`, `search`, `password`, etc. |\n| `label` | `string` | — | Label text |\n| `placeholder` | `string` | — | Placeholder |\n| `error` | `string` | — | Error message (shows error state). Replaces `helperText` while shown. |\n| `helperText` | `ReactNode` | — | Hint below the input, linked via `aria-describedby`. Hidden while `error` or `success` is shown. |\n| `success` | `boolean` | — | Success state |\n| `disabled` | `boolean` | `false` | Disable input |\n| `required` | `boolean` | `false` | Required field |\n| `clearable` | `boolean` | `false` | Show clear button when has value |\n| `
|
|
5368
|
-
"LineChart": "# LineChart\n\nThemed, responsive line chart with a data-first API (`data` + `index` + `categories`), token colours, an accessible data-table fallback and reduced-motion support. Wraps `recharts` (optional peer, `/charts` entry point).\n\n## Install\n\nCharts live on a separate entry point so apps that don't use them never install or bundle `recharts`:\n\n```bash\nnpm install recharts\n# React 18 apps: also pin react-is to your React major (recharts resolves it as a peer)\nnpm install react-is@18\n```\n\n## Import\n\n```jsx\nimport { LineChart } from '@dreamtree-org/twreact-ui/charts';\n```\n\nNever import charts from the root `@dreamtree-org/twreact-ui` — they are not exported there.\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `data` | `object[]` | — | **Required.** Rows to plot. An empty array renders `emptyState`. |\n| `index` | `string` | — | **Required.** Key of the x-axis (category) value in each row, e.g. `\"month\"`. |\n| `categories` | `string[]` | — | **Required.** Keys of the numeric series, one line per key. |\n| `colors` | `ChartColor[]` | `primary, success, warning, error, info, secondary, gray` | Series colours by position (cycled). A token (`\"primary\"`, `\"success-600\"`) resolves to the `--dt-*` CSS variable and follows the theme and dark mode; any other string is used as a CSS colour. |\n| `xAxisFormatter` | `(value) => string` | — | Formats category (x-axis) tick labels and the hidden table's row headers. |\n| `showLegend` | `boolean` | `true` | Legend below the chart (real DOM text). |\n| `showXAxis` | `boolean` | `true` | Show the x-axis. |\n| `showYAxis` | `boolean` | `true` | Show the y-axis. |\n| `showGrid` | `boolean` | `true` | Dashed grid lines (`--dt-line`). |\n| `yAxisWidth` | `number` | `48` | Width reserved for value-axis labels, px. |\n| `curveType` | `\"linear\"`, `\"monotone\"`, `\"step\"` or `\"natural\"` | `\"monotone\"` | Line interpolation. |\n| `showDots` | `boolean` | `false` | Draw a dot on every data point. |\n| `connectNulls` | `boolean` | `false` | Draw across `null` gaps instead of breaking the line. |\n| `valueFormatter` | `(value: number) => string` | `toLocaleString` | Formats values in the tooltip, value axis and hidden data table. |\n| `height` | `number` or `string` | `300` | Chart height (px number or any CSS length). The width is always 100% of the parent (responsive). |\n| `title` | `string` | — | Accessible name: `aria-label` of the `role=\"img\"` region and caption of the hidden table. Falls back to `aria-label`, then `\"Line chart\"`. |\n| `description` | `string` | — | Longer text alternative (the takeaway), linked with `aria-describedby`. |\n| `emptyState` | `ReactNode` | `\"No data to display\"` | Shown (as `role=\"status\"`) instead of the chart when `data` is empty. |\n| `accessibleTable` | `boolean` | `true` | Repeat the numbers in a visually-hidden `<table>` for screen-reader users. |\n| `animate` | `boolean` | `true` | Animate on mount / data change. Always off when the user prefers reduced motion. |\n| `showTooltip` | `boolean` | `true` |
|
|
5369
|
-
"Loader": "# Loader\n\nLoading indicator in three variants: spinner (default), dots, or bar. Configurable size, speed, colors, and optional label text.\n\n## Import\n\n```jsx\nimport { Loader } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` \\| `number` | `\"md\"` | Size: sm=14px, md=20px, lg=36px, or numeric px. |\n| `speed` | `number` | `0.9` | Animation duration in seconds. |\n| `thickness` | `number` | `2.5` | Stroke thickness (spinner). |\n| `color` | `string` | `\"currentColor\"` | Main color (spinner/dots/bar). |\n| `secondaryColor` | `string` |
|
|
5370
|
-
"LoadingState": "# LoadingState\n\nPage-level block: a skeleton shaped like the content that will replace it (table rows, cards, KPI tiles or a key–value panel). One `role=\"status\"` region with screen-reader text; no page-level spinners.\n\n## Import\n\n```jsx\nimport { LoadingState } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `variant` | `table` \\| `cards` \\| `stats` \\| `detail` | `\"table\"` | Shape of the skeleton |\n| `rows` | `number` | 5 (4 for stats, 3 for cards) | How many rows or tiles |\n| `label` | `string` | `\"Loading…\"` | Screen-reader text |\n| `className` | `string` | — | Extra CSS classes |\n\n## Example\n\n```jsx\n{isLoading ? <LoadingState variant=\"table\" rows={5} label=\"Loading obligations\" /> : <Table … />}\n```\n\n[← Component overview](README.md)\n",
|
|
5371
|
-
"LocationPicker": "# LocationPicker\n\nInput for latitude/longitude. User can type `lat, lng` (e.g. `12.34, 56.78`) or click the icon to use browser geolocation. Value is `{ lat, lng }` or `null`.\n\n## Import\n\n```jsx\nimport { LocationPicker } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `{ lat: number, lng: number }` \\| `null` | `null` | Current coordinates. |\n| `onChange` | `(coords: { lat, lng } \\| null) => void` | — | Called when value changes (typed or from geolocation). |\n| `inputProps` | `object` | `{}` | Props spread to the underlying input (e.g. `placeholder`). A supplied `inputProps.onChange` is forwarded **in addition to** (not instead of) the component's coord-parsing handler — both fire. `inputProps.className` is merged via `cn()`. |\n| `className` | `string` | — | Classes merged (via `cn()`) onto the root wrapper. |\n| `icon` | `ReactNode` | default pin SVG | Icon for \"use my location\" button. |\n| `iconProps` | `object` | `{}` | Props for the icon button (e.g. `className`, `onClick`). |\n| `iconPosition` | `\"left\"` \\| `\"right\"` | `\"right\"` | Position of the icon button. |\n| `placeholder` | `string` | `\"lat, lng\"` | Input placeholder. |\n\n## Behavior\n\n- Display format is `lat.toFixed(6), lng.toFixed(6)`.\n- Typing a valid \"lat, lng\" string calls `onChange` with `{ lat, lng }` (e.g.\n typing `\"1.5, 2.5\"` calls `onChange({ lat: 1.5, lng: 2.5 })`). A\n consumer-supplied `inputProps.onChange` still fires alongside it.\n- Clearing the input calls `onChange(null)`.\n- Clicking the icon calls `navigator.geolocation.getCurrentPosition` and passes the result to `onChange`. Requires HTTPS in production.\n- Forwards `ref` to the underlying `<input>` and spreads unknown props (`...rest`) onto the root wrapper.\n\n## Example\n\n```jsx\nconst [location, setLocation] = useState(null);\n<LocationPicker\n value={location}\n onChange={setLocation}\n placeholder=\"Enter lat, lng or use location\"\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
5372
|
-
"
|
|
5373
|
-
"PageHeader": "# PageHeader\n\nPage-level block: the page title (`h1`), a muted subtitle, the page's actions (one primary action) and an optional back link. Extracted from taskpro `features/shared/PageHeader.jsx`.\n\n## Import\n\n```jsx\nimport { PageHeader } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `title` | `ReactNode` | — | Page title, rendered as the page's only `h1` |\n| `subtitle` | `ReactNode` | — | One line under the title |\n| `actions` | `ReactNode` | — | Buttons on the right; keep one primary action |\n| `back` | `{ href: string; label: string }` | — | Back link for detail pages |\n| `linkAs` | `ElementType` | `\"a\"` | Link element for the back link |\n| `className` | `string` | — | Extra CSS classes |\n\n## Example\n\n```jsx\n<PageHeader\n title=\"Compliance calendar\"\n subtitle=\"Track deadlines, ownership and filing readiness across clients\"\n actions={<Button leftIcon={<Plus className=\"h-4 w-4\" />}>Add obligation</Button>}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
5374
|
-
"
|
|
5375
|
-
"
|
|
6486
|
+
"Drawer": "# Drawer\n\nSide panel (sheet) that slides over the page from the right, left, top or bottom. Same open/close contract as `Dialog` (`isOpen` + `onClose`), so a `Dialog size=\"lg\"` stand-in can be swapped for a `Drawer` without touching state code. Use it for record peeks, editors and filter panels.\n\nDrawer is built on the same internal core as [`Sheet`](Sheet.md): it slides in from its edge, slides back out before it unmounts, and can be swiped toward its edge to close. For snap points or a close reason, use `Sheet` directly.\n\n## Import\n\n```jsx\nimport { Drawer } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `isOpen` | `boolean` | — | Controls visibility (required) |\n| `onClose` | `function` | — | Called with no arguments on Escape, backdrop click, the close button or a swipe (required) |\n| `title` | `ReactNode` | — | Heading; also labels the dialog (`aria-labelledby`) |\n| `description` | `ReactNode` | — | Sub-heading; wired to `aria-describedby` |\n| `children` | `ReactNode` | — | Body content (scrolls when it overflows) |\n| `placement` | `string` | `\"right\"` | `right`, `left`, `top`, `bottom` |\n| `size` | `string` | `\"md\"` | `sm`, `md`, `lg`, `xl`, `full` — max width for left/right (`max-w-xs` … `max-w-full`); for top/bottom a max height in dynamic viewport units: `30dvh`, `50dvh`, `75dvh`, `90dvh`, `full` = `100dvh` |\n| `dismissible` | `boolean` | `true` | Allow close via Escape and backdrop click |\n| `showCloseButton` | `boolean` | `true` | Show the X button (44×44px touch target on coarse pointers) |\n| `footer` | `ReactNode` | — | Footer content (e.g. buttons), right-aligned |\n| `initialFocusRef` | `RefObject` | — | Element to focus on open (default: first focusable element) |\n| `handle` | `boolean` | `true` for `placement=\"bottom\"`, else `false` | Draws a decorative, `aria-hidden` grab handle (at the bottom edge for `placement=\"top\"`, else at the top) |\n| `swipeToClose` | `boolean` | `false` | Any placement: dragging the handle or header toward the drawer's own edge (down for bottom, up for top, right for right, left for left) past 80px (or half the panel, if smaller), or a quick flick, calls `onClose`. A shorter drag springs back. Ignored when `dismissible` is `false`. The scrolling body never starts a swipe |\n| `className` | `string` | — | Extra classes for the panel (merged with `cn`) |\n| `...rest` | `HTMLAttributes` | — | Forwarded to the panel (`aria-label`, `data-*`, …); `ref` points at the panel |\n\n## Composition\n\n`Drawer.Header`, `Drawer.Body` and `Drawer.Footer` accept `className` and `...rest`. With `swipeToClose`, `Drawer.Header` is a grab zone like the built-in header (your own pointer handlers still run). When `title`, `description` and `footer` are all omitted, `children` fill the panel directly so these parts lay out without double padding (the close button floats top-right). Give the panel a name with `aria-label` or `aria-labelledby` in that mode.\n\n## Examples\n\n### Basic\n\n```jsx\nconst [open, setOpen] = useState(false);\n\n<Button onClick={() => setOpen(true)}>Edit task</Button>\n<Drawer\n isOpen={open}\n onClose={() => setOpen(false)}\n title=\"Edit task\"\n description=\"Changes are saved when you press Save.\"\n footer={\n <>\n <Button variant=\"outline\" onClick={() => setOpen(false)}>Cancel</Button>\n <Button onClick={save}>Save</Button>\n </>\n }\n>\n <Input label=\"Title\" value={title} onChange={(e) => setTitle(e.target.value)} />\n</Drawer>\n```\n\n### Bottom sheet (phone)\n\n`placement=\"bottom\"` renders a phone-style sheet: rounded top corners, a grab handle, and bottom padding for the safe area (`env(safe-area-inset-bottom)`, so add `viewport-fit=cover` to your viewport meta). Add `swipeToClose` to let users drag it down to dismiss. Escape, backdrop click and the focus trap work as usual.\n\n```jsx\n<Drawer isOpen={open} onClose={close} title=\"Collect payment\" placement=\"bottom\" size=\"lg\" swipeToClose>\n …\n</Drawer>\n```\n\n### Composed (task peek)\n\n```jsx\n<Drawer isOpen={open} onClose={close} size=\"lg\" aria-label=\"Task peek\">\n <Drawer.Header><h2 className=\"text-lg font-semibold\">{task.title}</h2></Drawer.Header>\n <Drawer.Body>{task.description}</Drawer.Body>\n <Drawer.Footer><Button onClick={close}>Done</Button></Drawer.Footer>\n</Drawer>\n```\n\n## Accessibility & keyboard\n\n| Behavior | Detail |\n|----------|--------|\n| Role | Panel is `role=\"dialog\"` + `aria-modal=\"true\"`, rendered in a portal on `document.body` |\n| Focus on open | `initialFocusRef`, else the first focusable element, else the panel |\n| Focus trap | `Tab` / `Shift`+`Tab` cycle inside the panel |\n| Focus restore | On close, focus returns to the element that opened it |\n| `Escape` | Closes when `dismissible`; only the top-most overlay reacts (a Popover inside a Drawer closes first) |\n| Scroll lock | Body scroll is locked while open (reference-counted, safe with stacked overlays) |\n| Close button | 28px on desktop; grows to a 44×44px touch target on coarse pointers (`@media (pointer: coarse)`) |\n| Swipe to close | Pointer drag on the handle/header only, toward the drawer's edge; Escape and the close button remain the keyboard path |\n| Motion | Slides in from its edge and back out (about 200ms, `animate-sheet-in/out` for bottom, a transform transition for the other edges); the backdrop fades in and out. The panel stays mounted (with `data-state=\"exiting\"`, no pointer events) until the exit ends |\n| Reduced motion | Under `prefers-reduced-motion` there is no slide or fade, the swipe snap-back is instant and the drawer unmounts as soon as `isOpen` turns false |\n| Keyboard on phones | A bottom drawer moves up above the on-screen keyboard (`visualViewport`) |\n| Dark mode | Panel, borders and text use `dark:` variants |\n\n[← Component overview](README.md)\n",
|
|
6487
|
+
"EmptyState": "# EmptyState\n\nPage-level block: what a card shows when it has no data — an icon, a title, one line saying what to do, and the action that fixes it. Extracted from procurementpro `components/common/empty-state.tsx`.\n\n## Import\n\n```jsx\nimport { EmptyState } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `title` | `ReactNode` | — | What is empty |\n| `description` | `ReactNode` | — | One line of guidance |\n| `icon` | `ReactNode` | Inbox icon | Muted icon, in a `bg-secondary-100` circle (`dark:bg-gray-800` in dark mode) |\n| `action` | `ReactNode` | — | Primary or outlined button |\n| `compact` | `boolean` | `false` | Less vertical padding |\n| `className` | `string` | — | Extra CSS classes |\n\n## Example\n\n```jsx\n<EmptyState\n title=\"No obligations this month\"\n description=\"Add the first obligation for a client, or import your compliance register.\"\n action={<Button>Add obligation</Button>}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
6488
|
+
"ErrorState": "# ErrorState\n\nPage-level block: what a card shows when loading failed — neutral copy and a retry, rendered with `role=\"alert\"`. Never shows a raw `error.message`. Extracted from CampaignPro `components/app/page-error.tsx`.\n\n## Import\n\n```jsx\nimport { ErrorState } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `title` | `ReactNode` | `\"We couldn't load this\"` | What failed |\n| `description` | `ReactNode` | Reassurance line | What the user can do |\n| `onRetry` | `() => void` | — | Shows a Try again button |\n| `retryLabel` | `string` | `\"Try again\"` | Retry button label |\n| `className` | `string` | — | Extra CSS classes |\n\nThe alert icon sits in a `bg-error-50` / `text-error-600` circle, with `dark:bg-error-900/30` / `dark:text-error-400` in dark mode.\n\n## Example\n\n```jsx\n<ErrorState title=\"We couldn't load the compliance calendar\" onRetry={refetch} />\n```\n\n[← Component overview](README.md)\n",
|
|
6489
|
+
"FileUpload": "# FileUpload\n\nFile upload zone: drag-and-drop or click to open file dialog. Supports single/multiple, `accept`, `maxSize` validation, and list of selected files with remove. Calls `onFileSelect` with valid files and `onFileRemove` with index.\n\n## Import\n\n```jsx\nimport { FileUpload } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `accept` | `string` | — | Input accept (e.g. `\"image/*\"`, `\".pdf\"`). |\n| `multiple` | `boolean` | `false` | Allow multiple files. |\n| `maxSize` | `number` | — | Max file size in bytes; larger files trigger error and are not added. |\n| `onFileSelect` | `(files: File[]) => void` | — | Called with valid selected/dropped files. |\n| `onFileRemove` | `(fileIndex: number) => void` | — | Called when user removes a file from the list. |\n| `files` | `File[]` | `[]` | List of files to display (e.g. controlled from parent). |\n| `disabled` | `boolean` | `false` | Disable the zone, pick buttons and remove buttons. |\n| `capture` | `boolean` \\| `\"user\"` \\| `\"environment\"` | — | When set, touch screens also get a **Take photo** button, wired to a second hidden input that carries `capture` (and `accept`, defaulting to `\"image/*\"`). The **Choose file** input never has `capture`, so picking from the library still works. |\n| `className` | `string` | — | Wrapper CSS classes. |\n| ...rest | — | — | Passed to wrapper div. |\n\n## Behavior\n\n- Validates each file: if `maxSize` is set and file.size > maxSize, shows error and does not add that file.\n- File type icons: image, text/document, or generic file.\n- **Mouse / trackpad** (`pointer: fine`): the dashed zone is a `role=\"button\"` (Tab to it, Enter / Space opens the file dialog) and accepts drag and drop.\n- **Touch screens** (`pointer: coarse`): drag and drop isn't available, so the zone shows a 44 px **Choose file** (or **Choose files** with `multiple`) button, plus **Take photo** when `capture` is set. Dropping still works if the device supports it.\n- The selected-file list is a `<ul aria-label=\"Selected files\">` using the role tokens (`bg-surface`, `border-line`, `text-foreground`, `text-muted`), so it follows dark mode. Each remove button is labelled `Remove <filename>` and grows to a 44×44 px touch target on coarse pointers.\n- Validation errors render with `role=\"alert\"`.\n- Displays file size in human-readable form (Bytes, KB, MB, GB).\n\n## Example\n\n```jsx\nconst [files, setFiles] = useState([]);\n<FileUpload\n accept=\"image/*\"\n multiple\n maxSize={5 * 1024 * 1024}\n files={files}\n onFileSelect={(newFiles) => setFiles(prev => [...prev, ...newFiles])}\n onFileRemove={(index) => setFiles(prev => prev.filter((_, i) => i !== index))}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
6490
|
+
"FootNav": "# FootNav\n\nFixed bottom navigation for mobile (hidden on `md` and up via `md:hidden`): a native-feeling tab bar. Renders a row of items with icon and label; optional badge per item. Items are buttons (`onClick`) or links (`href` / `to`, rendered as `<a>` or your router `Link` via `linkAs`). The bar sits on the `z-sticky` layer, pads the bottom safe area (`pb-safe`, for the home indicator), and is a translucent `bg-surface/80` with backdrop blur where supported (solid `bg-surface` otherwise); borders use `border-line`, so it is dark-mode aware. Items share the width equally, long labels truncate (five tabs fit a 375px phone), and shrink slightly while pressed (off under reduced motion). Put a `<FootNav.Spacer />` at the end of the page content so nothing scrolls under the bar.\n\n## Import\n\n```jsx\nimport { FootNav } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `items` | `Array` | `[]` | Each item: `{ id, label, icon, onClick?, active?, badge?, href?, to?, variant?, 'aria-*'?, 'data-*'? }`. `variant: 'action'` renders a raised circular primary button (FAB); `href` / `to` render the item as a link; `aria-*` / `data-*` keys are passed to the item's button or link. |\n| `linkAs` | `ElementType` | `\"a\"` | Component for items with `href` / `to`, e.g. a router `Link`. It receives `href` and `to` (`item.to ?? item.href`). Items without `href`/`to` stay `<button type=\"button\">`. |\n| `hideOnScroll` | `boolean` | `false` | Slide the bar away while scrolling down and back on scroll up (passive listener, SSR-safe; instant under `prefers-reduced-motion`). Never hides within 64px of the top; keyboard focus entering the bar shows it. Sets `data-hidden` on the `<nav>`. |\n| `badgeMax` | `number` | `99` | Numeric badges above this read `${badgeMax}+` (e.g. `99+`). |\n| `aria-label` | `string` | `\"Primary\"` | Accessible name of the `<nav>` landmark. |\n| `className` | `string` | — | Nav wrapper CSS classes. |\n| ...rest | — | — | Passed to `<nav>`. The ref is forwarded to it. |\n\n## FootNav.Spacer\n\n`<FootNav.Spacer />` renders an `aria-hidden` block the height of the bar plus the bottom safe area (hidden from `md` up, like the bar). Place it after the page content so the last rows aren't covered. Accepts `className`, `...rest` and a ref.\n\n## Item shape\n\n- `id`: string or number (key).\n- `label`: string.\n- `icon`: ReactNode (e.g. Lucide icon).\n- `onClick(item)`: optional; called when item is clicked.\n- `active`: boolean; the current tab. Rendered with a tinted primary background, `text-primary-700` and semibold weight (inactive tabs use the neutral `text-muted` token), and gets `aria-current=\"page\"`.\n- `badge`: optional number or node; shown as a small badge anchored to that item's own icon. `0` renders as a badge; `null`/`undefined`/`false`/`''` render nothing. Numbers above `badgeMax` (default 99) read `99+`; the badge keeps a minimum width and tabular digits.\n- `href` / `to`: render the item as a link (`<a>`, or `linkAs`). The `active` item gets `aria-current=\"page\"`, same as a button item. `onClick(item)` still fires.\n- `variant`: `'action'` renders a raised, circular primary button (FAB): a 56px `bg-primary-600` circle lifted above the bar and ringed in the surface colour, with the label below. Put it in the middle of `items` to centre it, e.g. a \"+\" that opens a quick-actions sheet. Omit for a normal tab.\n- `aria-*` / `data-*`: passed through to the item's button or link, e.g. `'aria-haspopup': 'dialog'` and `'aria-expanded': open` on an item that opens a sheet, or `'aria-label'` to override the visible label as the accessible name. Other keys stay data and never reach the DOM.\n- Each item renders as a `<button type=\"button\">`, or a link when it has `href` / `to`.\n\n### Primary action (FAB)\n\n```jsx\n<FootNav\n items={[\n { id: 'today', label: 'Today', icon: <Home />, active: true, onClick: go },\n { id: 'beds', label: 'Beds', icon: <Bed />, onClick: go },\n {\n id: 'add',\n label: 'Add',\n icon: <Plus />,\n variant: 'action',\n 'aria-haspopup': 'dialog',\n 'aria-expanded': sheetOpen,\n onClick: () => setSheetOpen(true),\n },\n { id: 'payments', label: 'Payments', icon: <Wallet />, onClick: go },\n { id: 'more', label: 'More', icon: <Menu />, onClick: go },\n ]}\n/>\n```\n\n### Links, hide on scroll and the spacer\n\n```jsx\nimport { Link } from 'react-router-dom';\n\n<main>\n {content}\n <FootNav.Spacer />\n</main>\n<FootNav\n linkAs={Link}\n hideOnScroll\n items={[\n { id: 'home', label: 'Home', icon: <Home />, to: '/', active: path === '/' },\n { id: 'inbox', label: 'Inbox', icon: <Inbox />, to: '/inbox', badge: unread }, // 120 → 99+\n { id: 'me', label: 'Me', icon: <User />, to: '/me', active: path === '/me' },\n ]}\n/>\n```\n\n## Example\n\n```jsx\n<FootNav\n items={[\n { id: 'home', label: 'Home', icon: <Home />, onClick: () => navigate('/'), active: path === '/' },\n { id: 'search', label: 'Search', icon: <Search />, onClick: () => openSearch() },\n { id: 'notifications', label: 'Alerts', icon: <Bell />, badge: 3, onClick: () => openNotifications() },\n { id: 'profile', label: 'Profile', icon: <User />, onClick: () => navigate('/profile') },\n ]}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
6491
|
+
"Form": "# Form\n\nForm built on **react-hook-form** with a `fields` config array. Renders Input, Select, DatePicker, Checkbox, Switch, textarea, etc., and handles submit/reset. Use `validationSchema` (e.g. from `@hookform/resolvers` with yup) for validation.\n\n## Import\n\n```jsx\nimport { Form } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `fields` | `Array` | `[]` | Field configs (see Field shape below). |\n| `onSubmit` | `(data: object) => void \\| Promise<void>` | — | Submit handler; receives form data. If it returns a promise, the form awaits it: the submit button shows its loading state and stays disabled until the promise settles. |\n| `defaultValues` | `object` | `{}` | Default values for each field name. |\n| `validationSchema` | `object` | — | Resolver from e.g. `yupResolver(schema)`. |\n| `className` | `string` | — | Form wrapper CSS classes. |\n| `submitText` | `string` | `\"Submit\"` | Submit button text. |\n| `resetText` | `string` | `\"Reset\"` | Reset button text. |\n| `submitButtonClass` | `string` | — | Submit button CSS classes. |\n| `resetButtonClass` | `string` | — | Reset button CSS classes. |\n| `showReset` | `boolean` | `true` | Show reset button. |\n| `submitDisabled` | `boolean` | `false` | Disable submit button. |\n| ...rest | — | — | Passed to react-hook-form `useForm` (e.g. `mode`). |\n\n## Field shape\n\nEach item in `fields` can have:\n\n- `type`: `\"text\"` \\| `\"email\"` \\| `\"password\"` \\| `\"number\"` \\| `\"tel\"` \\| `\"url\"` \\| `\"select\"` \\| `\"date\"` \\| `\"textarea\"` \\| `\"checkbox\"` \\| `\"radio\"` \\| `\"file\"` \\| `\"switch\"` \\| `\"color\"` \\| `\"custom\"`.\n- `name`: string (required).\n- `label`: string.\n- `required`: boolean.\n- `placeholder`: string.\n- `helperText`: ReactNode — hint shown under the field and linked via `aria-describedby`, for **every** built-in type. The validation error replaces it while shown.\n- `swatches`, `showAlpha`: for `type: \"color\"` — forwarded to `ColorPicker`.\n- `options`: for `select` and `radio`, an array of `{ value, label }` choices (used only by those types and never spread as props). A plain **object** `options` on any type is spread onto the rendered component as extra props, e.g. `options: { maxLength: 4 }`.\n- `rows`: for textarea.\n- Radio options share the field `name` as a native group, and each option's label is linked to its input (`htmlFor`/`id`), so clicking the label selects it. The field `label` names the `radiogroup`.\n- `clearable`: for the text-like types (`text`, `email`, `password`, `number`, `tel`, `url`). The clear button resets the form value to `\"\"` and then calls the field's own `onClear`, if set. Since 2.0.0 `Input` no longer fires a synthetic `onChange` on clear, so `Form` wires this for you ([migration](MIGRATION-2.0.md#input-clear-no-longer-fires-onchange)).\n- Any other prop passed through to the rendered component (e.g. `disabled`).\n\n## Field groups\n\nAn item with `type: \"group\"` renders a titled section whose `fields` are laid\nout in a grid:\n\n- `title`, `description`: optional heading and intro text.\n- `gridCols`: `1`–`4` (default `1`). Layouts are responsive and always one\n column below the `sm` breakpoint: `2` → 2 columns from `sm`; `3` / `4` → 2\n columns from `sm`, 3 / 4 columns from `lg`. Values outside 1–4 are clamped.\n\n```jsx\n<Form\n fields={[\n {\n type: 'group',\n name: 'contact',\n title: 'Contact',\n gridCols: 2,\n fields: [\n { type: 'text', name: 'first', label: 'First name' },\n { type: 'text', name: 'last', label: 'Last name' },\n ],\n },\n ]}\n/>\n```\n\n## Color fields\n\n`type: \"color\"` mounts the library's [`ColorPicker`](ColorPicker.md) through a\nreact-hook-form `Controller`. The stored value is the string ColorPicker emits:\n`rgb(r, g, b)` (or `rgba(r, g, b, a)` when alpha < 1). Any hex / `rgb()` /\n`rgba()` string works as a default value and is submitted unchanged until the\nuser picks a colour.\n\n```jsx\n<Form\n defaultValues={{ brand: '#2563eb' }}\n fields={[\n {\n type: 'color',\n name: 'brand',\n label: 'Brand colour',\n swatches: ['#2563eb', '#16a34a', '#dc2626'],\n helperText: 'Used for buttons and links.',\n },\n ]}\n onSubmit={(data) => save(data)} // { brand: 'rgb(37, 99, 235)' } after a pick\n/>\n```\n\n## Helper text\n\n```jsx\n<Form\n fields={[\n { type: 'email', name: 'email', label: 'Email', helperText: 'Receipts only.' },\n { type: 'textarea', name: 'bio', label: 'Bio', helperText: 'Max 200 characters.' },\n ]}\n/>\n```\n\n`Input` / `Select` / `DatePicker` fields forward `helperText` to the component;\n`textarea`, `checkbox`, `radio`, `file`, `switch` and `color` fields render it\nin Form's own markup with the same rule: the validation error replaces the\nhelper text while present.\n\n## Example\n\n```jsx\nimport { Form } from '@dreamtree-org/twreact-ui';\nimport { yupResolver } from '@hookform/resolvers/yup';\nimport * as yup from 'yup';\n\nconst schema = yup.object({\n email: yup.string().email().required(),\n password: yup.string().min(6).required(),\n});\n\n<Form\n fields={[\n { type: 'email', name: 'email', label: 'Email', required: true, placeholder: 'you@example.com' },\n { type: 'password', name: 'password', label: 'Password', required: true },\n { type: 'select', name: 'role', label: 'Role', options: [{ value: 'user', label: 'User' }, { value: 'admin', label: 'Admin' }] },\n ]}\n defaultValues={{ email: '', password: '', role: 'user' }}\n validationSchema={yupResolver(schema)}\n onSubmit={(data) => console.log(data)}\n submitText=\"Sign in\"\n showReset\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
6492
|
+
"Input": "# Input\n\nText input with optional label, error/success states, clear button, character count, and password visibility toggle.\n\n## Import\n\n```jsx\nimport { Input } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `id` | `string` | — | Input id (auto-generated if not set) |\n| `className` | `string` | — | Extra CSS classes |\n| `type` | `string` | `\"text\"` | `text`, `search`, `password`, etc. |\n| `label` | `string` | — | Label text |\n| `placeholder` | `string` | — | Placeholder |\n| `error` | `string` | — | Error message (shows error state). Replaces `helperText` while shown. |\n| `helperText` | `ReactNode` | — | Hint below the input, linked via `aria-describedby`. Hidden while `error` or `success` is shown. |\n| `success` | `boolean` | — | Success state |\n| `disabled` | `boolean` | `false` | Disable input |\n| `required` | `boolean` | `false` | Required field |\n| `clearable` | `boolean` | `false` | Show clear button when has value |\n| `leftIcon` | `ReactNode` | — | Decorative icon before the value (`type=\"search\"` shows its own search icon instead). Replaces `icon` in 2.0.0 ([migration](MIGRATION-2.0.md#input-icon--iconposition--lefticon--righticon)) |\n| `rightIcon` | `ReactNode` | — | Decorative icon after the value, hidden while the password toggle or clear button shows. Replaces `icon` + `iconPosition=\"right\"` in 2.0.0. Both icons can be set together |\n| `onClear` | `function` | — | Called when the clear button is pressed. **Controlled** fields must clear their own `value` here: since 2.0.0 the clear no longer sends a synthetic `onChange({ target: { value: '' } })` ([migration](MIGRATION-2.0.md#input-clear-no-longer-fires-onchange)). Uncontrolled fields clear themselves. Focus returns to the input either way. |\n| `showCount` | `boolean` | `false` | Show character count |\n| `maxLength` | `number` | — | Max length (used with showCount) |\n| `readOnly` | `boolean` | `false` | Read-only |\n| `value` | `string` | — | Controlled value |\n| `defaultValue` | `string` | — | Uncontrolled default |\n| `onChange` | `function` | — | Change handler |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Field size. `sm` is 32px, `md` is 40px and grows to the 44px touch target on coarse pointers, `lg` is 44px everywhere. A number is passed through as the native `<input size>` attribute. |\n| `floatingLabel` | `boolean` | `false` | Render `label` inside the field; it floats above the value once the field is focused or filled. The field is taller (48px at `md`). |\n| `otp` | `boolean` | `false` | One-time code field: sets `autoComplete=\"one-time-code\"` and `inputMode=\"numeric\"` (unless you pass them). |\n| `ref` | `Ref<HTMLInputElement>` | — | Forwarded to the native `<input>`; object refs and callback refs both work. |\n| ...rest | — | — | Passed to native `<input>` |\n\n### Mobile keyboard hints\n\nWhen you don't pass the attribute yourself, Input picks the phone keyboard and\nautofill for native types:\n\n| `type` | Adds |\n|--------|------|\n| `tel` | `inputMode=\"tel\"` `autoComplete=\"tel\"` |\n| `email` | `inputMode=\"email\"` `autoComplete=\"email\"` |\n| `url` | `inputMode=\"url\"` `autoComplete=\"url\"` |\n| `search` | `inputMode=\"search\"` `enterKeyHint=\"search\"` |\n| `otp` prop | `inputMode=\"numeric\"` `autoComplete=\"one-time-code\"` |\n\nAny of these you pass explicitly wins (e.g. `autoComplete=\"off\"`).\n\nThe password toggle and clear buttons are real buttons with `aria-label`s and a\n`focus-visible` ring; they grow to 44×44 on coarse pointers. Colours use role\ntokens (`bg-surface`, `text-foreground`) with `dark:` counterparts, so the field\nfollows `ThemeProvider`.\n\nKeyboard focus shows a `focus-visible` primary ring in the neutral state too (not only for `error` / `success`).\n\n## Examples\n\n### Basic\n\n```jsx\n<Input label=\"Email\" placeholder=\"you@example.com\" />\n```\n\n### With helper text\n\n```jsx\n<Input label=\"Email\" type=\"email\" helperText=\"Use your work email.\" />\n```\n\n### Helper text and errors\n\n`helperText` renders a muted line below the control and is linked to it via\n`aria-describedby`, so screen readers announce it with the field. **While\n`error` is set, the error message replaces the helper text** (only one line is\nshown, and `aria-describedby` points at the error). This rule is the same on\nevery field-like component: `Input`, `Select`, `DatePicker`, `DateRangePicker`\nand `Form` fields.\n\n### With error\n\n```jsx\n<Input label=\"Email\" error=\"Invalid email address\" />\n```\n\n### Password with toggle\n\n```jsx\n<Input type=\"password\" label=\"Password\" placeholder=\"Enter password\" />\n```\n\n### Clearable and character count\n\n```jsx\n<Input\n label=\"Bio\"\n clearable\n showCount\n maxLength={200}\n placeholder=\"Tell us about yourself\"\n/>\n\n// Controlled: clear the value yourself in onClear (2.0)\n<Input\n label=\"Search\"\n clearable\n value={q}\n onChange={(e) => setQ(e.target.value)}\n onClear={() => setQ(\"\")}\n/>\n```\n\n### Icons\n\n```jsx\n<Input label=\"Amount\" leftIcon={<IndianRupee className=\"h-4 w-4\" />} />\n<Input label=\"Email\" type=\"email\" rightIcon={<Mail className=\"h-4 w-4\" />} />\n```\n\n### Sizes and floating label\n\n```jsx\n<Input size=\"lg\" label=\"Phone\" type=\"tel\" />\n<Input floatingLabel label=\"Email\" type=\"email\" />\n```\n\n### One-time code\n\n```jsx\n<Input otp label=\"Verification code\" maxLength={6} />\n```\n\n[← Component overview](README.md)\n",
|
|
6493
|
+
"LineChart": "# LineChart\n\nThemed, responsive line chart with a data-first API (`data` + `index` + `categories`), token colours, an accessible data-table fallback and reduced-motion support. Wraps `recharts` (optional peer, `/charts` entry point).\n\n## Install\n\nCharts live on a separate entry point so apps that don't use them never install or bundle `recharts`:\n\n```bash\nnpm install recharts\n# React 18 apps: also pin react-is to your React major (recharts resolves it as a peer)\nnpm install react-is@18\n```\n\n## Import\n\n```jsx\nimport { LineChart } from '@dreamtree-org/twreact-ui/charts';\n```\n\nNever import charts from the root `@dreamtree-org/twreact-ui` — they are not exported there.\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `data` | `object[]` | — | **Required.** Rows to plot. An empty array renders `emptyState`. |\n| `index` | `string` | — | **Required.** Key of the x-axis (category) value in each row, e.g. `\"month\"`. |\n| `categories` | `string[]` | — | **Required.** Keys of the numeric series, one line per key. |\n| `colors` | `ChartColor[]` | `primary, success, warning, error, info, secondary, gray` | Series colours by position (cycled). A token (`\"primary\"`, `\"success-600\"`) resolves to the `--dt-*` CSS variable and follows the theme and dark mode; any other string is used as a CSS colour. |\n| `xAxisFormatter` | `(value) => string` | — | Formats category (x-axis) tick labels and the hidden table's row headers. |\n| `showLegend` | `boolean` | `true` | Legend below the chart (real DOM text). |\n| `showXAxis` | `boolean` | `true` | Show the x-axis. |\n| `showYAxis` | `boolean` | `true` | Show the y-axis. |\n| `showGrid` | `boolean` | `true` | Dashed grid lines (`--dt-line`). |\n| `yAxisWidth` | `number` | `48` (`36` when the chart is narrower than 400 px) | Width reserved for value-axis labels, px. When set, it is used at every width. |\n| `curveType` | `\"linear\"`, `\"monotone\"`, `\"step\"` or `\"natural\"` | `\"monotone\"` | Line interpolation. |\n| `showDots` | `boolean` | `false` | Draw a dot on every data point. |\n| `connectNulls` | `boolean` | `false` | Draw across `null` gaps instead of breaking the line. |\n| `valueFormatter` | `(value: number) => string` | `toLocaleString` | Formats values in the tooltip, value axis and hidden data table. |\n| `height` | `number` or `string` | `300` | Chart height (px number or any CSS length). The width is always 100% of the parent (responsive). |\n| `aspectRatio` | `number` | — | Width / height ratio, e.g. `16 / 9`. When provided it replaces `height`, so the chart scales with its container (handy on phones); the empty state uses it too. |\n| `title` | `string` | — | Accessible name: `aria-label` of the `role=\"img\"` region and caption of the hidden table. Falls back to `aria-label`, then `\"Line chart\"`. |\n| `description` | `string` | — | Longer text alternative (the takeaway), linked with `aria-describedby`. |\n| `emptyState` | `ReactNode` | `\"No data to display\"` | Shown (as `role=\"status\"`) instead of the chart when `data` is empty. |\n| `accessibleTable` | `boolean` | `true` | Repeat the numbers in a visually-hidden `<table>` for screen-reader users. |\n| `animate` | `boolean` | `true` | Animate on mount / data change. Always off when the user prefers reduced motion. |\n| `showTooltip` | `boolean` | `true` | Tooltip (themed, dark-mode aware): on hover with a mouse; on touch a tap pins it at the tapped point, and a second tap on the chart or a tap outside unpins it. |\n| `className` | `string` | — | Extra classes for the root `<div>` (merged with `cn`). |\n| `ref` | `Ref<HTMLDivElement>` | — | Forwarded to the root `<div>`. |\n| ...rest | `HTMLAttributes<HTMLDivElement>` | — | Forwarded to the root `<div>` (`id`, `data-*`, `aria-*`, handlers). |\n\n## Small screens\n\n- x-axis ticks keep a wider gap on narrow charts (`minTickGap` 32 px below 360 px wide, 16 px below 640 px, otherwise 8 px), so labels don't collide.\n- The value axis narrows to 36 px below 400 px wide unless you pass `yAxisWidth`.\n- Use `aspectRatio` instead of a fixed `height` to keep the chart proportional to its width.\n\n## Examples\n\n### Basic\n\n```jsx\nconst data = [\n { month: 'Jan', revenue: 4200, costs: 2600 },\n { month: 'Feb', revenue: 5100, costs: 2900 },\n { month: 'Mar', revenue: 4800, costs: 3100 },\n];\n\n<LineChart\n data={data}\n index=\"month\"\n categories={['revenue', 'costs']}\n valueFormatter={(v) => `$${v.toLocaleString()}`}\n title=\"Revenue and costs, Q1\"\n description=\"Revenue grew 14% while costs grew 19%.\"\n/>\n```\n\n### Token colours, no grid\n\n```jsx\n<LineChart data={data} index=\"month\" categories={['revenue']} colors={['success']} showGrid={false} curveType=\"linear\" showDots />\n```\n\n## Accessibility\n\n| Behavior | Detail |\n|----------|--------|\n| Name | The SVG sits in a `role=\"img\"` region named by `title` (falls back to `aria-label`, then a generic name such as \"Line chart\") |\n| Description | `description` is rendered visually hidden and linked with `aria-describedby` |\n| Numbers | `accessibleTable` (default on) repeats the data in a visually-hidden `<table>` with a caption, column headers and row headers, formatted with `valueFormatter`; recharts' SVG itself has no accessible content |\n| Legend | Real DOM text, not SVG |\n| Motion | Animation is disabled when `prefers-reduced-motion: reduce` is set (and with `animate={false}`) |\n| Colour | Series colours come from the design tokens; pair the chart with the table / description so colour is never the only signal |\n| Dark mode | Axis text (`--dt-muted`), grid (`--dt-line`) and series tokens are re-read when the `dark` class or `data-theme` on `<html>` changes; tooltip and legend use `dark:` variants |\n| Empty | An empty `data` array renders `emptyState` in a `role=\"status\"` box with the same accessible name |\n\n## Server rendering\n\nSafe to render on the server (a Next.js `\"use client\"` page). The first client render matches the server's HTML: token colours stay `rgb(var(--dt-*))` and reduced motion reads as off until hydration finishes, then the live values apply. SVG ids come from `React.useId()` (`dt-chart-…`), not a module counter.\n\n[← Component overview](README.md)\n",
|
|
6494
|
+
"Loader": "# Loader\n\nLoading indicator in three variants: spinner (default), dots, or bar. Configurable size, speed, colors, and optional label text.\n\n## Import\n\n```jsx\nimport { Loader } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` \\| `number` | `\"md\"` | Size: sm=14px, md=20px, lg=36px, or numeric px. |\n| `speed` | `number` | `0.9` | Animation duration in seconds. Applied through the `--dt-loader-speed` CSS custom property on the wrapper. |\n| `thickness` | `number` | `2.5` | Stroke thickness (spinner). |\n| `color` | `string` | `\"currentColor\"` | Main color (spinner/dots/bar). |\n| `secondaryColor` | `string` | — | Track/background color. When omitted, a dark-safe track is used: 8% black in light mode (as before) and 15% white in dark mode. |\n| `variant` | `\"spinner\"` \\| `\"dots\"` \\| `\"bar\"` | `\"spinner\"` | Visual style. |\n| `className` | `string` | `\"\"` | Wrapper CSS classes. |\n| `inline` | `boolean` | `false` | If true, wrapper is inline-flex. |\n| `ariaLabel` | `string` | `\"Loading\"` | Accessibility label. |\n| `text` | `string` | — | Optional label shown next to loader. |\n| `reverse` | `boolean` | `false` | Reverse spinner rotation direction. |\n| `style` | `object` | — | Inline style for wrapper. |\n\n**Animation:** the keyframes ship in the library's Tailwind build (`animate-loader-spin`, `animate-loader-bounce`, `animate-loader-bar`), so no `<style>` tag is injected per instance. Under `prefers-reduced-motion` the spinner slows to one turn every 3 s (it still shows the app is busy), and the dots and bar stop. The spinner is a CSS rotation of the SVG, not SMIL `<animateTransform>`. The indeterminate bar now travels the full width of its track.\n\n## Examples\n\n```jsx\n<Loader />\n<Loader variant=\"dots\" size=\"sm\" />\n<Loader variant=\"bar\" text=\"Loading...\" />\n<Loader size={32} color=\"#2563eb\" />\n```\n\n[← Component overview](README.md)\n",
|
|
6495
|
+
"LoadingState": "# LoadingState\n\nPage-level block: a skeleton shaped like the content that will replace it (table rows, cards, KPI tiles or a key–value panel). One `role=\"status\"` region with screen-reader text; no page-level spinners.\n\n## Import\n\n```jsx\nimport { LoadingState } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `variant` | `table` \\| `cards` \\| `stats` \\| `detail` | `\"table\"` | Shape of the skeleton |\n| `rows` | `number` | 5 (4 for stats, 3 for cards) | How many rows or tiles |\n| `label` | `string` | `\"Loading…\"` | Screen-reader text |\n| `className` | `string` | — | Extra CSS classes |\n\nBones use the `bg-line` role token, and card and row borders use `border-line` / `divide-line`, so the skeleton is dark-safe. Bones pulse (`animate-pulse`) and stop under `prefers-reduced-motion`.\n\n## Example\n\n```jsx\n{isLoading ? <LoadingState variant=\"table\" rows={5} label=\"Loading obligations\" /> : <Table … />}\n```\n\n[← Component overview](README.md)\n",
|
|
6496
|
+
"LocationPicker": "# LocationPicker\n\nInput for latitude/longitude. User can type `lat, lng` (e.g. `12.34, 56.78`) or click the icon to use browser geolocation. Value is `{ lat, lng }` or `null`.\n\n## Import\n\n```jsx\nimport { LocationPicker } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `{ lat: number, lng: number }` \\| `null` | `null` | Current coordinates. |\n| `onChange` | `(coords: { lat, lng } \\| null) => void` | — | Called when value changes (typed or from geolocation). |\n| `inputProps` | `object` | `{}` | Props spread to the underlying input (e.g. `placeholder`). A supplied `inputProps.onChange` is forwarded **in addition to** (not instead of) the component's coord-parsing handler — both fire. `inputProps.className` is merged via `cn()`. |\n| `className` | `string` | — | Classes merged (via `cn()`) onto the root wrapper. |\n| `icon` | `ReactNode` | default pin SVG | Icon for \"use my location\" button. |\n| `iconProps` | `object` | `{}` | Props for the icon button (e.g. `className`, `onClick`). |\n| `iconPosition` | `\"left\"` \\| `\"right\"` | `\"right\"` | Position of the icon button. |\n| `placeholder` | `string` | `\"lat, lng\"` | Input placeholder. |\n| `fullWidth` | `boolean` | `false` | Stretch the root and input to the container width (drops the input's 200 px minimum). |\n\n## Behavior\n\n- Display format is `lat.toFixed(6), lng.toFixed(6)`.\n- Typing a valid \"lat, lng\" string calls `onChange` with `{ lat, lng }` (e.g.\n typing `\"1.5, 2.5\"` calls `onChange({ lat: 1.5, lng: 2.5 })`). A\n consumer-supplied `inputProps.onChange` still fires alongside it.\n- Clearing the input calls `onChange(null)`.\n- Clicking the icon calls `navigator.geolocation.getCurrentPosition` and passes the result to `onChange`. Requires HTTPS in production.\n- **Loading:** while the request is in flight the button shows a spinner (still\n under `prefers-reduced-motion`), has `aria-busy=\"true\"`, ignores repeat clicks,\n and a \"Finding your location…\" `role=\"status\"` line appears under the input.\n- **Errors** are shown in the UI as a `role=\"alert\"` line under the input (the\n input border turns `error`): permission denied, position unavailable, timeout,\n or geolocation unsupported — each with its own message. A later success clears\n it. The error is still logged with `console.error` as before. The status / error\n line is linked to the input and button with `aria-describedby`.\n- The location button grows to a 44×44 px touch target on coarse pointers, has a\n `focus-visible` ring and press feedback. The input renders at 16 px on touch\n screens (no iOS zoom) and has dark-mode styles.\n- Forwards `ref` to the underlying `<input>` and spreads unknown props (`...rest`) onto the root wrapper.\n\n## Example\n\n```jsx\nconst [location, setLocation] = useState(null);\n<LocationPicker\n value={location}\n onChange={setLocation}\n placeholder=\"Enter lat, lng or use location\"\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
6497
|
+
"MIGRATION-2.0": "# Migrating to 2.0\n\n2.0.0 is the one breaking release planned in the [UI/UX and mobile roadmap](../docs/roadmap/uiux-mobile-2026.md) (epic E6). It removes the APIs that 1.x deprecated and renames the icon props that broke the shared `leftIcon` / `rightIcon` vocabulary. Nothing else changes: there are no new peer dependencies, the Tailwind setup is the same, and no default behaviour was flipped.\n\n## Upgrade in three steps\n\n1. Install the new version:\n\n ```bash\n npm install @dreamtree-org/twreact-ui@^2.0.0\n ```\n\n2. Run the codemod over your source folder. It rewrites JSX on components imported from `@dreamtree-org/twreact-ui` and prints the spots it can't fix by itself:\n\n ```bash\n npx jscodeshift -t node_modules/@dreamtree-org/twreact-ui/codemods/v2.cjs src/\n ```\n\n The codemod parses `.js`, `.jsx`, `.ts` and `.tsx` files (its default parser is `tsx`). Add `--extensions=js,jsx,ts,tsx` if jscodeshift skips your files, `--dry --print` to preview the result, or `--parser=babel` for Flow code.\n\n3. Fix each line the codemod reports (lines that start with `REP`), then type-check. The TypeScript types drop every removed prop, so `tsc` flags any the codemod couldn't see, such as props passed through a spread.\n\n## What the codemod does\n\n| Change | Automated | Manual |\n|---|---|---|\n| [Input `icon` / `iconPosition`](#input-icon--iconposition--lefticon--righticon) | `icon` becomes `leftIcon`, or `rightIcon` with `iconPosition=\"right\"`; a literal `iconPosition` is removed | A non-literal `iconPosition={pos}` is reported |\n| [`Tabs.Tab` `iconLeft` / `iconRight`](#tabstab-iconleft--iconright) | Renamed to `leftIcon` / `rightIcon` | None |\n| [Input clear](#input-clear-no-longer-fires-onchange) | None | A `clearable` Input with `value` but no `onClear` is reported. Add `onClear` |\n| [ProgressBar HEX colours](#progressbar-hex-colour-props) | `color`, `bgColor` and `valueColor` are removed | Reported. Pick a `variant` if the bar wasn't primary |\n| [Checkbox `borderColor`](#checkbox-bordercolor) | Removed | None |\n| [Rate `activeColor` / `inactiveColor`](#rate-activecolor-and-inactivecolor) | Removed | None |\n| [Checkbox / Switch raw presets](#checkbox-and-switch-raw-colour-presets) | None | `color=\"blue\"`, `\"indigo\"` or `\"purple\"` is reported. Pick a token |\n\nThe codemod only sees literal JSX attributes. It doesn't follow props passed through a spread (`<Input {...props} />`), components re-exported from your own wrapper files, or `React.createElement` calls. The TypeScript types and the [removed props table](#removed-props-at-a-glance) cover those cases.\n\n## Renamed props\n\n### Input `icon` / `iconPosition` → `leftIcon` / `rightIcon`\n\nInput used one `icon` slot plus `iconPosition`. It now uses the same two slots as `Button`, so you can also set both at once.\n\n```jsx\n// 1.x\n<Input icon={<Search />} />\n<Input icon={<Mail />} iconPosition=\"right\" />\n\n// 2.0\n<Input leftIcon={<Search />} />\n<Input rightIcon={<Mail />} />\n<Input leftIcon={<IndianRupee />} rightIcon={<Info />} /> // new: both together\n```\n\nThe behaviour is the same as before. `rightIcon` hides while the password toggle or the clear button shows, and `type=\"search\"` still draws its own search icon on the left.\n\n**Codemod:** automated when `iconPosition` is missing, `\"left\"` or `\"right\"`. If `iconPosition` is an expression, the codemod reports it and you choose the slot yourself:\n\n```jsx\n// 1.x\n<Input icon={icon} iconPosition={end ? \"right\" : \"left\"} />\n// 2.0\n<Input leftIcon={end ? undefined : icon} rightIcon={end ? icon : undefined} />\n```\n\n### `Tabs.Tab` `iconLeft` / `iconRight`\n\n```jsx\n// 1.x\n<Tabs.Tab iconLeft={<Home />}>Home</Tabs.Tab>\n// 2.0\n<Tabs.Tab leftIcon={<Home />}>Home</Tabs.Tab>\n```\n\n**Codemod:** fully automated, including `UI.Tabs.Tab` when you use a namespace import.\n\n## Behaviour change\n\n### Input clear no longer fires `onChange`\n\nIn 1.x the clear button of a controlled `Input` called `onChange({ target: { value: \"\" } })`. That fake event had no `type`, no `currentTarget` and no `preventDefault`, so handlers that read anything beyond `target.value` broke. In 2.0 the clear button only calls `onClear`, and a controlled field clears its own `value` there.\n\n```jsx\n// 1.x: worked because clear sent a synthetic onChange\n<Input clearable value={q} onChange={(e) => setQ(e.target.value)} />\n\n// 2.0\n<Input\n clearable\n value={q}\n onChange={(e) => setQ(e.target.value)}\n onClear={() => setQ(\"\")}\n/>\n```\n\nUncontrolled fields (`defaultValue`, or no value) still clear themselves, and focus still returns to the input. `Form` text fields keep working: `Form` now passes `onClear` itself and resets the react-hook-form value, then calls the field's own `onClear` if you set one. `SearchField` has its own clear and isn't affected.\n\n**Codemod:** reports every `clearable` Input that has a `value` but no `onClear`. Add the handler yourself, because only you know which state to reset.\n\n## Removed props\n\n### ProgressBar HEX colour props\n\n`color`, `bgColor` and `valueColor` took HEX strings and painted the bar inline, which ignored the brand tokens and dark mode. They are gone. Use the new `variant` prop, a token tone for both the fill and the track.\n\n```jsx\n// 1.x\n<ProgressBar value={80} color=\"#16a34a\" bgColor=\"#dcfce7\" />\n// 2.0\n<ProgressBar value={80} variant=\"success\" />\n```\n\n| `variant` | Fill | Track |\n|---|---|---|\n| `primary` (default) | `bg-primary-600` | `bg-primary-100` / `dark:bg-primary-900/40` |\n| `secondary` | `bg-secondary-600` | `bg-secondary-100` / `dark:bg-secondary-900/40` |\n| `success` | `bg-success-600` | `bg-success-100` / `dark:bg-success-900/40` |\n| `warning` | `bg-warning-600` | `bg-warning-100` / `dark:bg-warning-900/40` |\n| `destructive` | `bg-error-600` | `bg-error-100` / `dark:bg-error-900/40` |\n\nThe value text inside the fill is always `text-white`. For a colour that isn't one of these tones, set it on the brand (`tokens/brands/<name>.json`) rather than on one bar.\n\n**Codemod:** removes the three props and reports each ProgressBar it changed, so you can add a `variant`.\n\n### Checkbox `borderColor`\n\nThe box border is always the `border-gray-300` / `dark:border-gray-600` token.\n\n```jsx\n// 1.x\n<Checkbox label=\"Accept\" borderColor=\"#9ca3af\" />\n// 2.0\n<Checkbox label=\"Accept\" />\n```\n\n**Codemod:** removes the prop.\n\n### Rate `activeColor` and `inactiveColor`\n\nStars always use `text-warning-400` (active) and `text-gray-200` (inactive), so a brand file recolours them.\n\n```jsx\n// 1.x\n<Rate value={4} activeColor=\"#ef4444\" inactiveColor=\"#fee2e2\" />\n// 2.0\n<Rate value={4} />\n```\n\n**Codemod:** removes both props.\n\n### Checkbox and Switch raw colour presets\n\nThe `blue`, `indigo` and `purple` presets used the raw Tailwind palette (`bg-blue-600`, …) instead of tokens. They are removed. The token presets remain: `primary`, `secondary`, `success`, `error` and `warning`, plus the aliases `green` (success) and `danger` / `red` (error).\n\n```jsx\n// 1.x\n<Checkbox color=\"indigo\" label=\"Remember me\" />\n<Switch color=\"blue\" />\n// 2.0\n<Checkbox color=\"primary\" label=\"Remember me\" />\n<Switch color=\"primary\" />\n```\n\n**Heads-up:** `color` still accepts any CSS colour string as an inline override, and `blue`, `indigo` and `purple` are CSS colour names. So the old values don't throw an error: they now paint the CSS colour inline (pure `#0000ff` and so on) with no dark-mode tone. Replace them.\n\n**Codemod:** reports every literal `color=\"blue\" | \"indigo\" | \"purple\"` on Checkbox and Switch. It doesn't pick a token for you, because the right tone depends on your brand.\n\n### Internal: `computePosition`\n\nThe internal `computePosition` helper in `src/utils/position.js` was never exported from the package. It was unused after every overlay moved to `useFloatingPosition` (floating-ui). It is deleted. Consumers are only affected if they deep-imported library source files, which isn't supported.\n\n## Removed props at a glance\n\n| Component | 1.x prop | 2.0 |\n|---|---|---|\n| `Input` | `icon` | `leftIcon` / `rightIcon` |\n| `Input` | `iconPosition` | Choose `leftIcon` or `rightIcon` |\n| `Input` | clear → synthetic `onChange` | `onClear` (controlled: clear `value` yourself) |\n| `Tabs.Tab` | `iconLeft` / `iconRight` | `leftIcon` / `rightIcon` |\n| `ProgressBar` | `color`, `bgColor`, `valueColor` (HEX) | `variant` |\n| `Checkbox` | `borderColor` | Removed (token border) |\n| `Checkbox` | `color=\"blue\" \\| \"indigo\" \\| \"purple\"` | A token preset |\n| `Switch` | `color=\"blue\" \\| \"indigo\" \\| \"purple\"` | A token preset |\n| `Rate` | `activeColor`, `inactiveColor` | Removed (token colours) |\n\nA removed prop that is still passed falls through `...rest` to the DOM element, where React logs an \"unknown prop\" warning in development. Treat those warnings as a list of the props left to clean up.\n\n## What did not change\n\n- **No default flips.** No 1.x ticket or changelog entry promised a 2.0 default change, so every default stays as it was. Examples are `presentation=\"auto\"`, `Sidebar swipeable`, `Slider showBubble` and the opt-in `haptics`.\n- **Sizes** already used the shared `xs | sm | md | lg | xl` scale. No size was renamed.\n- **Exports:** no component, hook or util was added, renamed or removed.\n\n[← Component overview](README.md)\n",
|
|
6498
|
+
"MobileNativeFeel": "# Mobile native feel and PWA guide\n\nHow to make an app built with `@dreamtree-org/twreact-ui` feel like a native app on phones and when installed as a PWA. The components already handle safe areas, the dynamic viewport, sheets, touch targets and reduced motion. Your app still has to add the page-level parts: the viewport meta, `theme-color`, the web manifest and a few layout rules. This guide covers both, then ends with a checklist for testing on real devices.\n\n## 1. Viewport meta\n\nPut this in your HTML `<head>` (in Next.js, export a `viewport` object from the root layout with the same values):\n\n```html\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n```\n\n- `viewport-fit=cover` lets the page draw under the notch and the home indicator, and makes `env(safe-area-inset-*)` return real values. Without it, `pt-safe`, `pb-safe` and the padding inside `AppShell`, `FootNav`, `Sheet` and `BottomActionBar` all resolve to `0`.\n- Don't add `maximum-scale=1` or `user-scalable=no` to stop iOS zooming into focused inputs. That blocks pinch-zoom, which people with low vision need. The library's stylesheet prevents the focus zoom another way (see [Global mobile baseline](#9-global-mobile-baseline)).\n\n## 2. Theme colour per theme\n\n`theme-color` tints the browser toolbar on Android Chrome and the status bar in an installed PWA. Give each colour scheme its own meta tag:\n\n```html\n<meta name=\"theme-color\" content=\"#ffffff\" media=\"(prefers-color-scheme: light)\">\n<meta name=\"theme-color\" content=\"#111827\" media=\"(prefers-color-scheme: dark)\">\n```\n\n`#ffffff` / `#111827` are the default `surface` token (light / dark), which is the colour of the `AppShell` top bar (`bg-surface`). If your brand file changes `surface`, or your top bar uses a different colour, use that colour here.\n\n### Keep it in sync with `ThemeProvider`\n\n`ThemeProvider` stores an explicit `'light'` or `'dark'` choice and writes `data-theme`, the `.dark` class and `color-scheme` on `<html>`. It does not follow the OS setting. So a user can pick dark mode on a phone set to light, and the media-query tags above would then show the wrong toolbar colour. Rewrite the tags whenever the theme changes. Read the colour from the `--dt-surface` token variable (stored as space-separated RGB channels), so the tag also follows a brand file:\n\n```jsx\nimport { useEffect } from 'react';\nimport { ThemeProvider, useTheme } from '@dreamtree-org/twreact-ui';\n\nfunction ThemeColorSync() {\n const { theme } = useTheme();\n useEffect(() => {\n const rgb = getComputedStyle(document.documentElement).getPropertyValue('--dt-surface').trim();\n if (!rgb) return;\n document\n .querySelectorAll('meta[name=\"theme-color\"]')\n .forEach((meta) => meta.setAttribute('content', `rgb(${rgb})`));\n }, [theme]);\n return null;\n}\n\nexport function Root({ children }) {\n return (\n <ThemeProvider defaultTheme=\"light\">\n <ThemeColorSync />\n {children}\n </ThemeProvider>\n );\n}\n```\n\n`useTheme()` throws outside a `ThemeProvider`, so render `ThemeColorSync` inside it. Keep the static tags in the HTML anyway: they give the right colour before JavaScript runs.\n\n## 3. Web manifest\n\nA manifest makes the app installable (\"Add to Home Screen\" / \"Install app\") and opens it without browser UI:\n\n```json\n{\n \"name\": \"PracticePro\",\n \"short_name\": \"PracticePro\",\n \"start_url\": \"/\",\n \"scope\": \"/\",\n \"display\": \"standalone\",\n \"background_color\": \"#f8fafc\",\n \"theme_color\": \"#ffffff\",\n \"icons\": [\n { \"src\": \"/icons/icon-192.png\", \"sizes\": \"192x192\", \"type\": \"image/png\" },\n { \"src\": \"/icons/icon-512.png\", \"sizes\": \"512x512\", \"type\": \"image/png\" },\n { \"src\": \"/icons/icon-maskable-512.png\", \"sizes\": \"512x512\", \"type\": \"image/png\", \"purpose\": \"maskable\" }\n ]\n}\n```\n\n```html\n<link rel=\"manifest\" href=\"/manifest.webmanifest\">\n<link rel=\"apple-touch-icon\" href=\"/icons/apple-touch-icon.png\">\n```\n\n- `display: \"standalone\"` removes the address bar and browser toolbars.\n- `background_color` is the splash screen colour; `#f8fafc` is the default `background` token (the page background), so the splash matches the first frame.\n- The manifest's `theme_color` has no light/dark variants. The `<meta name=\"theme-color\">` tags win once the page loads.\n- iOS uses `apple-touch-icon` for the home-screen icon. To draw the page under the iOS status bar in standalone mode, add `<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">`. Your top bar then needs `pt-safe`, which `AppShell` already has.\n- Offline support (a service worker) is up to you; the library ships none.\n\n## 4. Styling for standalone mode\n\nInstalled apps have no browser back button or address bar. Use `@media (display-mode: standalone)` for anything that should change:\n\n```css\n/* Show an in-app back button only when there is no browser UI. */\n.app-back-button { display: none; }\n\n@media (display-mode: standalone) {\n .app-back-button { display: inline-flex; }\n /* Native apps don't let you select UI chrome by accident. */\n .app-chrome { user-select: none; -webkit-user-select: none; }\n}\n```\n\nIn Tailwind 3.4 you can write it inline as an arbitrary variant: `hidden [@media(display-mode:standalone)]:inline-flex`. The library has no `standalone:` variant of its own; add one to your config if you use it a lot. In JavaScript, check `window.matchMedia('(display-mode: standalone)').matches` (and `navigator.standalone` on older iOS).\n\n## 5. Use `dvh`, not `100vh`\n\nOn phones, `100vh` is the height with the browser toolbars hidden, so a `h-screen` layout is taller than the visible area and its bottom (often the main action) ends up under the toolbar. Use the dynamic viewport units, which Tailwind 3.4 ships:\n\n| Instead of | Use |\n|------------|-----|\n| `h-screen`, `height: 100vh` | `h-dvh` (`height: 100dvh`) |\n| `min-h-screen` | `min-h-dvh` |\n| `max-h-[80vh]` | `max-h-[80dvh]` |\n\nThe library already uses them: `AppShell` fills `min-h-dvh` (its sidebar is `h-dvh`), side `Sheet`s are `h-dvh`, and `Dialog`, `Drawer`, `Tour` and the toast stack cap their height in `dvh`. If you only need a fixed full-height page and the content must never resize while the toolbar moves, `h-svh` (the smallest viewport) is the steadier choice.\n\n## 6. The app shell recipe: `AppShell` + `FootNav` + `Sheet`\n\n[`AppShell`](AppShell.md) gives you a sidebar on desktop, a top bar with a navigation drawer below `lg`, and a phone tab bar via `bottomNav`. Pass a [`FootNav`](FootNav.md) there: it renders below `md` only, and `AppShell` adds the `FootNav.Spacer` after `<main>` for you. Open quick tasks in a bottom [`Sheet`](Sheet.md) from a FAB item:\n\n```jsx\nimport { useState } from 'react';\nimport { Link } from 'react-router-dom';\nimport { AppShell, FootNav, Sheet, Button, Input } from '@dreamtree-org/twreact-ui';\nimport { Home, CheckSquare, Plus, Inbox } from 'lucide-react';\n\nexport function Layout({ path, children }) {\n const [adding, setAdding] = useState(false);\n\n return (\n <AppShell\n brand={<span className=\"text-lg font-bold\">PracticePro</span>}\n nav={[\n { id: 'home', label: 'Home', href: '/', active: path === '/' },\n { id: 'tasks', label: 'Tasks', href: '/tasks', active: path === '/tasks' },\n { id: 'inbox', label: 'Inbox', href: '/inbox', active: path === '/inbox' },\n ]}\n linkAs={Link}\n pageTitle=\"Tasks\"\n data-sheet-background\n bottomNav={\n <FootNav\n linkAs={Link}\n hideOnScroll\n items={[\n { id: 'home', label: 'Home', icon: <Home />, to: '/', active: path === '/' },\n { id: 'tasks', label: 'Tasks', icon: <CheckSquare />, to: '/tasks', active: path === '/tasks', badge: 3 },\n {\n id: 'add',\n label: 'Add',\n icon: <Plus />,\n variant: 'action',\n 'aria-haspopup': 'dialog',\n 'aria-expanded': adding,\n onClick: () => setAdding(true),\n },\n { id: 'inbox', label: 'Inbox', icon: <Inbox />, to: '/inbox', active: path === '/inbox' },\n ]}\n />\n }\n >\n {children}\n\n <Sheet open={adding} onClose={() => setAdding(false)} title=\"New task\" snapPoints={[0.5, 1]} scaleBackground>\n <Sheet.Body>\n <Input label=\"Title\" />\n </Sheet.Body>\n <Sheet.Footer>\n <Button onClick={() => setAdding(false)}>Save</Button>\n </Sheet.Footer>\n </Sheet>\n </AppShell>\n );\n}\n```\n\n- Keep `bottomNav` to the 3–5 most used destinations; the full `nav` stays in the drawer and the sidebar.\n- `data-sheet-background` goes to the `AppShell` root (it forwards `...rest`), so `scaleBackground` can scale the page behind the sheet, as iOS does. It does nothing under reduced motion.\n- Only render a `FootNav.Spacer` yourself when you use `FootNav` without `AppShell`.\n- For a form's primary action, use [`BottomActionBar`](BottomActionBar.md) (with `<BottomActionBar.Spacer />`) rather than a hand-built `fixed bottom-0` bar: it pads the safe area and rides above the on-screen keyboard.\n- Other native patterns ship as components: [`PullToRefresh`](PullToRefresh.md), [`SwipeableListItem`](SwipeableListItem.md), [`SearchField`](SearchField.md), [`SegmentedControl`](SegmentedControl.md) and [`ActionSheet`](ActionSheet.md).\n\n## 7. Overlays: `presentation` overrides\n\nOn small touch screens (`(pointer: coarse) and (max-width: 639px)`), these components switch to a phone presentation by default (`presentation=\"auto\"`):\n\n| Component | Phone presentation |\n|-----------|--------------------|\n| [`Select`](Select.md), [`DatePicker`](DatePicker.md), [`DateRangePicker`](DateRangePicker.md), [`Popover`](Popover.md), [`SearchField`](SearchField.md) results | Bottom `Sheet` |\n| [`Dialog`](Dialog.md), [`ConfirmDialog`](ConfirmDialog.md) | Bottom `Sheet` instead of the centred modal |\n| [`ThreeDotPopover`](ThreeDotPopover.md), the [`Table`](Table.md) row menu | Action sheet |\n| [`Tour`](Tour.md) | Step card docked at the bottom |\n\nEverywhere else, and on the server and while hydrating, they render the desktop popover or modal. Each takes `presentation=\"auto\" | \"popover\" | \"sheet\"`. Override it only for a reason:\n\n- **Force `popover`** when the content needs the context around it: a small `Popover` next to the control it adjusts, a sort menu that should not cover the list, a `Tour` step that must point at its target, or a `Dialog` that a tablet-sized coarse-pointer screen should keep centred.\n- **Force `sheet`** when the content is long or full of touch controls on every screen size: a filter panel, a long `Select` list, a multi-field quick form.\n- Leave `auto` alone otherwise. The breakpoint matches the one the rest of the library uses, so all overlays switch together.\n\n`Tooltip` has no sheet presentation (it opens on long-press on touch screens); use `Popover` for interactive content.\n\n## 8. Safe areas\n\nWith `viewport-fit=cover` set, the stylesheet's utilities pad for the notch, rounded corners and home indicator:\n\n| Class | Pads |\n|-------|------|\n| `pt-safe`, `pb-safe`, `pl-safe`, `pr-safe` | One edge (`env(safe-area-inset-*)`) |\n| `px-safe` | Left and right |\n| `py-safe` | Top and bottom |\n| `inset-safe` | Sets `top` / `right` / `bottom` / `left` to the insets (for positioned elements) |\n\nAlready padded, so don't add them again: the `AppShell` top bar (`pt-safe px-safe`), sidebar (`pt-safe pb-safe pl-safe`) and content column (`px-safe`); `FootNav` (`pb-safe`); `BottomActionBar` (`pb-safe px-safe`); `Sheet` (bottom: `pb-safe px-safe`, top: `pt-safe px-safe`, sides: `py-safe` plus the outer edge).\n\nAdd them yourself to any edge bar you build: a custom sticky header gets `pt-safe`, a full-bleed bottom element `pb-safe`, and in landscape any full-width row `px-safe`.\n\n## 9. Global mobile baseline\n\nImporting the library's CSS gives every page this baseline (from `src/styles/index.css`):\n\n- **No tap highlight.** `-webkit-tap-highlight-color: transparent` on `:root`; components show their own pressed and `focus-visible` states instead. `text-size-adjust: 100%` stops iOS from enlarging text in landscape.\n- **No zoom on input focus.** On coarse pointers, text `input`s, `select`s and `textarea`s are at least 16px (`font-size: max(16px, 1em) !important`, so it beats `text-sm`). iOS Safari zooms into fields with smaller text. Checkboxes, radios, ranges, colour and file inputs are not affected. Add `dt-keep-font-size` to a field to opt it out.\n- **No rubber-band overscroll.** `body` has `overscroll-behavior-y: none`, so the page doesn't bounce or chain scroll, and the browser's own pull-to-refresh doesn't fight [`PullToRefresh`](PullToRefresh.md). To get the browser's pull-to-refresh back, add `dt-overscroll-auto` to `<html>` or `<body>`.\n- **`color-scheme`** follows the theme (`light` by default, `dark` under `.dark` / `[data-theme=\"dark\"]`), so native scrollbars and form controls match.\n- **Reduced motion.** Under `prefers-reduced-motion: reduce`, animations and transitions are cut to near zero, and components such as `Sheet`, `FootNav` and `Dialog` skip their slides.\n- Touch targets in the components grow to 44px on coarse pointers.\n\n## 10. Haptics are opt-in\n\nNothing vibrates unless you ask. [`useHaptics`](useHaptics.md) wraps `navigator.vibrate` with named patterns (`selection`, `success`, `warning`, `error`), and `Switch`, `Checkbox`, `ConfirmDialog` and `Sheet` take a `haptics` prop that defaults to `false`:\n\n```jsx\n{/* Each alongside its usual props (open, onClose, onConfirm, …) */}\n<Switch haptics rightLabel=\"Notifications\" />\n<ConfirmDialog haptics destructive />\n<Sheet haptics snapPoints={[0.5, 1]} />\n```\n\n- iOS Safari has no vibration API, so haptics never fire on iPhones. Treat them as a bonus on Android, never as the only feedback.\n- Calls are no-ops on the server, while hydrating, under reduced motion and before the user's first interaction.\n- To let users turn haptics off, pass your setting as `useHaptics({ enabled })` or as the components' `haptics` prop.\n\n## 11. Real-device testing checklist\n\nEmulators and desktop responsive mode miss most of these. Test on at least one iPhone (iOS Safari) and one Android phone (Chrome), in the browser and installed to the home screen.\n\n**Safe area**\n- [ ] On a notched iPhone, nothing sits under the notch or the Dynamic Island in portrait, and nothing hides under the rounded corners in landscape.\n- [ ] The `FootNav` labels and `BottomActionBar` buttons sit above the home indicator.\n- [ ] A bottom `Sheet`'s last row isn't covered by the home indicator.\n\n**No zoom on input focus**\n- [ ] Tapping an `Input`, `Select`, `Textarea` or `SearchField` does not zoom the page on iOS Safari.\n- [ ] Pinch-zoom still works (the viewport meta doesn't block it).\n- [ ] With the keyboard open, the `BottomActionBar` and a bottom `Sheet` stay above it.\n\n**Sheet swipe**\n- [ ] Dragging a bottom `Sheet` (handle, header or body scrolled to the top) follows the finger and dismisses past the threshold; a short drag snaps back.\n- [ ] With `snapPoints`, the sheet settles on each snap point.\n- [ ] Scrolling inside `Sheet.Body` scrolls the content, not the page behind it.\n- [ ] The left navigation drawer of `AppShell` closes when dragged left.\n\n**No horizontal scroll at 375px**\n- [ ] At 375px wide (iPhone SE / mini), no page scrolls sideways. Wide tables, long words and fixed-width elements are the usual cause.\n- [ ] Five `FootNav` tabs fit, and long labels truncate.\n\n**Standalone mode**\n- [ ] The app installs from Android Chrome (\"Install app\") and iOS Safari (Share → Add to Home Screen) with the right icon and name.\n- [ ] It opens without browser UI, and the status bar / toolbar colour matches the theme in light and dark (including after switching with `ThemeProvider`).\n- [ ] The splash screen colour matches the first frame.\n- [ ] Every screen has a way back without a browser back button, and links within `scope` stay in the app.\n\n**General**\n- [ ] Pressing a button gives visual feedback with no grey tap flash.\n- [ ] Overlays open as sheets on the phone and as popovers / modals on a desktop browser.\n- [ ] With Reduce Motion on (iOS Settings → Accessibility → Motion; Android Remove animations), sheets and the tab bar appear without sliding.\n\n[← Component overview](README.md)\n",
|
|
6499
|
+
"Navbar": "# Navbar\n\nTop navigation bar with logo, nav items (links or buttons), optional search, notifications dropdown, and login/logout. Uses an **emitter** for notification actions (view, markAsRead, markAsUnread, clearAll, delete). Responsive: left icon and hamburger on mobile; desktop nav and search. You can replace the right area with a custom component via `rightMenuContent`. The bar, search box and dropdown panels use the semantic `bg-surface` / `border-line` / `text-foreground` tokens, so they follow dark mode.\n\n## Import\n\n```jsx\nimport { Navbar } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `emitter` | `object` | **required** | Event emitter for notifications (e.g. `notification:view`, `notification:markAsRead`, etc.). |\n| `logo` | `ReactNode` | — | Logo (e.g. image or link). Truncates on narrow screens. |\n| `center` | `ReactNode` | — | Centre slot (e.g. a search field or primary action) between the logo and the right-hand icons. Rendered in a `min-w-0 flex-1` column so it shrinks first on narrow screens; give its content a fluid width such as `w-full max-w-md`. |\n| `items` | `Array` | `[]` | Nav items: `{ id, label, href?, active?, onClick?(item) }`. If `href` is set, renders `<a>`; else `<button>` with `onClick`. |\n| `user` | `object` | — | `{ name, email, avatar?, menuItems? }`. If set, shows the user menu and a logout button; if omitted, a login button. Without `avatar`, the menu trigger shows `name` initials (via `Avatar`). |\n| `notifications` | `Array` | `[]` | List of notifications for the dropdown. Each: `{ id, title, message?, read? }`. |\n| `searchable` | `boolean` | `false` | Show the search input from `sm` up. Below `sm` a **Search** icon button opens a top `Sheet` holding a [`SearchField`](SearchField.md) instead (same term, same `onSearch`). |\n| `onSearch` | `(term: string) => void` | — | Called with the current term on search form submit (desktop) or Enter in the phone search sheet (the sheet then closes). |\n| `sticky` | `boolean` | `false` | Sticks the bar to the top (`sticky top-0 z-sticky`), pads it with the top safe area (`pt-safe`) and makes it translucent (`bg-surface/80 backdrop-blur` where `backdrop-filter` is supported; opaque `bg-surface` otherwise). |\n| `largeTitle` | `ReactNode` | — | iOS large title: an `<h1>` under the bar that collapses into a compact centred title in the bar once the window scrolls past 44px (and expands again under 8px). Passive scroll listener, SSR-safe (expanded on the server), instant under `prefers-reduced-motion`. |\n| `leftIcon` | `ReactNode` \\| `false` | `<ChevronLeft />` | Icon shown on mobile (e.g. back). Set `false` to hide. |\n| `onLeftIconClick` | `() => void` | — | Called when left icon (mobile) is clicked. |\n| `rightMenuContent` | `(props) => ReactNode` | — | Custom right menu. Receives `{ notifications }` and any Navbar `...props` (e.g. `emitter`, `onFetch`). If not provided, default renders notifications dropdown. |\n| `onLoginClick` | `() => void` | — | Called when the login icon button (accessible name \"Sign in\") is clicked. |\n| `onLogoutClick` | `() => void` | — | Called when the logout icon button (accessible name \"Sign out\") or the user-menu **Sign out** item is clicked. |\n| `onFetch` | `({ setNotifications }) => void` | — | Refresh handler for the default notifications dropdown. Called on mount and when `notifications` changes. Consumed by the right menu — it is **not** forwarded onto `<nav>`. |\n| `className` | `string` | — | Wrapper CSS classes. |\n| ...rest | — | — | Only DOM-valid attributes (e.g. `id`, `aria-*`, `data-*`, event handlers) are forwarded to the root `<nav>`. The component's own control props (`isMobileOpen`, `setIsMobileOpen`, `onFetch`, …) are consumed and never leak to the DOM. |\n\n`Navbar` forwards `ref` to its root `<nav>` element.\n\n## Touch targets and labels\n\nIcon buttons (back, search, notifications, sign in/out, menu, user menu) are 36px on desktop and grow to the 44×44px touch target on coarse pointers, with a `focus-visible` ring. Accessible names: **Go back**, **Search**, **Notifications** (or **Notifications, 3 unread**), **Sign in**, **Sign out**, **Open menu** / **Close menu** (with `aria-expanded`) and **User menu**.\n\nThe unread badge grows with its count (`min-w-4` + padding) and caps at **99+**. It is `aria-hidden`; the count is in the bell's name.\n\n## Sticky bar and large title\n\n```jsx\n<Navbar emitter={emitter} logo={<Logo />} sticky largeTitle=\"Invoices\" searchable onSearch={search} />\n```\n\nThe large title collapses with the window scroll, so use it on pages that scroll the document (not an inner scroll container). Add `viewport-fit=cover` to your viewport meta for the safe-area inset.\n\n## Small screens (≤ 390px)\n\nThe header fits a 390px phone: columns use `gap` + `min-w-0` so the logo\ntruncates and the `center` slot shrinks instead of overflowing. Below the `sm`\nbreakpoint the standalone logout icon is hidden and a **Sign out** item appears\nat the bottom of the user menu instead (it calls `onLogoutClick`). With\n`searchable`, the inline search box is replaced by a **Search** button that\nopens the search sheet. From `sm` up the layout is unchanged.\n\n## Centre slot and initials avatar\n\n```jsx\n<Navbar\n emitter={emitter}\n logo={<Logo />}\n center={<Input type=\"search\" placeholder=\"Search tasks...\" className=\"w-full max-w-md\" />}\n user={{ name: 'Ada Lovelace', email: 'ada@example.com' }} // no avatar → \"AL\"\n/>\n```\n\n## Item shape (items)\n\n- `id`: string or number (key).\n- `label`: string.\n- `href`: optional; if set, item is an `<a href={href}>`.\n- `active`: optional boolean; applies primary styling.\n- `onClick(item)`: optional; used when no `href` (button click).\n\n## Notification shape (notifications)\n\n- `id`: string or number.\n- `title`: string.\n- `message`: optional string.\n- `read`: optional boolean; unread count uses `!n.read`.\n\n## Default notifications dropdown\n\nIf `rightMenuContent` is not provided, the right menu shows a bell icon and dropdown with:\n\n- Unread count badge.\n- \"Clear all\" that clears the list and calls emitter `notification:clearAll`.\n- Per notification: title, message, \"Mark as Read\" / \"Mark as Unread\", \"Delete\" (emitter events).\n- Clicking a notification calls `emitter.emit('notification:view', [n])`.\n\nTo refresh the list from the server, pass `onFetch({ setNotifications })`; the default dropdown uses it on mount and when `notifications` prop changes.\n\n## Example\n\n```jsx\nimport { Navbar } from '@dreamtree-org/twreact-ui';\nimport MyEmitter from './emitter';\n\n<Navbar\n emitter={MyEmitter}\n logo={<a href=\"/\"><img src=\"/logo.svg\" alt=\"Home\" /></a>}\n items={[\n { id: '1', label: 'Home', href: '/', active: pathname === '/' },\n { id: '2', label: 'About', href: '/about' },\n { id: '3', label: 'Settings', onClick: () => openSettings() },\n ]}\n user={user}\n notifications={notifications}\n searchable\n onSearch={(term) => doSearch(term)}\n onLoginClick={() => navigate('/login')}\n onLogoutClick={logout}\n onFetch={async ({ setNotifications }) => {\n const res = await fetch('/api/notifications');\n const data = await res.json();\n setNotifications(data.items);\n }}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
6500
|
+
"PageHeader": "# PageHeader\n\nPage-level block: the page title (`h1`), a muted subtitle, the page's actions (one primary action) and an optional back link. Extracted from taskpro `features/shared/PageHeader.jsx`.\n\n## Import\n\n```jsx\nimport { PageHeader } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `title` | `ReactNode` | — | Page title, rendered as the page's only `h1` |\n| `subtitle` | `ReactNode` | — | One line under the title |\n| `actions` | `ReactNode` | — | Buttons on the right; keep one primary action |\n| `actionsPlacement` | `'inline' \\| 'bottom'` | `\"inline\"` | `inline`: actions next to the title. `bottom`: on phones (coarse pointer and ≤639px) the actions are pinned in a [`BottomActionBar`](BottomActionBar.md) (`fullWidth`) at the bottom of the screen; on desktop, and on the server until hydration, they stay inline |\n| `back` | `{ href: string; label: string }` | — | Back link for detail pages. Grows to a 44px touch target on coarse pointers; `focus-visible` ring |\n| `linkAs` | `ElementType` | `\"a\"` | Link element for the back link |\n| `className` | `string` | — | Extra CSS classes (merged with `cn`) |\n| ...rest | `HTMLAttributes<HTMLDivElement>` | — | Forwarded to the root `<div>`; the ref is forwarded to it |\n\n## Example\n\n```jsx\n<PageHeader\n title=\"Compliance calendar\"\n subtitle=\"Track deadlines, ownership and filing readiness across clients\"\n actions={<Button leftIcon={<Plus className=\"h-4 w-4\" />}>Add obligation</Button>}\n/>\n```\n\n### Actions at the bottom on phones\n\n```jsx\n<PageHeader\n title=\"New invoice\"\n back={{ href: '/invoices', label: 'Invoices' }}\n actionsPlacement=\"bottom\"\n actions={\n <>\n <Button variant=\"outline\">Save draft</Button>\n <Button>Send invoice</Button>\n </>\n }\n/>\n{form}\n<BottomActionBar.Spacer className=\"sm:hidden\" />\n```\n\nWith `actionsPlacement=\"bottom\"`, put a `BottomActionBar.Spacer` at the end of the page so the bar doesn't cover the last fields. The actions render once (never duplicated), so ids and test queries stay unique.\n\n[← Component overview](README.md)\n",
|
|
6501
|
+
"Pagination": "# Pagination\n\nPage navigation: first, previous, page numbers, next, last. Renders only when `totalPages > 1`. Optional `theme` for styling (e.g. from Table).\n\n## Import\n\n```jsx\nimport { Pagination } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `currentPage` | `number` | `1` | Current page (1-based). |\n| `totalPages` | `number` | `1` | Total pages. If ≤ 1, component returns null. |\n| `onPageChange` | `(page: number) => void` | — | Called when user selects a page. |\n| `showFirstLast` | `boolean` | `true` | Show first/last page buttons (and the `...` ellipses). The jump-to-first / jump-to-last button only renders when page 1 / the last page is outside the visible page window, so no page number ever appears twice. |\n| `showPrevNext` | `boolean` | `true` | Show previous/next buttons. |\n| `maxVisiblePages` | `number` | `5` | Max page number buttons to show. |\n| `className` | `string` | — | Nav wrapper CSS classes. |\n| ...rest | — | — | Passed to `<nav>`. |\n\n**Responsive:** below the `sm` breakpoint (640 px) the bar collapses to a compact `‹ Page 3 of 10 ›` layout. Only previous/next and the status text show; the page numbers, jump buttons and ellipses are hidden with CSS (`hidden sm:inline-flex`). From `sm` up the full layout is unchanged. When `showPrevNext={false}`, nothing would drive the compact view, so the full layout shows at every width. Buttons wrap instead of overflowing, grow to a 44 px touch target on coarse pointers, show a `focus-visible` ring, and have dark-mode colours.\n\n## Example\n\n```jsx\n<Pagination\n currentPage={page}\n totalPages={Math.ceil(total / pageSize)}\n onPageChange={setPage}\n maxVisiblePages={5}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
6502
|
+
"Popover": "# Popover\n\nAnchored, click-triggered panel for **interactive** content — filter forms, share menus, quick editors. Unlike `Tooltip` (hover/focus, read-only text), a Popover takes focus, can hold buttons and inputs, and stays open until dismissed. Positioned in a portal by the shared floating engine (flip to the opposite side, shift to stay in view). On phones it becomes a bottom sheet (see **Presentation**).\n\n## Import\n\n```jsx\nimport { Popover } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `trigger` | `ReactElement` | — | Element that toggles the popover (required). Gets `aria-haspopup`, `aria-expanded`, `aria-controls` and an `onClick`; its own `onClick` still runs (call `preventDefault()` to stop the toggle). A string is wrapped in a `<button>` |\n| `children` | `ReactNode or function` | — | Panel content, or `({ close }) => ReactNode` |\n| `open` | `boolean` | — | Controlled open state |\n| `defaultOpen` | `boolean` | `false` | Initial state when uncontrolled |\n| `onOpenChange` | `function` | — | `(open) => void`, called on every open/close |\n| `placement` | `string` | `\"bottom-start\"` | `top`, `bottom`, `left`, `right`, each optionally with `-start` or `-end`; flips when there is no room |\n| `offset` | `number` | `8` | Gap in px between trigger and panel |\n| `title` | `ReactNode` | — | Heading inside the panel; labels the dialog |\n| `initialFocusRef` | `RefObject` | — | Element to focus on open (default: first focusable element) |\n| `disabled` | `boolean` | `false` | Disable the trigger |\n| `presentation` | `\"auto\"`, `\"popover\"` or `\"sheet\"` | `\"auto\"` | `auto` shows a bottom [`Sheet`](Sheet.md) on small touch screens (`(pointer: coarse) and (max-width: 639px)`) and the anchored panel elsewhere; `popover` / `sheet` force one. `auto` renders the panel on the server and while hydrating |\n| `id` | `string` | — | Panel id (auto-generated otherwise) |\n| `className` | `string` | — | Extra classes for the panel, or for the sheet panel in sheet mode (merged with `cn`) |\n| `...rest` | `HTMLAttributes` | — | Forwarded to the panel (`aria-label`, `data-*`, …); `ref` points at the panel (the sheet panel in sheet mode). The panel carries `data-presentation=\"popover\"` or `\"sheet\"` |\n\nSupports both modes: **uncontrolled** (`defaultOpen`) for most cases, **controlled** (`open` + `onOpenChange`) when something else must open or close it.\n\n## Examples\n\n### Basic\n\n```jsx\n<Popover trigger={<Button variant=\"outline\">Filters</Button>} title=\"Filter tasks\">\n {({ close }) => (\n <div className=\"space-y-3\">\n <Checkbox label=\"Assigned to me\" />\n <Button size=\"sm\" onClick={close}>Apply</Button>\n </div>\n )}\n</Popover>\n```\n\n### Controlled\n\n```jsx\nconst [open, setOpen] = useState(false);\n\n<Popover open={open} onOpenChange={setOpen} placement=\"top-end\" aria-label=\"Share\" trigger={<Button>Share</Button>}>\n <p>Anyone with the link can view.</p>\n</Popover>\n```\n\n### Presentation (phones)\n\n```jsx\n// Default: anchored panel on desktop, bottom sheet on phones\n<Popover trigger={<Button>Sort</Button>} title=\"Sort by\">…</Popover>\n\n// Keep the anchored panel everywhere\n<Popover presentation=\"popover\" trigger={<Button>Sort</Button>}>…</Popover>\n```\n\nIn sheet mode the content renders inside `<Sheet.Body>` of a bottom `Sheet`: `title` becomes the sheet title (with its close button), and the sheet adds a grab handle, drag-to-dismiss, a backdrop and the safe-area padding. `open` / `onOpenChange`, the `({ close })` render prop, `initialFocusRef`, `className`, `id` and `ref` work the same way.\n\n## Accessibility & keyboard\n\n| Behavior | Detail |\n|----------|--------|\n| Trigger | `aria-haspopup=\"dialog\"`, `aria-expanded`, `aria-controls` (the panel id while open) |\n| Panel | `role=\"dialog\"` (non-modal), labelled by `title` or your `aria-label` |\n| Focus on open | `initialFocusRef`, else the first focusable element, else the panel |\n| `Escape` | Closes and returns focus to the trigger |\n| Outside click | Closes; focus stays where the user clicked |\n| `Tab` past the last item / `Shift`+`Tab` before the first | Closes and continues from the trigger, as if the panel sat right after it |\n| Reduced motion | Fade-in is disabled under `prefers-reduced-motion` |\n| Dark mode | Panel uses `dark:` variants |\n| Sheet mode | The sheet is a **modal** `role=\"dialog\"` (`aria-modal=\"true\"`): focus is trapped inside, `Escape`, a backdrop tap or a downward drag closes it, and focus returns to the trigger |\n\n[← Component overview](README.md)\n",
|
|
5376
6503
|
"PriceRangePicker": "# PriceRangePicker\n\nDual-thumb range slider for min/max values (e.g. price or any number). Optional number inputs and preset buttons. Controlled via `value` or uncontrolled via `defaultValue`.\n\n## Import\n\n```jsx\nimport { PriceRangePicker } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `min` | `number` | `0` | Minimum value. |\n| `max` | `number` | `1000` | Maximum value. |\n| `step` | `number` | `1` | Step for thumbs and inputs. |\n| `minGap` | `number` | `5` | Minimum gap between min and max. |\n| `value` | `{ min, max }` \\| `null` | `null` | Controlled value. |\n| `defaultValue` | `{ min, max }` | `{ min: 0, max: 1000 }` | Uncontrolled initial value. |\n| `onChange` | `(value: { min, max }) => void` | `() => {}` | Called on thumb/input change. |\n| `onFinalChange` | `(value: { min, max }) => void` | `() => {}` | Called when a thumb drag ends (mouse, touch or pen), on each thumb key press, and on Enter/blur in the inputs. |\n| `showInputs` | `boolean` | `true` | Show min/max number inputs. |\n| `showTooltips` | `boolean` | `true` | Show a value tooltip above the thumb while dragging. |\n| `disabled` | `boolean` | `false` | Disable the control. |\n| `presets` | `Array<{ id, label, value: { min, max } }>` | `[]` | Preset buttons. |\n| `formatValue` | `(n: number) => string` | `(n) => String(n)` | Format display (e.g. currency). |\n| `className` | `string` | `\"\"` | Container CSS classes. |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Thumb and spacing size. |\n| `colorClass` | `string` | `\"bg-primary-600\"` | Tailwind `bg-*` class for the active track and both thumbs. |\n| `ariaLabel` | `string` | `\"Price range\"` | Accessibility label. |\n\n## Accessibility and touch\n\n- Each thumb is a `role=\"slider\"` (`aria-label` is `\"{ariaLabel} - min\"` / `\"{ariaLabel} - max\"`). Arrow keys step, Page Up/Down move 10% of the range, Home/End jump to the bounds.\n- Pointer drag uses pointer capture: pressing anywhere on the track grabs the nearest thumb, and a release outside the track still ends the drag. Both thumbs work with mouse, touch and pen.\n- On touch screens each thumb has a 44px hit area; the number inputs use `inputMode=\"decimal\"`.\n- Since 1.4: the default colour is the brand `primary` token (was pink), and the component no longer injects a global `<style>` that restyled every `input[type=range]` on the page.\n\n## Example\n\n```jsx\nconst [range, setRange] = useState({ min: 100, max: 500 });\n<PriceRangePicker\n min={0}\n max={1000}\n value={range}\n onChange={setRange}\n formatValue={(n) => `$${n}`}\n presets={[\n { id: '1', label: 'Under $200', value: { min: 0, max: 200 } },\n ]}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
5377
|
-
"ProgressBar": "# ProgressBar\n\nProgress bar showing a value between 0 and max. Uses the brand's `primary` tokens by default (`bg-primary-600` fill, `bg-primary-100` track), so it follows `--dt-*` theming.
|
|
5378
|
-
"
|
|
5379
|
-
"
|
|
5380
|
-
"
|
|
5381
|
-
"
|
|
5382
|
-
"
|
|
5383
|
-
"
|
|
5384
|
-
"
|
|
5385
|
-
"
|
|
5386
|
-
"
|
|
6504
|
+
"ProgressBar": "# ProgressBar\n\nProgress bar showing a value between 0 and max. Uses the brand's `primary` tokens by default (`bg-primary-600` fill, `bg-primary-100` track), so it follows `--dt-*` theming. Pick another token tone with `variant`. Also supports an optional label, value text inside the bar, striped and animated styles.\n\n## Import\n\n```jsx\nimport { ProgressBar } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `number` | `0` | Current value. |\n| `max` | `number` | `100` | Maximum value. |\n| `size` | `\"xs\"` \\| `\"sm\"` \\| `\"md\"` \\| `\"lg\"` \\| `number` | `\"md\"` | Height: xs/sm/md/lg or px. |\n| `showValue` | `boolean` | `false` | Show percentage text inside the fill. |\n| `rounded` | `boolean` | `true` | Rounded corners. |\n| `variant` | `\"primary\"` \\| `\"secondary\"` \\| `\"success\"` \\| `\"warning\"` \\| `\"destructive\"` | `\"primary\"` | Token tone: fill `bg-<tone>-600`, track `bg-<tone>-100` (`dark:bg-<tone>-900/40`), value text `text-white`. `destructive` uses the `error` palette. Replaces the HEX `color` / `bgColor` / `valueColor` props removed in 2.0.0 ([migration](MIGRATION-2.0.md#progressbar-hex-colour-props)). |\n| `striped` | `boolean` | `false` | Striped fill pattern. |\n| `animated` | `boolean` | `false` | Animate stripe movement (`animate-progress-stripes`, a Tailwind keyframe, so no `<style>` tag is injected). Stops under `prefers-reduced-motion`. Value changes animate the fill width over 300 ms, with no transition under reduced motion. |\n| `label` | `string` | `\"\"` | Label above the bar. |\n| `ariaLabel` | `string` | `\"Progress\"` | Accessibility label. |\n| `className` | `string` | `\"\"` | Wrapper CSS classes. |\n| `style` | `object` | `{}` | Wrapper inline style. |\n\n## Example\n\n```jsx\n<ProgressBar value={60} max={100} />\n<ProgressBar value={75} showValue label=\"Upload\" />\n<ProgressBar value={40} striped animated size=\"lg\" />\n<ProgressBar value={90} variant=\"success\" showValue label=\"Storage\" />\n```\n\n[← Component overview](README.md)\n",
|
|
6505
|
+
"PullToRefresh": "# PullToRefresh\n\nTouch pull-to-refresh around a scroll container. Pull down while the list is scrolled to the top: a spinner follows the finger (rubber-band resistance, rotation and opacity tracking the pull). Release past `threshold` and `onRefresh()` runs; the spinner keeps spinning until its promise settles, then retracts. Normal scrolling is never intercepted.\n\n## Import\n\n```jsx\nimport { PullToRefresh } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `onRefresh` | `() => Promise \\| void` | — | Called on release past `threshold`. The indicator stays in the refreshing state until the returned promise settles (resolve or reject); a synchronous return settles on the next tick. Handle your own errors inside it |\n| `threshold` | `number` | `64` | Pull distance in px (after resistance) that arms a refresh; the refreshing indicator rests at this offset |\n| `maxPull` | `number` | `threshold * 2` | Ceiling of the resistance-scaled pull in px |\n| `disabled` | `boolean` | `false` | Ignores pulls (a refresh already in flight still finishes) |\n| `refreshingLabel` | `string` | `\"Refreshing…\"` | Text of the polite live region (`role=\"status\"`, `aria-live=\"polite\"`) while refreshing |\n| `scrollRef` | `RefObject<HTMLElement>` | — | Your own scroll container inside `children`. Omit it and PullToRefresh renders the scroller (`h-full overflow-y-auto overscroll-y-contain`) |\n| `contentClassName` | `string` | — | Extra classes for the inner content element (the scroller when `scrollRef` is omitted) |\n| `children` | `ReactNode` | — | The list / feed |\n| `className` | `string` | — | Root classes (merged with `cn`). Give the root a height (`h-96`, `flex-1`, …) so the inner scroller can scroll |\n| `...rest` | `HTMLAttributes` | — | Forwarded to the root `div`; `ref` points at the root |\n\n## Behavior\n\n- **Gesture:** touch only (desktop has no pull-to-refresh; give mouse and keyboard users a refresh button too). A pull starts only while the scroller's `scrollTop` is `0` and the finger moves down; a sideways swipe, an upward drag, or a scrolled list is left to the browser. Scroll back to the top mid-gesture and you can keep pulling; scroll the content mid-pull and the gesture is handed back.\n- **Listeners:** `touchstart`, `touchend` and `touchcancel` are passive. `touchmove` is non-passive, but calls `preventDefault()` only while it is actively pulling at the top, so ordinary scrolling is never blocked. A cancelled touch retracts without refreshing.\n- **Page overscroll:** the library's global `overscroll-behavior-y: none` on `body` keeps the browser's own pull-to-refresh and bounce from fighting this one; the built-in scroller also uses `overscroll-y-contain`.\n- **States:** the root carries `data-state=\"idle\" | \"pulling\" | \"refreshing\"`; the content gets `aria-busy=\"true\"` while refreshing. The spinner turns the primary colour once the pull passes `threshold`.\n- **Reduced motion:** under `prefers-reduced-motion` the indicator is static: no rotation while pulling, no spin while refreshing, and the retract is instant (no transition). The content still follows the finger (direct manipulation).\n- **Dark mode:** the indicator uses the `bg-surface` / `border-line` / `text-muted` tokens.\n\n## Examples\n\n```jsx\n<PullToRefresh onRefresh={() => refetchOrders()} className=\"h-[calc(100dvh-4rem)]\">\n <ul>{orders.map((o) => <OrderRow key={o.id} order={o} />)}</ul>\n</PullToRefresh>\n```\n\n```jsx\n// Your own scroll container\nconst listRef = useRef(null);\n\n<PullToRefresh scrollRef={listRef} onRefresh={refetch} refreshingLabel=\"Loading new orders…\" className=\"h-96\">\n <ul ref={listRef} className=\"h-full overflow-y-auto\">{rows}</ul>\n</PullToRefresh>\n```\n\n[← Component overview](README.md)\n",
|
|
6506
|
+
"Radio": "# Radio\n\nSingle radio button; use multiple with the same `name` for a group. Controlled via `checked` or uncontrolled via `defaultChecked`.\n\n## Import\n\n```jsx\nimport { Radio } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `id` | `string` | auto | Input id. |\n| `name` | `string` | — | Group name (required for grouping). |\n| `value` | `any` | — | Value when selected. |\n| `checked` | `boolean` | — | Controlled checked. |\n| `defaultChecked` | `boolean` | — | Uncontrolled initial. |\n| `onChange` | `(e: Event) => void` | — | Change handler. |\n| `disabled` | `boolean` | `false` | Disable. |\n| `required` | `boolean` | `false` | Required. |\n| `label` | `string` | — | Label text. |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Size. |\n| `labelPosition` | `\"left\"` \\| `\"right\"` | `\"right\"` | Label side. |\n| `radioColor` | `string` | `\"rgb(var(--dt-primary-600))\"` | Accent colour for the checked dot, as any CSS colour. Defaults to the brand's primary token, so it follows theming; prefer leaving it unset. |\n| `className` | `string` | `\"\"` | Label wrapper classes. |\n| `ariaLabel` | `string` | — | Accessibility label. |\n\n## Behavior\n\n- **Uncontrolled groups** (`defaultChecked`, no `checked`): selecting one radio\n re-syncs every same-`name` Radio in the same form, so only one dot is ever\n shown as selected. Give the group a shared `name`.\n- **Focus:** the native input is visually hidden; keyboard focus draws a\n `focus-visible` ring on the visual circle.\n- The visual circle exposes `data-state=\"checked\" | \"unchecked\"` for styling.\n- **Look:** token surfaces (`bg-surface`, grey ring with a dark counterpart); the\n ring takes the accent colour when checked and the dot springs in (off under\n `prefers-reduced-motion`). The whole row is the hit area and is at least 44px\n tall on coarse pointers.\n\n## Example\n\n```jsx\nconst [choice, setChoice] = useState('a');\n<>\n <Radio name=\"choice\" value=\"a\" checked={choice === 'a'} onChange={() => setChoice('a')} label=\"Option A\" />\n <Radio name=\"choice\" value=\"b\" checked={choice === 'b'} onChange={() => setChoice('b')} label=\"Option B\" />\n</>\n```\n\n[← Component overview](README.md)\n",
|
|
6507
|
+
"Rate": "# Rate\n\nStar (or custom icon) rating input/display. Controlled via `value` or uncontrolled via `defaultValue`. Supports hover preview and optional read-only. Stars use token classes (`text-warning-400` active, `text-gray-200` inactive); the inline `activeColor` / `inactiveColor` overrides were removed in 2.0.0 ([migration](MIGRATION-2.0.md#rate-activecolor-and-inactivecolor)).\n\n## Import\n\n```jsx\nimport { Rate } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `count` | `number` | `5` | Number of stars. |\n| `value` | `number` | — | Controlled value (0 to count). |\n| `defaultValue` | `number` | `0` | Uncontrolled initial value. |\n| `readOnly` | `boolean` | `false` | Read-only (no onChange). |\n| `icon` | `ReactNode` | `<StarOutline />` | Icon for inactive star. |\n| `toggledIcon` | `ReactNode` | `<StarFilled />` | Icon for active star. |\n| `size` | `number` | `20` | Icon size in px. |\n| `onChange` | `(value: number) => void` | — | Called when value changes. |\n| `onClick` | `(value: number) => void` | — | Called on star click. |\n| `text` | `ReactNode` | — | Optional text (e.g. label) next to stars. |\n| `className` | `string` | `\"\"` | Wrapper CSS classes. |\n| `id` | `string` | — | Wrapper id. |\n| `name` | `string` | — | Form field name. |\n\n## Accessibility & interaction\n\n- Rendered as a `radiogroup` of star `radio`s. Only the star equal to the\n current value has `aria-checked=\"true\"`.\n- **Roving tabindex:** the group is one tab stop — the selected star, or the\n first star when the value is `0`.\n- **Keyboard:** `ArrowRight` / `ArrowUp` and `ArrowLeft` / `ArrowDown` move\n focus to the next / previous star **and** select it (calling `onChange`,\n clamped to 1…`count`); `Home` / `End` jump to the first / last star;\n `Enter` / `Space` commit the focused star (calling `onChange` and `onClick`).\n- **Hover preview** is shown for mouse/pen pointers only and clears on pointer\n leave; touch taps never leave a stuck preview.\n- **Touch drag:** on touch screens, slide a finger across the stars — the\n highlighted value follows the finger (pointer capture keeps tracking it\n even off the stars; sliding past either end clamps to 1 or `count`) and is\n committed once, on release (`onChange` and `onClick` fire then, and focus\n moves to that star). A tap without sliding still selects via click.\n `pointercancel` drops the preview. The group uses `touch-action: pan-y`,\n so vertical page scrolling still works over it.\n- Each star grows to a 44×44 px hit area on coarse (touch) pointers.\n- `readOnly` removes the stars from the tab order.\n\n## Example\n\n```jsx\nconst [rating, setRating] = useState(0);\n<Rate value={rating} onChange={setRating} count={5} />\n<Rate defaultValue={3} readOnly />\n```\n\n[← Component overview](README.md)\n",
|
|
6508
|
+
"RichTextEditor": "# RichTextEditor\n\nWYSIWYG rich-text field built on Tiptap (StarterKit): bold, italic, strike, inline code, headings, lists, quotes, links and undo/redo, with a keyboard-accessible toolbar and the same `label` / `error` / `helperText` wiring as `Input`. Lives on the `/rich-text` entry point so Tiptap is an optional peer dependency.\n\n## Install\n\n```bash\nnpm install @tiptap/react @tiptap/pm @tiptap/starter-kit\n# TypeScript + React 18: keep the React type packages on your React major\nnpm install -D @types/react@18 @types/react-dom@18\n```\n\n## Import\n\n```jsx\nimport { RichTextEditor } from '@dreamtree-org/twreact-ui/rich-text';\n```\n\nNever import it from the root `@dreamtree-org/twreact-ui` — it is not exported there.\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `string` or `JSONContent` | — | Controlled content: an HTML string (or Tiptap JSON). An external change replaces the content without calling `onChange`. |\n| `defaultValue` | `string` or `JSONContent` | — | Uncontrolled initial content. |\n| `onChange` | `(value, editor) => void` | — | Called on every edit with HTML (or Tiptap JSON when `outputFormat=\"json\"`) and the Tiptap editor. |\n| `outputFormat` | `\"html\"` or `\"json\"` | `\"html\"` | What `onChange` receives. |\n| `label` | `ReactNode` | — | Visible label; names the editor through `aria-labelledby`. Clicking it focuses the editor. |\n| `error` | `ReactNode` | — | Error message: red border, `aria-invalid`, icon + text linked with `aria-describedby`. |\n| `helperText` | `ReactNode` | — | Hint below the field, linked with `aria-describedby` (shown together with `error`). |\n| `placeholder` | `string` | — | Shown while the document is empty; also `aria-placeholder`, and the accessible name when there is no `label` or `aria-label`. |\n| `required` | `boolean` | `false` | Visual `*` and `aria-required`. |\n| `disabled` | `boolean` | `false` | Not editable, dimmed, toolbar buttons disabled, `aria-disabled`. |\n| `readOnly` | `boolean` | `false` | Shows the formatted content, not editable, toolbar hidden, `aria-readonly`. |\n| `toolbar` | `RichTextToolbarItem[]` or `false` | `bold, italic, strike, code, heading2, heading3, bulletList, orderedList, blockquote, link, undo, redo` | Toolbar buttons in order (also available: `heading1`, `codeBlock`), or `false` for none. |\n| `toolbarLabel` | `string` | `\"Formatting\"` | Accessible name of the `role=\"toolbar\"`. |\n| `stickyToolbar` | `boolean`, `number` or `string` | `false` | Keep the toolbar pinned (`sticky`, `z-sticky`) to the top of the scrolling page while long content scrolls. `true` sticks at the top; a number (px) or CSS length sets the offset, e.g. `\"3.5rem\"` below `AppShell`'s sticky top bar. The frame switches from `overflow-hidden` to `overflow-clip` so the toolbar can stick. |\n| `minHeight` | `number` or `string` | `120` | Minimum height of the editable area (px or CSS length). |\n| `maxHeight` | `number` or `string` | — | Height after which the editable area scrolls. |\n| `autoFocus` | `boolean` | `false` | Focus the editor on mount. |\n| `onFocus` | `(event) => void` | — | Editor focus. |\n| `onBlur` | `(event) => void` | — | Editor blur. |\n| `extensions` | `AnyExtension[]` | — | Extra Tiptap extensions, appended after StarterKit. |\n| `editorClassName` | `string` | — | Extra classes for the editable (ProseMirror) element. |\n| `id` | `string` | — | Id of the editable element (auto-generated otherwise). |\n| `className` | `string` | — | Extra classes for the root `<div>` (merged with `cn`). |\n| `ref` | `Ref<RichTextEditorHandle>` | — | Imperative handle: `editor`, `element`, `focus()`, `clear()`, `getHTML()`, `getJSON()`, `setContent(content)`. |\n| ...rest | `HTMLAttributes<HTMLDivElement>` | — | Forwarded to the root `<div>`. |\n\nWorks controlled (`value` + `onChange`) or uncontrolled (`defaultValue`, read with the ref). Controlled is recommended for forms.\n\nThe editor renders on the client after mount (`immediatelyRender: false`), so it is safe in Next.js / SSR; the label, toolbar and placeholder render on the server.\n\n### Security: sanitize the HTML on the server\n\n`onChange` returns whatever HTML the editor holds, and a user can send any HTML to your API without using the editor at all. **Always sanitize rich-text HTML on the server before you store or render it** (for example with DOMPurify / `sanitize-html` and an allow-list of the tags and attributes above). This library ships no sanitizer, and its components never render raw HTML. The link form only accepts `http(s):`, `mailto:`, `tel:`, relative and `#` URLs, but that is a UX guard, not a security boundary.\n\n## Examples\n\n### Controlled\n\n```jsx\nconst [body, setBody] = useState('<p>Hello</p>');\n\n<RichTextEditor\n label=\"Description\"\n placeholder=\"Describe the task…\"\n helperText=\"Markdown shortcuts work: **bold**, # heading, - list.\"\n value={body}\n onChange={setBody}\n maxHeight={320}\n/>\n```\n\n### Compact toolbar with validation\n\n```jsx\n<RichTextEditor label=\"Comment\" required toolbar={['bold', 'italic', 'link']} minHeight={80} error={errors.comment} />\n```\n\n### Uncontrolled + ref\n\n```jsx\nconst editorRef = useRef(null);\n\n<RichTextEditor label=\"Notes\" defaultValue=\"<p>Draft</p>\" ref={editorRef} />\n<Button onClick={() => save(editorRef.current.getHTML())}>Save</Button>\n```\n\n### Sticky toolbar for long documents\n\n```jsx\n// Inside AppShell: stick below its 56px top bar.\n<RichTextEditor label=\"Article\" stickyToolbar=\"3.5rem\" value={body} onChange={setBody} />\n```\n\n### JSON output\n\n```jsx\n<RichTextEditor label=\"Body\" outputFormat=\"json\" onChange={(doc) => setDoc(doc)} />\n```\n\n## Accessibility\n\n| Behavior | Detail |\n|----------|--------|\n| Field | The editable element is `role=\"textbox\"` with `aria-multiline=\"true\"`, named by `label` (`aria-labelledby`), else `aria-label`, else `placeholder` |\n| Description | `error` and `helperText` ids (plus any `aria-describedby` you pass) are joined into `aria-describedby`; `aria-invalid`, `aria-required`, `aria-readonly`, `aria-disabled` reflect the props |\n| Toolbar | `role=\"toolbar\"` with `aria-label` and `aria-controls`; one tab stop with roving focus (Left / Right arrows, Home, End); every control is a real `<button>` with `aria-label`, `title` and `aria-keyshortcuts` |\n| Touch | Toolbar buttons are 32px on desktop and grow to 44px on coarse pointers. They cancel `pointerdown` (and `mousedown`), so tapping one keeps the editor selection on iOS and the format applies to the selected text |\n| Toggle state | Mark / block buttons expose `aria-pressed`; Undo / Redo expose `aria-disabled` when nothing can be undone / redone |\n| Keyboard shortcuts | StarterKit defaults: Mod+B, Mod+I, Mod+Shift+S, Mod+E, Mod+Alt+1..3, Mod+Shift+7 / 8, Mod+Shift+B, Mod+Z, Mod+Shift+Z, plus Markdown input rules |\n| Links | The link button opens an inline URL field (Enter applies, Escape cancels and returns focus to the editor); invalid schemes show a `role=\"alert\"` message |\n| Error | Paired with an icon and text, not colour alone |\n| Focus ring | `focus-within:ring-2 ring-primary-500` on the editing area (error ring when invalid); toolbar buttons use `focus-visible:ring-2` |\n| Dark mode | Surface, border and content (headings, quotes, code, links) use `dark:` variants; the toolbar uses the role tokens (`bg-background`, `border-line`, `text-muted`, `text-foreground`) |\n\n[← Component overview](README.md)\n",
|
|
6509
|
+
"RoundedTag": "# RoundedTag\n\nTag/chip with optional avatar (image or initials) and optional remove button. Used for labels, selected items, or filters.\n\n## Import\n\n```jsx\nimport { RoundedTag } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `label` | `string` | — | Main label text. |\n| `avatarSrc` | `string` | `null` | Image URL for avatar. |\n| `avatarAlt` | `string` | `\"avatar\"` | Alt for avatar image. |\n| `initials` | `string` | `\"\"` | Initials when no image (e.g. \"AB\"). |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Size. |\n| `onRemove` | `(e?) => void` | `null` | If provided, shows remove button and calls on click. The button is labelled `Remove <label>` (plain `Remove` when `label` is not a string). |\n| `onClick` | `(e?) => void` | `null` | Click handler for the tag. When set, the tag becomes `role=\"button\"`, joins the tab order (`tabIndex=0`), activates on Enter/Space, and shows a focus-visible ring. |\n| `className` | `string` | `\"\"` | Wrapper CSS classes. |\n| `closeClass` | `string` | `\"\"` | Remove button CSS classes. |\n| `ariaLabel` | `string` | — | Optional accessibility label. No default — when omitted, the visible `label` text is the accessible name (previously defaulted to `\"tag\"`, which hid the label from screen readers). |\n| `avatarPosition` | `\"left\"` \\| `\"right\"` | `\"left\"` | Avatar position. |\n| `avatarClassname` | `string` | `\"\"` | Avatar wrapper classes. |\n| `initialClassname` | `string` | `\"\"` | Initials span classes. |\n\nThe tag, its avatar and the remove button have dark-mode tones. A clickable tag shrinks slightly while pressed (off under `prefers-reduced-motion`).\n\n## Example\n\n```jsx\n<RoundedTag label=\"React\" />\n<RoundedTag label=\"John\" initials=\"JD\" onRemove={() => {}} />\n<RoundedTag label=\"With image\" avatarSrc=\"/user.jpg\" size=\"lg\" />\n```\n\n[← Component overview](README.md)\n",
|
|
6510
|
+
"SearchField": "# SearchField\n\niOS-style search input: a `type=\"search\"` field with a leading search icon, a clear button, and a **Cancel** text button on phones while the field is focused. Optionally shows results: in a full-height `Sheet` with the field pinned at the top on phones, and in a popover beneath the field elsewhere.\n\n## Import\n\n```jsx\nimport { SearchField } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `string` | — | Controlled value. Pair with `onValueChange` (or `onChange`) |\n| `defaultValue` | `string` | `\"\"` | Uncontrolled initial value |\n| `onValueChange` | `(value: string) => void` | — | Called with the new text on every change: typing, the clear button, Escape and Cancel. Recommended for controlled use |\n| `onChange` | `(event) => void` | — | The input's change event while typing. Clear / Escape / Cancel call it with `{ target: { value: \"\" } }` (same convention as `Input`) |\n| `onSearch` | `(value: string) => void` | — | Called with the current text on Enter (not during IME composition) |\n| `onClear` | `() => void` | — | Called after the clear button (or Escape) empties the field |\n| `onCancel` | `() => void` | — | Called after Cancel clears and blurs the field (and closes the results) |\n| `showCancel` | `boolean` \\| `\"auto\"` | `\"auto\"` | `\"auto\"`: Cancel shows on phones while the field is focused. `true`: always. `false`: never |\n| `cancelLabel` | `string` | `\"Cancel\"` | Cancel button text |\n| `clearLabel` | `string` | `\"Clear search\"` | Accessible name of the clear button |\n| `placeholder` | `string` | `\"Search\"` | Placeholder; also the default accessible name |\n| `results` | `ReactNode` | — | Results content. Passing it (or `renderResults`) turns on the results surface |\n| `renderResults` | `({ value, close }) => ReactNode` | — | Render function for results; `close()` hides them. Return `null` to show nothing (e.g. while `value` is empty) |\n| `resultsOpen` | `boolean` | — | Controlled results visibility. Uncontrolled by default: opens on focus / typing (popover) or on tap / typing (sheet) |\n| `onResultsOpenChange` | `(open: boolean) => void` | — | Called when the results want to open or close |\n| `resultsLabel` | `string` | `\"Search results\"` | Accessible name of the results region and of the results sheet |\n| `presentation` | `\"auto\"` \\| `\"popover\"` \\| `\"sheet\"` | `\"auto\"` | How results present. `\"auto\"`: a sheet on coarse-pointer screens under 640px, a popover elsewhere (popover on the server). Also decides whether `showCancel=\"auto\"` shows Cancel |\n| `disabled` | `boolean` | `false` | Disables the field; no clear or Cancel button |\n| `readOnly` | `boolean` | `false` | Read-only; no clear button |\n| `className` | `string` | — | Classes for the root wrapper (merged with `cn`), e.g. a width |\n| `inputClassName` | `string` | — | Classes for the `<input>` (merged with `cn`) |\n| `aria-label` | `string` | `placeholder` | Accessible name. Omitted when you pass `aria-labelledby` |\n| `...rest` | `InputHTMLAttributes` | — | Forwarded to the `<input>` (`name`, `id`, `autoFocus`, `aria-*`, `data-*`, handlers) |\n\n`ref` points at the `<input>`. The input always has `type=\"search\"`, `enterKeyHint=\"search\"` and `inputMode=\"search\"`, so phones show a **Search** key. The native WebKit clear button is hidden in favour of the component's own.\n\n## Keyboard\n\n| Key | Action |\n|-----|--------|\n| Enter | `onSearch(value)` |\n| Escape | Closes the results popover; with no popover open, clears the text |\n| Escape (sheet) | Closes the results sheet; focus returns to the inline field |\n| Tab | Moves out of the field and closes the popover |\n\n## Touch\n\nThe clear button reaches 44×44px on coarse pointers, and so does the field itself (40px on desktop, 44px on touch). Pressing clear or Cancel does not blur the field first, so the press always lands.\n\n## Results\n\n```jsx\nconst [q, setQ] = useState('');\nconst hits = useSearch(q);\n\n<SearchField\n value={q}\n onValueChange={setQ}\n onSearch={submit}\n renderResults={({ value, close }) =>\n value ? (\n <ul>\n {hits.map((h) => (\n <li key={h.id}>\n <button type=\"button\" onClick={() => { open(h); close(); }}>{h.title}</button>\n </li>\n ))}\n </ul>\n ) : null\n }\n/>\n```\n\n- **Desktop / tablet:** results render in a region beneath the field (`z-popover`, `bg-surface`, `border-line`), opened on focus or typing and closed by Escape, a press outside the field, tabbing away, Cancel or `close()`.\n- **Phones:** tapping or typing in the field opens a full-height bottom `Sheet` (`role=\"dialog\"`, named by `resultsLabel`) with a second field pinned at the top and the results scrolling below. That field gets focus and shares the same value; its Cancel button clears, closes the sheet and calls `onCancel`. Escape, the backdrop or a downward drag close it too.\n\nThe results region is plain content: it is not a `combobox` / `listbox`. Put real buttons or links in it.\n\n## Examples\n\n### Plain search\n\n```jsx\n<SearchField placeholder=\"Search invoices\" onSearch={(q) => router.push(`/invoices?q=${q}`)} className=\"max-w-sm\" />\n```\n\n### Always show Cancel (e.g. inside a search screen)\n\n```jsx\n<SearchField showCancel autoFocus onCancel={() => navigate(-1)} />\n```\n\n[← Component overview](README.md)\n",
|
|
6511
|
+
"SegmentedControl": "# SegmentedControl\n\niOS-style segmented control for switching a **view or value** (list / board, day / week / month). It is a `role=\"radiogroup\"` with one `role=\"radio\"` per option and a sliding thumb under the selected option. Controlled or uncontrolled. When the choice switches **panels of content**, use `Tabs variant=\"segmented\"` instead: it looks the same but has tab / tabpanel semantics.\n\n## Import\n\n```jsx\nimport { SegmentedControl } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `options` | `Array<{ value, label, icon?, disabled? }>` | `[]` | The segments. `value` is a string or number and must be unique; `label` is a node; `icon` is a node shown before the label (hidden from screen readers); `disabled` skips the option. |\n| `value` | `string` | — | Controlled selected value (a string or number matching an option's `value`). |\n| `defaultValue` | `string` | — | Uncontrolled initial value (string or number). Without `value` or `defaultValue` nothing is selected and the first enabled option is the tab stop. |\n| `onChange` | `(value) => void` | — | Receives the newly selected option's `value` (not a DOM event). Not called when the selected option is clicked again. |\n| `size` | `string` | `\"md\"` | `sm`, `md` or `lg`. Same padding and type scale as `Tabs` segments. Every size grows to 44px tall on coarse pointers (touch screens). |\n| `fullWidth` | `boolean` | `false` | Stretch to the container's width. Segments are equal width either way. |\n| `name` | `string` | — | Submits the selected value with a form through a hidden input. |\n| `disabled` | `boolean` | `false` | Disable the whole group (dimmed, out of the tab order, `aria-disabled` on the root). |\n| `aria-label` | `string` | — | Accessible name of the group. Pass this or `aria-labelledby`. |\n| `aria-labelledby` | `string` | — | Id of a visible label for the group. |\n| `className` | `string` | — | Merged onto the root with `cn`, so your classes win on conflict. |\n\nOther props (`id`, `data-*`, event handlers, …) go to the root `<div role=\"radiogroup\">`, which also receives the forwarded `ref`.\n\n## Behavior\n\n- **Keyboard:** one tab stop (roving tabindex): the selected option, or the first enabled option when nothing is selected. `ArrowLeft` / `ArrowRight` / `ArrowUp` / `ArrowDown` move focus **and select**, skip disabled options and wrap around. `Home` / `End` jump to the first / last enabled option. Under `dir=\"rtl\"` the left and right arrows swap.\n- **Thumb:** the selected background is a thumb measured from the selected option (in a layout effect, re-measured on resize) that slides between options. The slide is off under `prefers-reduced-motion`. Before the first measurement (server render, hidden container) the selected option draws its own background, so nothing flashes.\n- **Styling:** grey track (the `bg-line` role token, which follows dark mode) with a `bg-surface` thumb; the selected label is `text-foreground` and the others are `text-muted`. Options show a `focus-visible` ring and shrink slightly while pressed (off under reduced motion).\n- **Icon-only options:** give the label visually hidden text, e.g. `label: <span className=\"sr-only\">Grid</span>`, so each radio still has a name.\n\n## Example\n\n```jsx\nconst [view, setView] = useState('list');\n\n<SegmentedControl\n aria-label=\"View\"\n value={view}\n onChange={setView}\n options={[\n { value: 'list', label: 'List', icon: <List className=\"h-4 w-4\" /> },\n { value: 'board', label: 'Board' },\n { value: 'calendar', label: 'Calendar', disabled: true },\n ]}\n/>\n\n<SegmentedControl aria-label=\"Period\" fullWidth size=\"lg\" defaultValue=\"week\" options={[\n { value: 'day', label: 'Day' },\n { value: 'week', label: 'Week' },\n { value: 'month', label: 'Month' },\n]} />\n```\n\n[← Component overview](README.md)\n",
|
|
6512
|
+
"Select": "# Select\n\nSingle or multi-select dropdown with search, grouped options, creatable options, and optional select-all.\n\n> **The open menu portals to `document.body`.** In popover mode the dropdown is\n> rendered through a React portal with `position: fixed`, anchored to the\n> trigger by floating-ui: it flips above when there is no room below, shifts to\n> stay in view, matches the trigger width and caps its height to the space\n> left. It **escapes any ancestor with `overflow: hidden | auto | scroll`**\n> instead of being clipped by it, follows the trigger on scroll/resize while\n> open, and sits on the `popover` z-layer (above dialogs and sheets).\n>\n> **On phones it opens as a bottom sheet.** With the default\n> `presentation=\"auto\"`, small touch screens (`(pointer: coarse) and\n> (max-width: 639px)`) get a bottom [`Sheet`](./Sheet.md) instead:\n>\n> - It is titled by `label`, or by `placeholder` when there is no label.\n> - Search is pinned in the header and is **not** auto-focused, so the\n> keyboard does not cover the options.\n> - Option rows are 48px, with a check on the selected one.\n> - Multi-select gets a **Done** button; single-select closes on pick.\n> - It keeps `role=\"listbox\"`, `aria-selected` and the same keyboard\n> navigation.\n>\n> Pass `presentation=\"popover\"` to keep the dropdown everywhere. On coarse\n> pointers the search box is not auto-focused in popover mode either, and the\n> clear and tag-remove controls grow to a 44px touch target.\n\n## Import\n\n```jsx\nimport { Select } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `options` | `Array` | `[]` | `[{ value, label, disabled, group? }]`. With `grouped`: groups `[{ label, options: [...] }]` and/or flat options carrying a `group` key. |\n| `value` | `any` \\| `any[]` | — | Selected value(s); array when `multiSelect` |\n| `onChange` | `function` | — | `(value)` or `(values[])` |\n| `placeholder` | `string` | `\"Select an option...\"` | Placeholder text |\n| `label` | `string` | — | Visible label. Wired to the trigger (`<label for>` + `aria-labelledby`), so the control's accessible name is the label followed by the current value. |\n| `error` | `string` | — | Error message. Sets `aria-invalid` and replaces `helperText` while shown. |\n| `helperText` | `ReactNode` | — | Hint below the control, linked via `aria-describedby`. Hidden while `error` is shown. |\n| `disabled` | `boolean` | — | Disable select |\n| `required` | `boolean` | — | Required |\n| `multiSelect` | `boolean` | `false` | Allow multiple selection |\n| `searchable` | `boolean` | `false` | Show search input |\n| `grouped` | `boolean` | `false` | Render options in groups. Accepts group objects or flat options with a `group` key; malformed entries are skipped with a console warning instead of crashing. |\n| `allowClear` | `boolean` | `false` | Show the clear button (`aria-label=\"Clear selection\"`) when something is selected. It is a separate button beside the trigger and returns focus to the trigger. Off by default (it was `true` up to 1.3.2): turn it on only when an empty value is valid for the field. |\n| `creatable` | `boolean` | `false` | Allow creating new option when no match |\n| `onCreateOption` | `function` | — | Called when creating option |\n| `onSearch` | `function` | — | Search term callback |\n| `loading` | `boolean` | `false` | Loading state |\n| `selectAllOption` | `boolean` | `true` | Show select all (multi) |\n| `closeOnSelect` | `boolean` | `false` | Close dropdown on select (single) |\n| `maxTagCount` | `number` | `3` | Max tags shown in multi (rest as \"+N\") |\n| `onMenuItemRender` | `function` | — | Custom option render |\n| `renderGroupLabel` | `function` | — | Custom group label |\n| `name` | `string` | — | Form field name |\n| `size` | `string` | `\"md\"` | `sm`, `md` or `lg`. Trigger height: `sm` 32px, `md` 38px (44px on coarse pointers, so it meets the touch-target minimum on phones), `lg` 44px everywhere |\n| `presentation` | `string` | `\"auto\"` | `auto`, `popover` or `sheet`. How the open menu presents. `auto`: a bottom sheet on small touch screens (`(pointer: coarse) and (max-width: 639px)`), an anchored popover elsewhere and on the server. `popover` and `sheet` force one. |\n| `className` | `string` | — | Extra CSS classes |\n\n## Examples\n\n### Basic single select\n\n```jsx\nconst options = [\n { value: 'a', label: 'Option A' },\n { value: 'b', label: 'Option B' },\n];\n<Select\n options={options}\n value={value}\n onChange={setValue}\n placeholder=\"Choose one\"\n/>\n```\n\n### Multi select with search\n\n```jsx\n<Select\n options={options}\n value={selected}\n onChange={setSelected}\n multiSelect\n searchable\n placeholder=\"Choose multiple\"\n/>\n```\n\n### Grouped options\n\n```jsx\nconst grouped = [\n { label: 'Fruits', options: [{ value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }] },\n { label: 'Veggies', options: [{ value: 'carrot', label: 'Carrot' }] },\n];\n<Select options={grouped} grouped value={value} onChange={setValue} />\n```\n\n### Grouped from a flat list (`group` key)\n\n`grouped` also accepts the flat shape most APIs return. Options are bucketed by\n`group` in first-seen order; options without a `group` render first-seen in an\nunlabelled bucket (no header). Malformed entries (`null`, a group whose\n`options` is not an array, …) are ignored with a one-time `console.warn` — the\ncomponent never throws on bad input.\n\n```jsx\nconst connections = [\n { value: 'gh', label: 'GitHub', group: 'Code' },\n { value: 'sl', label: 'Slack', group: 'Chat' },\n { value: 'gl', label: 'GitLab', group: 'Code' },\n];\n<Select grouped options={connections} value={value} onChange={setValue} />\n```\n\n### Label, helper text and accessibility\n\n```jsx\n<Select\n label=\"Role\"\n helperText=\"Controls what this person can see.\"\n options={roles}\n value={role}\n onChange={setRole}\n/>\n// getByRole('button', { name: /^Role/ }) — the trigger is a button with\n// aria-haspopup=\"listbox\"; the open listbox is labelled by the same label.\n```\n\nPass `id` to control the trigger's id (the label's `for` follows it); otherwise\none is generated with `useId`.\n\n### Keyboard\n\nThe trigger is a button that opens a listbox (the WAI-ARIA collapsible\nlistbox pattern). Every control in the popup is a real, focusable element:\n\n- **Trigger:** Enter / Space opens the menu. Opening moves focus to the search\n box (`searchable`) or to the listbox.\n- **In the menu:** ↑ / ↓ move the active option (wrapping), Home / End jump to\n the first / last (in the listbox, not the search box), Enter selects, Space\n toggles the active option in the listbox (`multiSelect`), Escape closes. Tab\n closes and continues the tab order from the trigger.\n- Only the rows on screen are walked: search-filtered, with collapsed groups\n skipped. The active option is exposed with `aria-activedescendant` on the\n listbox (and the search box) and scrolled into view. Options carry\n `role=\"option\"`, `aria-selected` and `aria-disabled`.\n- Single selection, Escape and the clear button return focus to the trigger.\n- With `creatable`, Enter in the search box with no active option creates the\n typed value (same as clicking **Add \"…\"**).\n- **Buttons:** the clear \"X\" (`Clear selection`), the group toggle\n (`Collapse <group>` / `Expand <group>`, with `aria-expanded`), select-all\n (`Select all` / `Deselect all`) and the creatable row are all\n `<button type=\"button\">`. None of them is nested inside the trigger.\n\nWhen a value is selected the component renders two buttons (trigger and clear),\nso name the trigger in tests: `getByRole('button', { name: /^Role/ })`.\n\n### Helper text and errors\n\n`helperText` renders a muted line below the control and is linked to it via\n`aria-describedby`, so screen readers announce it with the field. **While\n`error` is set, the error message replaces the helper text** (only one line is\nshown, and `aria-describedby` points at the error). This rule is the same on\nevery field-like component: `Input`, `Select`, `DatePicker`, `DateRangePicker`\nand `Form` fields.\n\n[← Component overview](README.md)\n",
|
|
6513
|
+
"Sheet": "# Sheet\n\nPhone-native sheet that slides in from an edge: bottom (default), top, left or right. Bottom and top sheets can rest on **snap points** (e.g. half and full height), and every sheet can be **dragged to dismiss** with a rubber band past its limits. On phones the bottom sheet stays above the on-screen keyboard. Use `Sheet` for phone sheets (filters, pickers, quick forms, activity feeds); keep `Drawer` for desktop record peeks and editors.\n\n## Import\n\n```jsx\nimport { Sheet } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `open` | `boolean` | — | Controls visibility |\n| `isOpen` | `boolean` | — | Alias of `open` (same name as `Drawer` / `Dialog`) |\n| `onClose` | `(reason) => void` | — | Called when the sheet asks to close; `reason` is `'escape'`, `'backdrop'`, `'drag'` or `'close-button'` |\n| `placement` | `string` | `\"bottom\"` | `bottom`, `top`, `left`, `right` |\n| `snapPoints` | `number[]` | — | Bottom/top only. Heights as fractions of the visual viewport height, each in `(0, 1]`, e.g. `[0.5, 1]`. Without them the sheet takes its content height (capped below the top safe area) |\n| `activeSnap` | `number` | — | Controlled snap index into `snapPoints` |\n| `defaultSnap` | `number` | `0` | Uncontrolled initial snap index; restored each time the sheet opens |\n| `onSnapChange` | `(index) => void` | — | Called with the new snap index after a drag settles or an arrow key on the handle |\n| `haptics` | `boolean` | `false` | Opt-in: a `selection` vibration (10ms) each time a drag or arrow key settles on a new snap point (not when you change `activeSnap` yourself). No-op without `navigator.vibrate` (iOS Safari) and under reduced motion. See [useHaptics](useHaptics.md) |\n| `dismissible` | `boolean` | `true` | `false` disables Escape, backdrop click and drag dismissal; a drag rubber-bands back |\n| `handle` | `boolean` | `true` for `placement=\"bottom\"`, else `false` | Grab handle. With `snapPoints` it is a focusable \"Resize sheet\" button (ArrowUp / ArrowDown); otherwise decorative and `aria-hidden` |\n| `scaleBackground` | `boolean` | `false` | While open, scales and rounds the element marked `data-sheet-background` (your app wrapper). Does nothing under reduced motion |\n| `title` | `ReactNode` | — | Rendered in an automatic `Sheet.Header`; labels the dialog (`aria-labelledby`) |\n| `description` | `ReactNode` | — | Rendered under the title; wired to `aria-describedby` |\n| `showCloseButton` | `boolean` | `true` | Close (X) button in the automatic header (only rendered with `title` / `description`) |\n| `initialFocus` | `RefObject` | — | Element to focus on open (default: first focusable element, skipping the handle) |\n| `children` | `ReactNode` | — | Usually `Sheet.Header` / `Sheet.Body` / `Sheet.Footer` |\n| `className` | `string` | — | Extra classes for the panel (merged with `cn`) |\n| `...rest` | `HTMLAttributes` | — | Forwarded to the panel (`aria-label`, `data-*`, …); `ref` points at the panel |\n\n## Composition\n\n| Part | Description |\n|------|-------------|\n| `Sheet.Header` | Fixed top section. Dragging it moves the sheet (buttons inside still click) |\n| `Sheet.Body` | Scrolls internally (`overflow-y-auto overscroll-contain`). A drag on the body moves the sheet only when the body is scrolled to the top and the drag heads toward dismiss |\n| `Sheet.Footer` | Sticky bottom row for actions, right-aligned |\n\nAll parts accept `className`, `...rest` and `ref`. Content without a `title` needs a name: pass `aria-label`, or `aria-labelledby` pointing at your own heading.\n\nSafe areas are padded by the panel: `pb-safe` on bottom sheets, `pt-safe` on top sheets, `py-safe` plus the outer edge on side sheets. Add `viewport-fit=cover` to your viewport meta.\n\n## Examples\n\n### Bottom sheet\n\n```jsx\nconst [open, setOpen] = useState(false);\n\n<Button onClick={() => setOpen(true)}>Filters</Button>\n<Sheet open={open} onClose={() => setOpen(false)} title=\"Filters\" description=\"Narrow the task list\">\n <Sheet.Body>…</Sheet.Body>\n <Sheet.Footer>\n <Button variant=\"outline\" onClick={reset}>Reset</Button>\n <Button onClick={apply}>Apply</Button>\n </Sheet.Footer>\n</Sheet>\n```\n\n### Snap points\n\n```jsx\n// Uncontrolled: opens at half height.\n<Sheet open={open} onClose={close} title=\"Activity\" snapPoints={[0.5, 1]} defaultSnap={0}>\n <Sheet.Body>{comments}</Sheet.Body>\n</Sheet>\n\n// Controlled.\nconst [snap, setSnap] = useState(0);\n<Sheet open={open} onClose={close} title=\"Activity\" snapPoints={[0.5, 1]} activeSnap={snap} onSnapChange={setSnap}>\n …\n</Sheet>\n```\n\nA drag settles on the nearest snap point. A fast downward flick (faster than 0.5 px/ms) or a drag well below the lowest point dismisses the sheet when `dismissible` is on.\n\n### Side sheet\n\n```jsx\n<Sheet open={open} onClose={close} placement=\"left\" title=\"Navigation\">\n <Sheet.Body>…</Sheet.Body>\n</Sheet>\n```\n\nSide sheets are full height (`h-dvh`), `min(88vw, 24rem)` wide, and drag horizontally toward their own edge.\n\n### Form inside (keyboard-aware)\n\n```jsx\n<Sheet open={open} onClose={close} title=\"Collect payment\">\n <Sheet.Body>\n <Input label=\"Amount\" inputMode=\"decimal\" />\n </Sheet.Body>\n <Sheet.Footer><Button onClick={save}>Record payment</Button></Sheet.Footer>\n</Sheet>\n```\n\nWhen `window.visualViewport` is available, the bottom sheet listens to its `resize` and sits above the on-screen keyboard (its max height shrinks to the visible area), and a focused field stays scrolled into view.\n\n### Background scale\n\n```jsx\n<div data-sheet-background>\n <App />\n</div>\n\n<Sheet open={open} onClose={close} title=\"Share\" scaleBackground>…</Sheet>\n```\n\nOpt-in, because scaling the wrapper breaks `position: fixed` descendants inside it.\n\n## Accessibility & keyboard\n\n| Behavior | Detail |\n|----------|--------|\n| Role | Panel is `role=\"dialog\"` + `aria-modal=\"true\"`, rendered in a portal on `document.body` (nothing on the server) |\n| Focus on open | `initialFocus`, else the first focusable element (not the handle), else the panel |\n| Focus trap | `Tab` / `Shift`+`Tab` cycle inside the panel |\n| Focus restore | On close, focus returns to the element that opened it |\n| `Escape` | Closes with reason `'escape'` when `dismissible`; only the top-most overlay reacts |\n| Scroll lock | Body scroll is locked while open (reference-counted, safe with stacked overlays) |\n| Snap points | The handle is a `button` labelled \"Resize sheet\": `ArrowUp` grows to the next snap point, `ArrowDown` shrinks (inverted for `placement=\"top\"`) |\n| Drag | Pointer events with pointer capture on the handle and header; the body only at its scroll top. Escape and the close button remain the keyboard path |\n| Close button | 44×44px touch target on coarse pointers |\n| Reduced motion | No slide-in/out, no drag snap-back animation, `scaleBackground` is ignored |\n| Dark mode | Uses the `surface`, `foreground`, `muted` and `line` role colours |\n\n[← Component overview](README.md)\n",
|
|
6514
|
+
"Sidebar": "# Sidebar\n\nCollapsible side navigation with optional nested items, logo, and user block. Supports desktop collapsed state and mobile drawer. The content renders **once**: from `md` up it is a sticky in-flow column, below `md` the same tree is an off-canvas drawer. Tooltips show when collapsed; optional `drawerPosition` for mobile. The panel uses the semantic `bg-surface` / `border-line` / `text-foreground` tokens (section labels use `text-muted`), so it follows dark mode.\n\nEvery item — parent or child — renders as an `<a>`. Items with `href` are real links, so Ctrl/Cmd-click, Shift-click and middle-click open them in a new tab or window (the sidebar does not intercept those clicks). Items without `href` render as `<a role=\"button\">` and stay keyboard-operable (Enter / Space).\n\n## Import\n\n```jsx\nimport { Sidebar } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `items` | `Array` | `[]` | Nav items: `{ id, label, icon, href?, target?, rel?, children?, onClick?, active?, disabled?, badge?, badgeLabel?, group? }`. `children` = nested items. |\n| `collapsed` | `boolean` | `false` | Desktop collapsed state (icon-only). |\n| `onToggle` | `() => void` | — | Toggle collapse (e.g. chevron click). |\n| `className` | `string` | — | Classes for the sidebar container inside the `<aside>` (the element `ref` points to). Applies to both the column and the drawer. |\n| `containerClassName` | `string` | — | Classes for the outer desktop sticky wrapper (default `sticky top-0 h-[100dvh]`; `display: contents` below `md`), merged with `tailwind-merge` — e.g. `\"h-full static\"` to size it from its parent. |\n| `stickyOffset` | `number` \\| `string` | — | Height of a top bar above the sidebar (number = px, or any CSS length such as `\"4rem\"`). The desktop sidebar sticks at that offset and its height becomes `calc(100dvh - offset)`, so a footer / user block stays on screen. Omit for the default full-viewport `100dvh`. |\n| `expandedIds` | `string[]` | — | Controlled open groups (parent item ids). |\n| `defaultExpandedIds` | `string[]` | `[]` | Initially open groups (uncontrolled). |\n| `onExpandedChange` | `(ids: string[]) => void` | — | Called with the next open ids when a group is opened/closed (and, when controlled, to request opening the active item's ancestors). |\n| `logo` | `ReactNode` | — | Logo at top. |\n| `footer` | `ReactNode` \\| `({ collapsed, isMobile, closeMobile }) => ReactNode` | — | Pinned region at the bottom, above the user block (e.g. Settings / Help). The function form lets it adapt to the collapsed rail and close the mobile drawer on click. |\n| `user` | `object` | — | User block at bottom: `{ name, email, avatar? }`. |\n| `onUserClick` | `(user) => void` | — | When set, the user block is a real `<button type=\"button\">` (Enter/Space work natively). Without it the block is static content. |\n| `drawerPosition` | `\"left\"` \\| `\"right\"` | `\"left\"` | Mobile drawer side. |\n| `isMobileOpen` | `boolean` | — | Controlled mobile open. |\n| `setIsMobileOpen` | `(open: boolean) => void` | — | Set mobile open (when controlled). |\n| `showCollapsedTooltips` | `boolean` | `true` | Show tooltips when collapsed. |\n| `swipeable` | `boolean` | `true` | Phones: swipe in from the screen edge (20px, the `drawerPosition` side) to open the drawer, and swipe the open drawer back toward its edge to close it. Needs `setIsMobileOpen`. `false` turns both gestures off. |\n| `tooltipOptions` | `object` | — | Options for internal Tooltip. |\n| `linkAs` | `ElementType` | `\"a\"` | Element used for items with `href` — pass Next.js `Link` (or a React Router wrapper that maps `href` to `to`) for client-side navigation. Receives `href`, `target`, `rel`, `className`, `onClick` and ARIA props. |\n\n## Item shape\n\n- `id`: string (required for expand/active).\n- `label`: string.\n- `icon`: ReactNode.\n- `children`: optional array of same shape (nested items).\n- `href`: optional URL. The item renders as a link (`linkAs`, default `<a>`) and gets `aria-current=\"page\"` when active.\n- `target` / `rel`: optional anchor attributes; `rel` defaults to `\"noopener noreferrer\"` when `target=\"_blank\"`.\n- `onClick`: optional handler, called with the item on a plain click of a leaf. Not called for Ctrl/Cmd/Shift/Alt-clicks on links — those are left to the browser.\n- `active`: optional boolean for current page.\n- `disabled`: optional boolean; the item is dimmed, has no `href`, is skipped in the tab order and ignores clicks.\n- `badge`: optional `string | number` — a pill after the label; when collapsed, a compact badge on the icon. `0`, `\"\"`, `null` and `undefined` render nothing.\n- `badgeLabel`: optional string — accessible text for the badge (e.g. `\"3 unread\"`); the visible value is then hidden from screen readers. Collapsed items are named `\"<label>, <badgeLabel ?? badge>\"`.\n- `group`: optional string (top-level items only) — items sharing a `group` render under that section heading, in order of first appearance (same convention as `AppShell`'s `nav[].group`). Ungrouped items render without a heading. When collapsed, headings become dividers (the name stays available to screen readers via `role=\"group\"` + `aria-labelledby`).\n\n## Expansion\n\n- **The group holding the current page is open on load.** Every ancestor of an item with `active: true` (at any depth) is opened on mount and whenever the active item changes; a parent that is itself `active` opens too. Groups the user opened stay open, and the user can still close the active group.\n- **Uncontrolled** (default): `defaultExpandedIds` seeds the open groups; `onExpandedChange(ids)` reports user changes.\n- **Controlled**: pass `expandedIds` + `onExpandedChange`. Sidebar never changes the open set itself; when the active item's ancestors are missing it calls `onExpandedChange` with them added (on mount and when the active item changes).\n\n```jsx\nconst [expandedIds, setExpandedIds] = useState(() => JSON.parse(localStorage.navOpen ?? '[]'));\n<Sidebar items={items} expandedIds={expandedIds} onExpandedChange={(ids) => {\n setExpandedIds(ids);\n localStorage.navOpen = JSON.stringify(ids);\n}} />\n```\n\n## Mobile drawer\n\nBelow `md` (`(min-width: 768px)` does not match) the sidebar is a drawer (`<aside role=\"dialog\" aria-modal=\"true\" aria-label=\"Navigation\">`) driven by `isMobileOpen` / `setIsMobileOpen`. While closed it is `inert` and `aria-hidden`, so its links are out of the tab order and the accessibility tree. While open: focus moves into the drawer, Tab / Shift+Tab are trapped inside it, **Escape** calls `setIsMobileOpen(false)`, and on close focus returns to the element that had it before (e.g. your menu button). Body scroll is locked while open (ref-counted and shared with `Dialog` / `Drawer`, so closing the drawer under a stacked dialog keeps the page locked); while CSS hides the drawer at `md`+, Tab is not trapped. Only the top-most overlay reacts to Escape. The slide is disabled under `prefers-reduced-motion`.\n\nFrom `md` up the same `<aside>` is a plain column: no `role=\"dialog\"`, never `inert`, no focus trap and no scroll lock, even if `isMobileOpen` is still `true`. On the server (and while hydrating) the sidebar renders as the column; on a phone it switches to the closed drawer right after hydration. The drawer is never collapsed — `collapsed` only affects the column — and its header button reads **Close navigation** and calls `setIsMobileOpen(false)`.\n\nThe drawer is `h-dvh`, padded with `pt-safe` / `pb-safe` and the safe area on its outer edge (`pl-safe` left, `pr-safe` right; add `viewport-fit=cover` to your viewport meta). It stacks with the z-layer tokens: the backdrop on `z-overlay`, the drawer on `z-modal` (no inline `zIndex`). The collapse / close button is 44×44px.\n\n### Swipe gestures\n\nWith `swipeable` (default) and `setIsMobileOpen`:\n\n- **Swipe to close.** Drag the open drawer toward its edge; it follows the finger and closes past 80px or on a flick (faster than 0.5 px/ms). Shorter drags spring back. The drawer is `touch-pan-y`, so vertical scrolling of a long nav stays native.\n- **Edge swipe to open.** A horizontal touch swipe that starts within 20px of the screen edge (left, or right for `drawerPosition=\"right\"`) pulls the drawer in and opens it past 56px or on a flick. The listeners sit on `window`, so nothing covers the page edge and taps there still reach your content. Vertical swipes are ignored.\n\n## Height / top bar\n\nBy default the desktop sidebar is `sticky top-0` and `100dvh` tall. Under a fixed/sticky top bar pass its height:\n\n```jsx\n<header className=\"sticky top-0 h-16\">…</header>\n<div className=\"flex\">\n <Sidebar items={items} stickyOffset={64} footer={<HelpLink />} />\n <main className=\"flex-1\">…</main>\n</div>\n```\n\nOr size it from a container with `containerClassName=\"static h-full\"` (the parent must have a height).\n\n## Click behavior\n\n| Item | Plain click | Ctrl/Cmd/Shift-click or middle-click |\n|------|-------------|--------------------------------------|\n| Leaf with `href` | Navigates; calls `onClick`; closes the mobile drawer | Browser opens a new tab/window; sidebar does nothing |\n| Parent with `href` | Navigates and expands its group (a second click keeps it open) | Browser opens a new tab/window; group is not toggled |\n| Parent chevron (with `href`) | Toggles the group without navigating | — |\n| Item without `href` | Leaf: calls `onClick`. Parent: toggles its group | Same as plain click |\n\nParents expose `aria-expanded` and `aria-controls`.\n\n## Sections, badges and footer\n\n```jsx\n<Sidebar\n linkAs={Link}\n items={[\n { id: 'home', label: 'Home', href: '/', icon: <Home /> },\n { id: 'inbox', label: 'Inbox', href: '/inbox', group: 'Work', badge: 12, badgeLabel: '12 unread', icon: <Mail /> },\n { id: 'billing', label: 'Billing', group: 'Finance', icon: <Receipt />, children: [\n { id: 'invoices', label: 'Invoices', href: '/billing/invoices', active: true }, // \"Billing\" opens on load\n ]},\n ]}\n footer={({ collapsed, closeMobile }) => (\n <Link href=\"/settings\" onClick={closeMobile} aria-label={collapsed ? 'Settings' : undefined}>\n <Settings />{!collapsed && 'Settings'}\n </Link>\n )}\n/>\n```\n\n## Example\n\n```jsx\nconst [collapsed, setCollapsed] = useState(false);\n<Sidebar\n items={[\n { id: 'dash', label: 'Dashboard', icon: <Home /> },\n { id: 'settings', label: 'Settings', icon: <Settings />, children: [\n { id: 'profile', label: 'Profile' },\n { id: 'security', label: 'Security' },\n ]},\n ]}\n collapsed={collapsed}\n onToggle={() => setCollapsed(!collapsed)}\n logo={<Logo />}\n user={{ name: 'Jane', avatar: '/jane.jpg' }}\n/>\n```\n\n### Links (Next.js)\n\n```jsx\nimport Link from 'next/link';\n\n<Sidebar\n linkAs={Link}\n items={[\n { id: 'home', label: 'Home', href: '/', icon: <Home />, active: pathname === '/' },\n { id: 'settings', label: 'Settings', href: '/settings', icon: <Settings />, children: [\n { id: 'profile', label: 'Profile', href: '/settings/profile' },\n { id: 'security', label: 'Security', href: '/settings/security' },\n ]},\n { id: 'docs', label: 'Docs', href: 'https://example.com/docs', target: '_blank' },\n ]}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
6515
|
+
"Skeleton": "# Skeleton\n\nPlaceholder blocks for loading states. Renders shimmer bars (or circles) when `active` is true; otherwise renders `children`. Items use the `bg-gray-200` token class (`dark:bg-gray-700`), so brands and dark mode apply.\n\n## Import\n\n```jsx\nimport { Skeleton } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `count` | `number` | `1` | Number of skeleton items. |\n| `circle` | `boolean` | `false` | Render items as circles. |\n| `height` | `number` \\| `string` | `16` | Height in px or CSS unit (e.g. `\"2rem\"`). |\n| `width` | `number` \\| `string` | — | Width; default 100% (column) or height (inline/circle). |\n| `rounded` | `boolean` | `true` | Rounded corners (when not circle). |\n| `animated` | `boolean` | `true` | Shimmer animation (`animate-shimmer`, a Tailwind keyframe, so no `<style>` tag is injected). The highlight is soft white in light mode and a faint 10% white in dark mode, so no bright streak shows. Under `prefers-reduced-motion` the bone is static and plain. |\n| `active` | `boolean` | `true` | If true show skeletons; if false render children. |\n| `gap` | `string` | `\"8px\"` | Spacing between items. |\n| `inline` | `boolean` | `false` | Layout items in a row. |\n| `className` | `string` | `\"\"` | Wrapper CSS classes. |\n| `style` | `object` | `{}` | Wrapper inline style. |\n| `children` | `ReactNode` | `null` | Rendered when `active` is false. |\n\n## Examples\n\n```jsx\n<Skeleton />\n<Skeleton count={3} height={20} gap=\"12px\" />\n<Skeleton circle height={40} />\n<Skeleton active={loading} count={2}>\n <RealContent />\n</Skeleton>\n```\n\n[← Component overview](README.md)\n",
|
|
6516
|
+
"Slider": "# Slider\n\nHorizontal slider for a single value or a range (two thumbs). Pass a number for one thumb, a `[low, high]` array for a range. Keyboard, pointer (mouse, touch, pen) and form submission (hidden inputs) are built in. For a min/max **price** picker with text inputs and presets, `PriceRangePicker` remains available.\n\n## Import\n\n```jsx\nimport { Slider } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `number or [number, number]` | — | Controlled value; an array makes it a range slider |\n| `defaultValue` | `number or [number, number]` | `min` | Uncontrolled initial value |\n| `onChange` | `function` | — | `(value) => void` on every change (drag or key) |\n| `onChangeEnd` | `function` | — | `(value) => void` when a drag ends or a key press commits |\n| `min` | `number` | `0` | Minimum |\n| `max` | `number` | `100` | Maximum |\n| `step` | `number` | `1` | Step; decimals are supported |\n| `largeStep` | `number` | — | PageUp/PageDown increment (default: a tenth of the range) |\n| `disabled` | `boolean` | `false` | Disable interaction |\n| `label` | `ReactNode` | — | Visible label; names a single thumb |\n| `showValue` | `boolean` | `false` | Show the formatted value next to the label |\n| `showBubble` | `boolean` | `true` | Show a value bubble (formatted with `formatValue`) above the thumb while it is dragged; hidden otherwise |\n| `formatValue` | `function` | `String` | `(number) => string`; used for display and `aria-valuetext` |\n| `thumbLabels` | `[string, string]` | `[\"Minimum\", \"Maximum\"]` | Accessible names of the two range thumbs |\n| `name` | `string` | — | Emits hidden inputs: `name`, or `name[0]` and `name[1]` for a range |\n| `className` | `string` | — | Extra classes for the root (merged with `cn`) |\n| `...rest` | `HTMLAttributes` | — | Forwarded to the root; `ref` points at it. `aria-label` names the thumb when there is no `label` |\n\nRange thumbs never cross: the low thumb stops at the high value and vice versa.\n\n## Examples\n\n### Single value\n\n```jsx\n<Slider label=\"Volume\" defaultValue={40} showValue />\n```\n\n### Range (controlled)\n\n```jsx\nconst [budget, setBudget] = useState([2000, 8000]);\n\n<Slider\n label=\"Budget\"\n min={0}\n max={10000}\n step={500}\n value={budget}\n onChange={setBudget}\n showValue\n formatValue={(v) => `₹${v.toLocaleString('en-IN')}`}\n/>\n```\n\n## Accessibility & keyboard\n\nOn coarse pointers (touch screens) the track row is 44px tall and each thumb has a 44×44 hit area; the visual size is unchanged. Thumbs use a `focus-visible` ring.\n\n| Key | Action |\n|-----|--------|\n| `ArrowRight` / `ArrowUp` | Increase by `step` |\n| `ArrowLeft` / `ArrowDown` | Decrease by `step` |\n| `PageUp` / `PageDown` | Increase / decrease by `largeStep` |\n| `Home` / `End` | Minimum / maximum (for a range thumb: up to the other thumb) |\n\nEach thumb is `role=\"slider\"` with `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext` and `aria-orientation=\"horizontal\"`; range thumbs report the other thumb as their bound. Disabled thumbs are `aria-disabled` and removed from the tab order. Thumbs have the standard focus ring; the track and fill use `dark:` variants.\n\n[← Component overview](README.md)\n",
|
|
6517
|
+
"Sortable": "# Sortable\n\nReorderable list with **pointer drag** (mouse, touch, pen — no extra dependency) **and keyboard reordering** announced through a live region. Replaces hand-rolled Move up / Move down buttons; `moveUp` / `moveDown` are still handed to `renderItem` if you want them as a visible fallback.\n\n## Import\n\n```jsx\nimport { Sortable } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `items` | `array` | `[]` | Items in their current order |\n| `onReorder` | `function` | — | `(newItems, { from, to }) => void` after a drop; set your state to `newItems` |\n| `renderItem` | `function` | — | `(item, state) => ReactNode`; `state` = `{ index, originalIndex, isDragging, isGrabbed, handleProps, moveUp, moveDown }`. Defaults to the item label |\n| `getKey` | `function` | `item.id or item.key or index` | Stable key per item |\n| `getItemLabel` | `function` | `label, title, name or the string itself` | Text used in screen-reader announcements |\n| `disabled` | `boolean` | `false` | Turn reordering off |\n| `handle` | `boolean` | `false` | Drag only from the element you spread `handleProps` on. The handle has `touch-none`, so a touch drag on it starts immediately; without a handle, touch drags start after a long-press |\n| `orientation` | `string` | `\"vertical\"` | `vertical` or `horizontal` (arrow keys and drop detection follow it) |\n| `as` | `string` | `\"ul\"` | `ul`, `ol` or `div` (a `div` list gets `role=\"list\"` / `listitem`) |\n| `itemClassName` | `string` | — | Classes for every item wrapper |\n| `messages` | `object` | — | Override announcement text: `instructions`, `grabbed`, `moved`, `dropped`, `cancelled` |\n| `className` | `string` | — | Extra classes for the list (merged with `cn`) |\n| `...rest` | `HTMLAttributes` | — | Forwarded to the list (`aria-label`, `data-*`, …); `ref` points at it |\n\n`Sortable` is controlled: it previews the move while dragging/carrying and calls `onReorder` once on drop. It never mutates `items`.\n\n## Examples\n\n### Basic\n\n```jsx\nconst [tasks, setTasks] = useState(initialTasks);\n\n<Sortable\n aria-label=\"Backlog\"\n items={tasks}\n onReorder={(next) => setTasks(next)}\n getItemLabel={(t) => t.title}\n renderItem={(t) => <div className=\"rounded-md border p-3\">{t.title}</div>}\n/>\n```\n\n### Drag handle + buttons\n\n```jsx\n<Sortable\n handle\n aria-label=\"Backlog\"\n items={tasks}\n onReorder={(next, { from, to }) => saveOrder(next)}\n renderItem={(t, { handleProps, moveUp, moveDown }) => (\n <div className=\"flex items-center gap-2\">\n <span {...handleProps} aria-hidden=\"true\"><GripVertical /></span>\n <span className=\"flex-1\">{t.title}</span>\n <Button size=\"sm\" variant=\"ghost\" onClick={moveUp} aria-label={`Move ${t.title} up`}>↑</Button>\n <Button size=\"sm\" variant=\"ghost\" onClick={moveDown} aria-label={`Move ${t.title} down`}>↓</Button>\n </div>\n )}\n/>\n```\n\n## Accessibility & keyboard\n\n| Key (focus on an item) | Action |\n|------------------------|--------|\n| `Space` / `Enter` | Pick the item up; press again to drop it |\n| `ArrowUp` / `ArrowDown` (vertical) or `ArrowLeft` / `ArrowRight` (horizontal) | Move the carried item |\n| `Escape` | Cancel and restore the original order |\n\nEvery item is focusable (`tabIndex=0`), has `aria-roledescription=\"sortable item\"` and is described by visually hidden instructions. Each pick-up, move, drop and cancel is announced in an `aria-live=\"assertive\"` status region (\"Bravo moved to position 1 of 3.\"). Keys pressed inside interactive children (buttons, inputs) are ignored, and focus stays on the carried item. Moving focus out of the list while carrying cancels the move. Pointer drags start after a 4px threshold so clicks inside items still work.\n\n## Touch & scrolling\n\n- Items without a handle do **not** set `touch-action: none`, so swiping over the list scrolls the page as usual.\n- On touch (`pointerType === \"touch\"`), a whole-item drag starts after a **250 ms long-press**. If the finger moves more than **8 px** before that, it is treated as a scroll and no drag starts. Once the drag is live, native scrolling is suppressed until the finger lifts. Mouse and pen keep the 4 px threshold.\n- While dragging within **40 px** of the edge of the nearest scrollable ancestor (or the viewport when there is none), that container auto-scrolls along the list's axis, faster closer to the edge.\n- Keyboard reordering is unchanged.\n\n[← Component overview](README.md)\n",
|
|
5387
6518
|
"Sparkline": "# Sparkline\n\nA word-sized trend line (or area) with no axes, grid or legend — for stat cards and table cells. Still accessible: it is a named `role=\"img\"` with a hidden data table. Wraps `recharts` (optional peer, `/charts` entry point).\n\n## Install\n\nCharts live on a separate entry point so apps that don't use them never install or bundle `recharts`:\n\n```bash\nnpm install recharts\n# React 18 apps: also pin react-is to your React major (recharts resolves it as a peer)\nnpm install react-is@18\n```\n\n## Import\n\n```jsx\nimport { Sparkline } from '@dreamtree-org/twreact-ui/charts';\n```\n\nNever import charts from the root `@dreamtree-org/twreact-ui` — they are not exported there.\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `data` | `object[]` | — | **Required.** Rows to plot. An empty array renders `emptyState`. |\n| `category` | `string` | `\"value\"` | Key of the plotted value in each row. |\n| `index` | `string` | — | Optional key of a label per row (tooltip label and hidden-table row header). |\n| `color` | `ChartColor` | `\"primary\"` | Line colour token (`\"success\"`, `\"error-400\"`, …). |\n| `type` | `\"line\"` or `\"area\"` | `\"line\"` | Draw a line or a filled area. |\n| `valueFormatter` | `(value: number) => string` | `toLocaleString` | Formats values in the tooltip, value axis and hidden data table. |\n| `height` | `number` or `string` | `40` | Chart height (px number or any CSS length). The width is always 100% of the parent (responsive). |\n| `title` | `string` | — | Accessible name: `aria-label` of the `role=\"img\"` region and caption of the hidden table. Falls back to `aria-label`, then `\"Sparkline\"`. |\n| `description` | `string` | — | Longer text alternative (the takeaway), linked with `aria-describedby`. |\n| `emptyState` | `ReactNode` | `\"No data\"` | Shown (as `role=\"status\"`) instead of the chart when `data` is empty. |\n| `accessibleTable` | `boolean` | `true` | Repeat the numbers in a visually-hidden `<table>` for screen-reader users. |\n| `animate` | `boolean` | `false` | Animate on mount / data change. Always off when the user prefers reduced motion. |\n| `showTooltip` | `boolean` | `false` | Hover tooltip (themed, dark-mode aware). |\n| `className` | `string` | — | Extra classes for the root `<div>` (merged with `cn`). |\n| `ref` | `Ref<HTMLDivElement>` | — | Forwarded to the root `<div>`. |\n| ...rest | `HTMLAttributes<HTMLDivElement>` | — | Forwarded to the root `<div>` (`id`, `data-*`, `aria-*`, handlers). |\n\n## Examples\n\n### In a stat card\n\n```jsx\nconst weekly = [{ week: 'W1', signups: 12 }, { week: 'W2', signups: 18 }, { week: 'W3', signups: 15 }, { week: 'W4', signups: 27 }];\n\n<Sparkline data={weekly} index=\"week\" category=\"signups\" color=\"success\" title=\"Signups, last 4 weeks\" />\n```\n\n### Area with tooltip\n\n```jsx\n<Sparkline data={weekly} index=\"week\" category=\"signups\" type=\"area\" showTooltip height={56} />\n```\n\n## Accessibility\n\n| Behavior | Detail |\n|----------|--------|\n| Name | The SVG sits in a `role=\"img\"` region named by `title` (falls back to `aria-label`, then a generic name such as \"Sparkline\") |\n| Description | `description` is rendered visually hidden and linked with `aria-describedby` |\n| Numbers | `accessibleTable` (default on) repeats the data in a visually-hidden `<table>` with a caption, column headers and row headers, formatted with `valueFormatter`; recharts' SVG itself has no accessible content |\n| Legend | Real DOM text, not SVG |\n| Motion | Animation is disabled when `prefers-reduced-motion: reduce` is set (and with `animate={false}`) |\n| Colour | Series colours come from the design tokens; pair the chart with the table / description so colour is never the only signal |\n| Dark mode | Axis text (`--dt-muted`), grid (`--dt-line`) and series tokens are re-read when the `dark` class or `data-theme` on `<html>` changes; tooltip and legend use `dark:` variants |\n| Empty | An empty `data` array renders `emptyState` in a `role=\"status\"` box with the same accessible name |\n\n## Server rendering\n\nSafe to render on the server (a Next.js `\"use client\"` page). The first client render matches the server's HTML: token colours stay `rgb(var(--dt-*))` and reduced motion reads as off until hydration finishes, then the live values apply. SVG ids come from `React.useId()` (`dt-chart-…`), not a module counter.\n\n[← Component overview](README.md)\n",
|
|
5388
|
-
"SpeechToText": "# SpeechToText\n\nVoice-to-text using the Web Speech API. Headless by default — render a custom\ncontrol via `renderButton`, or drive it imperatively through the ref. The\nbuilt-in fallback button is `className`-extensible and forwards arbitrary props.\n\n## Import\n\n```jsx\nimport { SpeechToText } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `lang` | `string` | `\"en-US\"` | Recognition language |\n| `continuous` | `boolean` | `true` | Keep listening until stopped (forced `false` on mobile) |\n| `interimResults` | `boolean` | `true` | Return interim results |\n| `onSpeechComplete` | `function` | — | Final transcript chunk callback `(text) => void` |\n| `onSpeaking` | `function` | — | Interim transcript callback `(text) => void` |\n| `onError` | `function` | — | Fatal error callback `(Error) => void`. Routine events (`no-speech`, `aborted`) are **not** surfaced |\n| `onStart` | `function` | — | Start callback |\n| `onStop` | `function` | — | Stop callback |\n| `renderButton` | `function` | — | Custom control render-prop (see signature below) |\n| `autoStart` | `boolean` | `false` | Start on mount (once, after support is detected) |\n| `disabled` | `boolean` | `false` | Disable |\n| `resetOnStart` | `boolean` | `false` | Clear the accumulated transcript when a new session starts |\n| `className` | `string` | — | Merged (via `cn`) onto the default button |\n| `...rest` | — | — | Forwarded to the default `<button>` |\n\nThe default button is keyboard-accessible (`aria-pressed`, `aria-label`) and\ntoken-driven. When `renderButton` is supplied, `className`/`...rest` are not\napplied (you own the rendered control).\n\n## `renderButton` signature\n\n```js\nrenderButton({ isListening, isSupported, error, start, stop, toggle, disabled })\n```\n\n## Ref methods\n\n`start()`, `stop()`, `toggle()`, `isListening()`, `isSupported()`,\n`getTranscript()`, `clearTranscript()`, `getError()`.\n\n## Example\n\n```jsx\nconst [transcript, setTranscript] = useState('');\n<SpeechToText\n onSpeechComplete={(chunk) => setTranscript((t) => `${t} ${chunk}`.trim())}\n onError={(e) => console.error(e)}\n/>\n<div>Transcript: {transcript}</div>\n```\n\n[← Component overview](README.md)\n",
|
|
5389
|
-
"StatCard": "# StatCard\n\nPage-level block: a KPI tile with an icon, a label, a big tabular number and an optional change vs a previous period. Extracted from PracticePro `components/ui/StatCard.tsx`.\n\n## Import\n\n```jsx\nimport { StatCard } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `label` | `ReactNode` | — | What the number counts |\n| `value` | `ReactNode` | — | The figure |\n| `icon` | `ReactNode` | — | Lucide icon shown in a tinted chip |\n| `tone` | `primary` \\| `secondary` \\| `success` \\| `warning` \\| `error` \\| `info` | `\"primary\"` | Chip colour |\n| `change` | `{ value: number; period?: string; unit?: '%' \\| ''; goodWhenUp?: boolean }` | — | Change indicator; `goodWhenUp: false` makes a rise red. Drawn in `success-700` / `error-700` (at least 4.5:1 on white), `-400` in dark mode |\n| `loading` | `boolean` | `false` | Shows a skeleton instead of the value |\n| `className` | `string` | — | Extra CSS classes |\n\n## Example\n\n```jsx\n<StatCard label=\"Due this week\" value={18} tone=\"primary\" icon={<CalendarDays className=\"h-6 w-6\" />} />\n<StatCard label=\"Overdue\" value={3} tone=\"error\" change={{ value: 2, unit: '', period: 'vs last week', goodWhenUp: false }} />\n```\n\n[← Component overview](README.md)\n",
|
|
5390
|
-
"StatusPill": "# StatusPill\n\nPage-level block: one status badge for every domain state. Map each state to a tone and a human label once; unknown states are humanized (`in_review` → \"In review\"), never shown raw. Extracted from billpro `components/StatusPill.jsx`.\n\n## Import\n\n```jsx\nimport { StatusPill } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `status` | `string` | — | Domain state to look up in `map` |\n| `map` | `Record<string, { label; tone; icon? }>` | — | State → label and tone (`neutral`, `info`, `success`, `warning`, `danger`, `muted`). `info` renders with the `info` palette (Badge `variant=\"info\"`), not the brand accent |\n| `tone` | `string` | `\"neutral\"` | Tone when not using `map`; also accepts semantic tones |\n| `icon` | `ReactNode` | — | Small leading icon |\n| `children` | `ReactNode` | — | Label override |\n| `className` | `string` | — | Extra CSS classes |\n\n## Example\n\n```jsx\nconst OBLIGATION_STATUS = {\n overdue: { label: 'Overdue', tone: 'danger' },\n docs_missing: { label: 'Missing documents', tone: 'warning' },\n ready: { label: 'Ready for review', tone: 'info' },\n filed: { label: 'Filed', tone: 'success' },\n};\n\n<StatusPill status=\"docs_missing\" map={OBLIGATION_STATUS} />\n<StatusPill tone=\"success\">Filed</StatusPill>\n```\n\n[← Component overview](README.md)\n",
|
|
5391
|
-
"Stepper": "# Stepper\n\nHorizontal or vertical step indicator. Each step has circle (with check for completed), optional line, and label. Optional `onStepClick` for clickable steps.\n\n## Import\n\n```jsx\nimport { Stepper } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `steps` | `Array` | `[]` | Each step: `{ id?, title?, label?, description? }`. `title` is the step's text; `label` is accepted as an alias (when both are set, `title` wins). |\n| `currentStep` | `number` | `0` | Zero-based index of current step. |\n| `orientation` | `\"horizontal\"` \\| `\"vertical\"` | `\"horizontal\"` | Layout. |\n| `variant` | `
|
|
5392
|
-
"
|
|
5393
|
-
"Table": "# Table\n\nData table with sorting, filtering, pagination, row selection, expandable details, and responsive mobile card view.\n\n## Import\n\n```jsx\nimport { Table } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `data` | `Array` | `[]` | Row data (array of objects) |\n| `columns` | `Array` | `[]` | Column definitions `[{ key, label, sortable?, isVisible?, editable?, type?, options?, required?, validate?, onRender?, render?, searchable?, wrap?, hideOnMobile? }]`. `searchable: false` excludes the column from the global search, `wrap: true` lets its cells wrap, `hideOnMobile: true` drops it from the mobile card view. |\n| `interactive` | `boolean` | `false` | Enable interactive mode with inline cell editing (double-click cell), keyboard shortcuts (`Tab`/`Shift+Tab` cell navigation, `Enter` to save modified changes, `Arrows`, `Escape`), primary Save button, outline Clear Changes button, constraint validation before save, and instant new row addition. |\n| `onSave` | `function` | — | Save callback in interactive mode `({ updatedRows, newRows, allData, tableData }) => Promise<allData | { data: allData } | void> | void`. Returning a dataset array or `{ data }`/`{ allData }` updates internal state with returned records. |\n| `onEdit` | `function` | — | Live edit callback fired when a cell value changes or row edit action is clicked `({ rowKey, colKey, newValue, row, action, isNew }) => void` |\n| `sortable` | `boolean` | `true` | Enable column sort |\n| `filterable` | `boolean` | `false` | Show a filter input per column. Client-side, each non-empty filter keeps rows whose value in that column contains the text (case-insensitive); filters combine with each other and with the global search. In `serverSide` mode nothing is filtered locally. |\n| `selectable` | `boolean` | `false` | Row selection checkboxes |\n| `pagination` | `boolean` | `false` | Enable pagination |\n| `pageSize` | `number` | `25` | Rows per page |\n| `onSort` | `function` | — | Sort callback |\n| `onFilter` | `function` | — | Filter callback |\n| `onFetch` | `function` | — | Server-side fetch (setData, setLoading, filters, page, limit, sort) |\n| `onFilterChange` | `function` | — | Filter change callback |\n| `onSelectionChange` | `function` | — | Selected rows callback |\n| `onRowClick` | `function` | — | Row click callback |\n| `hasDetails` | `boolean` or `(row, index) => boolean` | `false` | Expandable row details. Pass a predicate to give only matching rows an expand toggle; non-matching rows render an empty placeholder cell so columns stay aligned. |\n| `DetailsComponent` | `Component` | — | Component for expanded content |\n| `withAction` | `boolean` | `true` | Show actions column |\n| `onAction` | `function` | — | Action menu callback |\n| `actions` | `Array` or `(row, index) => Array` | — | Row actions. Entries are `{ name, label, icon, render?, onClick?, hidden?, disabled?, inline? }`; `label`, `icon`, `hidden` and `disabled` each accept a value or `(row, index) => value`. `inline: true` renders the action as an icon button in the actions cell instead of inside the kebab menu; the kebab is hidden when no menu actions remain. Entries are merged onto the built-in Edit/Delete/View set by `name` (so `{ name: 'edit', onClick }` keeps the default label and icon, and `{ name: 'delete', hidden: true }` removes a default); unmatched entries are appended. Set `replaceDefaultActions` to opt out. |\n| `replaceDefaultActions` | `boolean` | `false` | Replace the built-in Edit/Delete/View actions entirely instead of patching them by `name`. |\n| `showSerial` | `boolean` | `true` | Show row number column |\n| `cellClass` | `string` \\| `function` | — | Cell className |\n| `rowClass` | `string` \\| `function` | — | Row className |\n| `globalSearch` | `boolean` | `false` | Global search box. Works with or without `filterable`. Client-side, keeps rows where any value outside a `searchable: false` column contains the term (case-insensitive). In `serverSide` mode it only reports `filters.global`. |\n| `limitOptions` | `number[]` | `[10,25,50,100]` | Page size options |\n| `showLimitSelector` | `boolean` | `true` | Show page size selector |\n| `showReloadButton` | `boolean` | `true` | Show reload button. It is named \"Reload\" (`aria-label`) because its text label is hidden below the `sm` breakpoint |\n| `onReload` | `function` | — | Reload click handler |\n| `stripedRows` | `boolean` | `true` | Striped row background |\n| `theme` | `object` | see below | `stripedColors`, `rowHover`, `accentColor`, `menuBg`, `menuItemHover`. Defaults are dark-mode aware: `{ stripedColors: ['bg-surface', 'bg-background'], rowHover: 'bg-background', accentColor: 'bg-background', menuBg: 'bg-surface', menuItemHover: 'hover:bg-background' }`. A `stripedColors` entry may be a Tailwind `bg-*` class (applied as a class) or a CSS colour such as `'#f7fafc'` (applied as an inline background). `rowHover` is a `bg-*` token or a full hover class string. In the pagination controls, the shipped tokens (`bg-background`, `bg-surface`, `bg-gray-50`, `bg-gray-100`, `bg-primary-50`, `bg-primary-100`) map to static `hover:` literals; for any other colour pass the full class (e.g. `'hover:bg-gray-200'`) so your Tailwind build generates it — a bare unknown token is still prefixed with `hover:` at runtime for backward compatibility, but Tailwind can purge such a class. Pagination buttons grow to a 44×44 px touch target on coarse pointers. |\n| `serverSide` | `boolean` | `false` | Server-side pagination/sort/filter |\n| `totalRecords` | `number` | `0` | Total count (serverSide). Drives the page count and the \"start - end of total\" counter; when omitted the counter falls back to the rows received. |\n| `pageNumber` | `number` | — | Controlled page. Client-side, a page past the last one is clamped to the last page once rows exist. |\n| `onPageChange` | `(page: number) => void` | — | Called with the new page number when the user changes page, and when the table clamps a page that no longer exists (client-side). Not called for the reset to page 1 on a filter or page-size change. Consumed by the table — it is **not** forwarded onto the `<table>`. |\n| `responsiveBreakpoint` | `number` | `768` | px breakpoint for mobile cards |\n| `showToolbar` | `boolean` | `true` | Show the toolbar above the table (reload, page size, result count, pager header) and the column picker. `false` hides all of it; the pager footer stays, and in `interactive` mode the add-row / Save / Clear controls still render. `globalSearch` is its own opt-in and is unaffected. |\n| `showColumnPicker` | `boolean` | `true` | Show the column-picker button in the header (accessible name \"Choose columns\", with `aria-expanded`). `false` also removes its (otherwise empty) column from every row. |\n| `wrapCells` | `boolean` | `false` | Let cell text wrap instead of staying on one line. A column's own `wrap` overrides it. |\n| `mobileMaxFields` | `number` | — | Cap the fields shown on each mobile card. Applied in column order, after `hideOnMobile` columns are removed. |\n\nOnly DOM-valid attributes in `...rest` (e.g. `id`, `aria-*`, `data-*`) are\nforwarded to the root `<table>`; the table's own control props (`onPageChange`,\n`serverSide`, `pagination`, the `on*` callbacks, …) are consumed and never leak\nto the DOM as unknown attributes.\n\n## Examples\n\n### Basic table\n\n```jsx\nconst columns = [\n { key: 'name', label: 'Name' },\n { key: 'email', label: 'Email' },\n];\nconst data = [\n { id: 1, name: 'Alice', email: 'alice@example.com' },\n { id: 2, name: 'Bob', email: 'bob@example.com' },\n];\n<Table data={data} columns={columns} />\n```\n\n### With pagination and selection\n\n```jsx\n<Table\n data={data}\n columns={columns}\n pagination\n pageSize={10}\n selectable\n onSelectionChange={(selected) => console.log(selected)}\n/>\n```\n\n### Server-side data\n\n```jsx\n<Table\n data={data}\n columns={columns}\n serverSide\n pagination\n totalRecords={total}\n onFetch={async ({ setData, setLoading, page, limit, sort }) => {\n setLoading(true);\n const res = await fetch(`/api/users?page=${page}&limit=${limit}`);\n const json = await res.json();\n setData(json.rows);\n setLoading(false);\n }}\n/>\n```\n\n### Filtering and page clamping (client-side)\n\nWithout `serverSide`, the table filters the rows itself:\n\n- **Global search** (`globalSearch`) applies on Enter or the search button,\n with or without `filterable`. Columns with `searchable: false` are skipped.\n- **Column filters** (`filterable`) apply as you type. Each one matches only its\n own column's raw value (not a `render` output).\n- All matches are case-insensitive \"contains\" tests, and every active filter\n must match (AND).\n- `onFilter` / `onFilterChange` still receive the whole `filters` object\n (`{ global?, [columnKey]: text }`) on every change.\n\nWhen the rows shrink (filtering, deleted rows, a new `data` prop) and the\ncurrent page no longer exists, the table shows the last page that does and\ncalls `onPageChange` with it. A parent that drives `pageNumber` can simply\nstore that value, and no `clampPage` helper is needed. The clamp waits while\nthere are no rows, so a page restored from the URL survives until the data\narrives.\n\nWith `serverSide`, none of this runs locally: filters go to `onFetch` /\n`onFilter` / `onFilterChange`, the rows shown are exactly the rows you pass,\nand keeping `pageNumber` within `totalRecords` is up to you.\n\n### Compact, read-only table\n\nHide the chrome for a dashboard card, keep id columns out of the search, let\nlong text wrap, and keep phone cards short.\n\n```jsx\nconst columns = [\n { key: 'id', label: 'ID', searchable: false, hideOnMobile: true },\n { key: 'client', label: 'Client' },\n { key: 'status', label: 'Status' },\n { key: 'notes', label: 'Notes', wrap: true },\n { key: 'owner', label: 'Owner' },\n];\n\n<Table\n data={rows}\n columns={columns}\n showToolbar={false}\n showSerial={false}\n withAction={false}\n mobileMaxFields={3}\n/>\n```\n\nOn a phone each card shows Client, Status and Notes. With no serial, selection,\nexpand toggle or actions, the card has no header strip either.\n\n`showToolbar={false}` does not change pagination: with `pagination` on, the pager\nfooter still renders. Hide only the picker with `showColumnPicker={false}`.\n\n### Conditional row details\n\n`hasDetails` accepts a predicate so only rows that actually have something to\nshow get an expand toggle. Rows that don't qualify still render the (empty)\ntoggle cell, so the columns stay aligned.\n\n```jsx\nconst ErrorDetails = ({ row }) => <pre>{row.stackTrace}</pre>;\n\n<Table\n data={jobs}\n columns={columns}\n hasDetails={(row) => row.status === 'failed'}\n DetailsComponent={ErrorDetails}\n/>\n```\n\n### Conditional and inline actions\n\n`hidden` and `disabled` accept a `(row, index)` predicate, `label` and `icon`\naccept a `(row, index)` producer, and `inline: true` promotes an action out of\nthe kebab menu into the row itself. When no menu actions remain for a row, the\nkebab button is not rendered at all.\n\n```jsx\n<Table\n data={jobs}\n columns={columns}\n replaceDefaultActions\n actions={[\n { name: 'view', label: 'View', icon: <Eye size={16} />, inline: true },\n { name: 'edit', label: 'Edit', icon: <Edit size={16} />,\n disabled: (row) => row.locked },\n { name: 'retry', label: (row) => `Retry (${row.tries})`,\n hidden: (row) => row.status !== 'failed' },\n ]}\n onAction={({ action, row }) => handle(action.name, row)}\n/>\n```\n\nFor a large table, wrap a function-valued `actions` in `useCallback` — a fresh\narrow on every render defeats the per-row memoization.\n\n```jsx\nconst actions = useCallback(\n (row) => (row.role === 'admin' ? adminActions : userActions),\n []\n);\n```\n\nNote that because `icon` may be a `(row, index) => node` producer, passing a\nbare component reference (`icon: Edit`) calls it rather than rendering it. Pass\nan element instead: `icon: <Edit size={16} />`.\n\n### Patching the default actions\n\nEntries you supply are merged onto the built-in Edit/Delete/View set by `name`,\nso you can swap one handler without restating the rest. Unmatched names are\nappended after the defaults, and `hidden: true` removes a default outright.\n\n```jsx\n<Table\n data={users}\n columns={columns}\n actions={[\n { name: 'edit', onClick: ({ row }) => edit(row) }, // keeps default label + icon\n { name: 'delete', hidden: (row) => !row.canDelete },\n { name: 'invite', label: 'Invite', icon: <Mail size={16} /> }, // appended\n ]}\n/>\n```\n\nPass `replaceDefaultActions` to skip merging and use only your own set:\n\n```jsx\n<Table\n data={users}\n columns={columns}\n replaceDefaultActions\n actions={[{ name: 'approve', label: 'Approve' }]}\n/>\n```\n\n### Interactive Table Mode\n\nPass `interactive={true}` to enable inline cell editing (double-click a cell or press standard `'e'` key on a focused active cell to enter edit mode), instant record addition (at top or bottom), keyboard navigation (`Tab`/`Shift+Tab` to move between cells, `Escape` to exit edit mode, `Enter` to save modified changes), and constraint validation before saving.\n\n#### Interactive Props & Options\n\n- `interactive`: `boolean` (default: `false`) — Enables interactive mode.\n- `editKeyBinding`: `string | ((e) => boolean)` (default: `\"e\"`) — Standard key binding to enter cell edit mode on active focused cell.\n- `newRowPosition`: `\"bottom\" | \"top\"` (default: `\"bottom\"`) — Position where empty new rows are inserted (at the bottom or top of table dataset).\n- `addRowRef`: `React.MutableRefObject` — External ref populated with `(position?: \"top\" | \"bottom\") => void` to trigger adding a new row from external buttons.\n- `onInitAddRow`: `(addRowFn: (position?: \"top\" | \"bottom\") => void) => void` — Callback receiving the add-row function on component initialization.\n- `renderAddRowButton`: `({ addRow, newRowPosition }) => ReactNode` — Render prop for rendering a custom external add-row button in the table toolbar.\n\n#### Interactive Usage Example\n\n```jsx\nconst columns = [\n { key: 'name', label: 'Name', type: 'text', required: true },\n { key: 'role', label: 'Role', type: 'select', options: ['Admin', 'User'], required: true },\n { key: 'notes', label: 'Notes', type: 'textarea' },\n];\n\nconst addRowRef = useRef();\n\nreturn (\n <div>\n <button onClick={() => addRowRef.current?.(\"top\")}>\n Add Row at Top\n </button>\n\n <Table\n interactive\n editKeyBinding=\"e\"\n newRowPosition=\"top\"\n addRowRef={addRowRef}\n columns={columns}\n data={users}\n onSave={async ({ updatedRows, newRows, allData }) => {\n await api.saveUsers({ updatedRows, newRows });\n }}\n />\n </div>\n);\n```\n\n[← Component overview](README.md)\n",
|
|
5394
|
-
"Tabs": "# Tabs\n\nTabbed content with list and panels. Use subcomponents: `Tabs.List`, `Tabs.Tab`, `Tabs.Panels`, `Tabs.Panel`. Supports controlled (`index` + `onChange`) or uncontrolled (`defaultIndex`), keyboard navigation, and variants.\n\n**Accessibility and SSR:** tab and panel ids come from `React.useId()`, so server-rendered markup hydrates without mismatches and two Tabs on one page never collide. Each tab's `aria-controls` and each panel's `aria-labelledby` point at elements that exist. Panels mount lazily, so only the selected tab carries `aria-controls`.\n\n## Import\n\n```jsx\nimport { Tabs } from '@dreamtree-org/twreact-ui';\n```\n\n## Props (Tabs root)\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Must include `Tabs.List` and `Tabs.Panels`. |\n| `defaultIndex` | `number` | `0` | Uncontrolled initial active tab index. |\n| `index` | `number` | — | Controlled active index. |\n| `onChange` | `(index: number) => void` | — | Called when tab changes. |\n| `orientation` | `\"horizontal\"` \\| `\"vertical\"` | `\"horizontal\"` | Layout direction. |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Tab size. Other `Size` values (`xs`, `xl`, `2xl`) fall back to `md`. |\n| `variant` | `\"line\"` \\| `\"pills\"` \\| `\"unstyled\"` | `\"line\"` | Tab style. |\n| `animated` | `boolean` | `true` | Fade-in (`animate-fade-in`) on the newly active panel; skipped under `prefers-reduced-motion`. |\n| `className` | `string` | `\"\"` | Wrapper CSS classes. |\n\n## Usage\n\n```jsx\n<Tabs defaultIndex={0} variant=\"pills\" size=\"md\">\n <Tabs.List>\n <Tabs.Tab>Home</Tabs.Tab>\n <Tabs.Tab>Profile</Tabs.Tab>\n <Tabs.Tab>Settings</Tabs.Tab>\n </Tabs.List>\n <Tabs.Panels>\n <Tabs.Panel>Home content</Tabs.Panel>\n <Tabs.Panel>Profile content</Tabs.Panel>\n <Tabs.Panel>Settings content</Tabs.Panel>\n </Tabs.Panels>\n</Tabs>\n```\n\nControlled:\n\n```jsx\nconst [index, setIndex] = useState(0);\n<Tabs index={index} onChange={setIndex}>\n ...\n</Tabs>\n```\n\n[← Component overview](README.md)\n",
|
|
6519
|
+
"SpeechToText": "# SpeechToText\n\nVoice-to-text using the Web Speech API. Headless by default — render a custom\ncontrol via `renderButton`, or drive it imperatively through the ref. The\nbuilt-in fallback button is `className`-extensible and forwards arbitrary props.\n\n## Import\n\n```jsx\nimport { SpeechToText } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `lang` | `string` | `\"en-US\"` | Recognition language |\n| `continuous` | `boolean` | `true` | Keep listening until stopped (forced `false` on mobile) |\n| `interimResults` | `boolean` | `true` | Return interim results |\n| `onSpeechComplete` | `function` | — | Final transcript chunk callback `(text) => void` |\n| `onSpeaking` | `function` | — | Interim transcript callback `(text) => void` |\n| `onError` | `function` | — | Fatal error callback `(Error) => void`. Routine events (`no-speech`, `aborted`) are **not** surfaced |\n| `onStart` | `function` | — | Start callback |\n| `onStop` | `function` | — | Stop callback |\n| `renderButton` | `function` | — | Custom control render-prop (see signature below) |\n| `autoStart` | `boolean` | `false` | Start on mount (once, after support is detected) |\n| `disabled` | `boolean` | `false` | Disable |\n| `resetOnStart` | `boolean` | `false` | Clear the accumulated transcript when a new session starts |\n| `className` | `string` | — | Merged (via `cn`) onto the default button |\n| `...rest` | — | — | Forwarded to the default `<button>` |\n\nThe default button is keyboard-accessible (`aria-pressed`, `aria-label`) and\ntoken-driven. When `renderButton` is supplied, `className`/`...rest` are not\napplied (you own the rendered control).\n\n## `renderButton` signature\n\n```js\nrenderButton({ isListening, isHearing, isSupported, error, start, stop, toggle, disabled })\n```\n\n`isHearing` is `true` while the engine reports speech (between `speechstart` and `speechend`, or once results arrive); it resets when listening stops.\n\n## Listening feedback\n\nWhile listening, the default button shows a pulsing ring behind the mic icon and three small level bars after the label. The bars rest until speech is heard, then animate. There is no audio-level API without `getUserMedia` (which this component does not request), so the bars show *whether* speech is detected, not its volume. Under `prefers-reduced-motion` the pulse becomes a static ring and the bars stop animating. The button also has a `focus-visible` ring, press feedback, a 44 px minimum height on touch screens, and dark-mode colours; it exposes `data-listening` / `data-hearing` attributes for styling.\n\n## Ref methods\n\n`start()`, `stop()`, `toggle()`, `isListening()`, `isSupported()`,\n`getTranscript()`, `clearTranscript()`, `getError()`.\n\n## Example\n\n```jsx\nconst [transcript, setTranscript] = useState('');\n<SpeechToText\n onSpeechComplete={(chunk) => setTranscript((t) => `${t} ${chunk}`.trim())}\n onError={(e) => console.error(e)}\n/>\n<div>Transcript: {transcript}</div>\n```\n\n[← Component overview](README.md)\n",
|
|
6520
|
+
"StatCard": "# StatCard\n\nPage-level block: a KPI tile with an icon, a label, a big tabular number and an optional change vs a previous period. Extracted from PracticePro `components/ui/StatCard.tsx`.\n\n## Import\n\n```jsx\nimport { StatCard } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `label` | `ReactNode` | — | What the number counts |\n| `value` | `ReactNode` | — | The figure |\n| `icon` | `ReactNode` | — | Lucide icon shown in a tinted chip |\n| `tone` | `primary` \\| `secondary` \\| `success` \\| `warning` \\| `error` \\| `info` | `\"primary\"` | Chip colour (light tint in light mode, a deep translucent tint with a lighter icon in dark mode) |\n| `change` | `{ value: number; period?: string; unit?: '%' \\| ''; goodWhenUp?: boolean }` | — | Change indicator; `goodWhenUp: false` makes a rise red. Drawn in `success-700` / `error-700` (at least 4.5:1 on white), `-400` in dark mode |\n| `loading` | `boolean` | `false` | Shows a skeleton instead of the value |\n| `className` | `string` | — | Extra CSS classes |\n\n## Example\n\n```jsx\n<StatCard label=\"Due this week\" value={18} tone=\"primary\" icon={<CalendarDays className=\"h-6 w-6\" />} />\n<StatCard label=\"Overdue\" value={3} tone=\"error\" change={{ value: 2, unit: '', period: 'vs last week', goodWhenUp: false }} />\n```\n\n[← Component overview](README.md)\n",
|
|
6521
|
+
"StatusPill": "# StatusPill\n\nPage-level block: one status badge for every domain state. Map each state to a tone and a human label once; unknown states are humanized (`in_review` → \"In review\"), never shown raw. Extracted from billpro `components/StatusPill.jsx`.\n\n## Import\n\n```jsx\nimport { StatusPill } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `status` | `string` | — | Domain state to look up in `map` |\n| `map` | `Record<string, { label; tone; icon? }>` | — | State → label and tone (`neutral`, `info`, `success`, `warning`, `danger`, `muted`). `info` renders with the `info` palette (Badge `variant=\"info\"`), not the brand accent |\n| `tone` | `string` | `\"neutral\"` | Tone when not using `map`; also accepts semantic tones |\n| `icon` | `ReactNode` | — | Small leading icon (rendered in Badge's decorative `icon` slot) |\n| `children` | `ReactNode` | — | Label override |\n| `className` | `string` | — | Extra CSS classes |\n\nColours come from `Badge`, so every tone has dark-mode tones too.\n\n## Example\n\n```jsx\nconst OBLIGATION_STATUS = {\n overdue: { label: 'Overdue', tone: 'danger' },\n docs_missing: { label: 'Missing documents', tone: 'warning' },\n ready: { label: 'Ready for review', tone: 'info' },\n filed: { label: 'Filed', tone: 'success' },\n};\n\n<StatusPill status=\"docs_missing\" map={OBLIGATION_STATUS} />\n<StatusPill tone=\"success\">Filed</StatusPill>\n```\n\n[← Component overview](README.md)\n",
|
|
6522
|
+
"Stepper": "# Stepper\n\nHorizontal or vertical step indicator. Each step has circle (with check for completed), optional line, and label. Optional `onStepClick` for clickable steps.\n\n## Import\n\n```jsx\nimport { Stepper } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `steps` | `Array` | `[]` | Each step: `{ id?, title?, label?, description? }`. `title` is the step's text; `label` is accepted as an alias (when both are set, `title` wins). |\n| `currentStep` | `number` | `0` | Zero-based index of current step. |\n| `orientation` | `\"horizontal\"` \\| `\"vertical\"` \\| `\"auto\"` | `\"horizontal\"` | Layout. `\"auto\"` stacks vertically below the `sm` breakpoint (640 px) and lays out horizontally from `sm` up, using CSS only. |\n| `variant` | `\"default\"` \\| `\"dots\"` | `\"default\"` | `\"dots\"` renders compact progress dots: one labelled dot per step, the current step as a wider pill, and no titles or descriptions. Clicks work as with `onStepClick`. Unknown values render the default stepper. |\n| `onStepClick` | `(step, stepIndex) => void` | — | Called when a step is clicked (e.g. for completed/current). |\n| `className` | `string` | — | Nav wrapper CSS classes. |\n| ...rest | — | — | Passed to `<nav>`. The nav defaults to `aria-label=\"Progress\"`; a caller's `aria-label` replaces it. |\n\n## Step status\n\n- **completed**: `stepIndex < currentStep` (check icon, primary color).\n- **current**: `stepIndex === currentStep`.\n- **upcoming**: `stepIndex > currentStep` (gray circle; title and description use the `text-muted` role, 7.5:1 on white).\n\nConnectors flex to fill the space between circles (horizontal) or between steps (vertical), so any number of steps fits its container without overflowing. The connector after a completed step is primary. Step buttons show a `focus-visible` ring, have dark-mode colours, and grow to a 44 px touch target on coarse pointers when they are clickable.\n\nEach step button (and each dot) is named `Step <n>: <title or label>`, with ` (completed)` for completed steps (their circle shows only a check icon).\n\n## Example\n\n```jsx\n<Stepper\n steps={[\n { id: '1', label: 'Details' },\n { id: '2', label: 'Payment' },\n { id: '3', label: 'Confirm' },\n ]}\n currentStep={1}\n orientation=\"horizontal\"\n onStepClick={(step, idx) => setStep(idx)}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
6523
|
+
"SwipeableListItem": "# SwipeableListItem\n\nA list row with swipe actions, the iOS Mail / Gmail pattern. Swipe right to reveal the **leading** actions, left to reveal the **trailing** ones; release to snap open or closed, or swipe all the way to commit the first action on that side. Past the actions the row rubber-bands. Every action is also reachable without a gesture through a **More actions** button that opens them as a menu (an action sheet on phones).\n\n## Import\n\n```jsx\nimport { SwipeableListItem } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `leadingActions` | `SwipeAction[]` | `[]` | Revealed by swiping right. The first one sits at the left edge and is the full-swipe action |\n| `trailingActions` | `SwipeAction[]` | `[]` | Revealed by swiping left. The first one sits at the right edge and is the full-swipe action |\n| `fullSwipe` | `boolean` \\| `{ leading?, trailing? }` | `true` | A swipe past `fullSwipeThreshold` commits the first action of that side. Pass an object to allow it on one side only |\n| `fullSwipeThreshold` | `number` | `0.6` | Fraction of the row width a full swipe must travel (and at least 24px past the revealed actions) |\n| `actionWidth` | `number` | `80` | Width in px of each revealed action |\n| `open` | `'leading'` \\| `'trailing'` \\| `null` | — | Controlled open side |\n| `defaultOpen` | `'leading'` \\| `'trailing'` \\| `null` | `null` | Uncontrolled initial open side |\n| `onOpenChange` | `(side) => void` | — | Called with `'leading'`, `'trailing'` or `null` when the row opens or closes |\n| `disabled` | `boolean` | `false` | Turns off the gesture and the More actions button |\n| `moreActions` | `'focus'` \\| `'always'` \\| `false` | `'focus'` | The gesture-free button. `focus`: shown on keyboard focus and mouse hover. `always`: always visible. `false`: removed (only when you provide another way to reach the actions) |\n| `moreActionsLabel` | `string` | `\"More actions\"` | Accessible name of the button and its menu |\n| `menuPresentation` | `'auto'` \\| `'popover'` \\| `'sheet'` | `'auto'` | How the More actions menu opens. `auto` uses an action sheet on small touch screens |\n| `as` | `ElementType` | `\"div\"` | Root element, e.g. `\"li\"` inside a `<ul>` |\n| `contentClassName` | `string` | — | Classes for the sliding content layer (default background `bg-surface`) |\n| `className` | `string` | — | Root classes (merged with `cn`) |\n| `children` | `ReactNode` | — | The row content |\n| `...rest` | `HTMLAttributes` | — | Forwarded to the root; `ref` points at the root |\n\n### `SwipeAction`\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `key` | `string` | Unique key |\n| `label` | `ReactNode` | Visible text (also the menu item text) |\n| `icon` | `ReactNode` \\| `Component` | Shown above the label (`<Trash2 />` or `Trash2`) |\n| `tone` | `string` | `default`, `primary`, `secondary`, `success`, `warning`, `info`, `error` or `destructive`. Sets the action's background; `error` / `destructive` also mark it destructive in the menu |\n| `disabled` | `boolean` | Not selectable |\n| `onSelect` | `({ action, source }) => void` | Runs the action. `source` is `'button'` (a revealed action was tapped), `'full-swipe'` or `'menu'`. The row closes afterwards |\n\n## Behaviour\n\n- **Pointer events**, with `touch-action: pan-y` on the content: a swipe only starts once horizontal movement dominates, so vertical scrolling is never blocked.\n- **Snap**: on release the row opens when dragged past half of its actions (or flicked toward open) and closes otherwise.\n- **Full swipe**: while past the threshold the first action grows to fill the revealed area; releasing there commits it.\n- **Closing**: a tap outside the row, a tap on the open row (which does not activate the row), Escape, or selecting an action.\n- **No ghost clicks**: the click that ends a swipe is swallowed.\n- **Reduced motion**: snapping animates with the motion tokens and is instant under `prefers-reduced-motion`.\n\n## Accessibility\n\n- Actions that are not revealed are `aria-hidden` and out of the tab order; revealed actions are regular buttons.\n- The **More actions** button (`aria-haspopup=\"menu\"`) is in the tab order even when visually hidden (`moreActions=\"focus\"`) and appears on focus. It opens every action as a `role=\"menu\"` (arrow keys, Home/End, typeahead, Escape returns focus) or, on phones, as an action sheet.\n- Escape closes an open row.\n\n## Examples\n\n### Inbox row\n\n```jsx\nimport { Archive, Trash2, Flag, MailOpen } from 'lucide-react';\n\n<ul className=\"divide-y divide-line\">\n {messages.map((m) => (\n <SwipeableListItem\n key={m.id}\n as=\"li\"\n leadingActions={[{ key: 'read', label: 'Read', icon: MailOpen, tone: 'primary', onSelect: () => markRead(m) }]}\n trailingActions={[\n { key: 'delete', label: 'Delete', icon: Trash2, tone: 'destructive', onSelect: () => remove(m) },\n { key: 'flag', label: 'Flag', icon: Flag, tone: 'warning', onSelect: () => flag(m) },\n { key: 'archive', label: 'Archive', icon: Archive, tone: 'success', onSelect: () => archive(m) },\n ]}\n >\n <div className=\"px-4 py-3\">{m.subject}</div>\n </SwipeableListItem>\n ))}\n</ul>\n```\n\n### One row open at a time\n\n```jsx\nconst [open, setOpen] = useState(null); // { id, side }\n\n<SwipeableListItem\n open={open?.id === m.id ? open.side : null}\n onOpenChange={(side) => setOpen(side ? { id: m.id, side } : null)}\n trailingActions={actions}\n>\n …\n</SwipeableListItem>\n```\n\n### In a Table\n\n`Table` builds its mobile cards on this component through the `swipeActions` prop; see [Table](Table.md#mobile-cards-filters-and-swipe-actions).\n\n[← Component overview](README.md)\n",
|
|
6524
|
+
"Switch": "# Switch\n\nToggle switch (on/off). Controlled or uncontrolled. Without an `id` prop, the id comes from `React.useId()`, so it is stable across server rendering and hydration.\n\n## Import\n\n```jsx\nimport { Switch } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `checked` | `boolean` | — | Controlled checked |\n| `defaultChecked` | `boolean` | `false` | Uncontrolled default |\n| `onChange` | `(checked: boolean) => void` | — | Receives the new boolean state (not a DOM event) |\n| `disabled` | `boolean` | `false` | Disable |\n| `size` | `string` | `\"md\"` | `sm`, `md`, `lg`. `md` takes the iOS 51×31 size on coarse pointers (touch screens). |\n| `color` | `string` | `\"primary\"` | Token preset (`primary`, `secondary`, `success`, `error`, `warning`; aliases `green`, `danger`, `red`) or a custom CSS colour string. The raw-palette presets `blue`, `indigo`, `purple` were **removed in 2.0.0** ([migration](MIGRATION-2.0.md#checkbox-and-switch-raw-colour-presets)). |\n| `name` | `string` | — | Form name |\n| `topLabel` | `string` | — | Label above |\n| `bottomLabel` | `string` | — | Label below |\n| `leftLabel` | `string` | — | Label left |\n| `rightLabel` | `string` | — | Label right |\n| `haptics` | `boolean` | `false` | Opt-in: a `selection` vibration (10ms) on every toggle (tap, drag or keyboard). No-op without `navigator.vibrate` (iOS Safari) and under reduced motion. See [useHaptics](useHaptics.md) |\n\n## Behavior\n\n- **Tap or drag:** a tap/click toggles; dragging the knob left or right (pointer events) sets off / on, iOS style. A drag toward the current state is a no-op, and a cancelled drag reverts. `Space` / `Enter` toggle from the keyboard.\n- The off track is a visible grey in light and dark mode; colour presets (`primary`, `success`, …) paint the on track.\n- Transitions are `transition-colors` / `transition-transform` and turn off under `prefers-reduced-motion`. Focus shows a `focus-visible` ring.\n\n## Example\n\n```jsx\n<Switch checked={enabled} onChange={checked => setEnabled(checked)} />\n<Switch defaultChecked rightLabel=\"Enable notifications\" />\n```\n\n[← Component overview](README.md)\n",
|
|
6525
|
+
"Table": "# Table\n\nData table with sorting, filtering, pagination, row selection, expandable details, and responsive mobile card view.\n\n## Import\n\n```jsx\nimport { Table } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `data` | `Array` | `[]` | Row data (array of objects) |\n| `columns` | `Array` | `[]` | Column definitions `[{ key, label, sortable?, isVisible?, editable?, type?, options?, required?, validate?, onRender?, render?, searchable?, wrap?, hideOnMobile? }]`. `searchable: false` excludes the column from the global search, `wrap: true` lets its cells wrap, `hideOnMobile: true` drops it from the mobile card view. |\n| `interactive` | `boolean` | `false` | Enable interactive mode with inline cell editing (double-click cell), keyboard shortcuts (`Tab`/`Shift+Tab` cell navigation, `Enter` to save modified changes, `Arrows`, `Escape`), primary Save button, outline Clear Changes button, constraint validation before save, and instant new row addition. |\n| `onSave` | `function` | — | Save callback in interactive mode `({ updatedRows, newRows, allData, tableData }) => Promise<allData | { data: allData } | void> | void`. Returning a dataset array or `{ data }`/`{ allData }` updates internal state with returned records. |\n| `onEdit` | `function` | — | Live edit callback fired when a cell value changes or row edit action is clicked `({ rowKey, colKey, newValue, row, action, isNew }) => void` |\n| `sortable` | `boolean` | `true` | Enable column sort. Each sortable header is a real `<button>` inside its `<th>` (Tab to it, Enter / Space to sort) and the `<th>` carries `aria-sort` (`none`, `ascending` or `descending`). |\n| `filterable` | `boolean` | `false` | Show a filter input per column. Client-side, each non-empty filter keeps rows whose value in that column contains the text (case-insensitive); filters combine with each other and with the global search. In `serverSide` mode nothing is filtered locally. |\n| `selectable` | `boolean` | `false` | Row selection checkboxes |\n| `pagination` | `boolean` | `false` | Enable pagination |\n| `pageSize` | `number` | `25` | Rows per page |\n| `onSort` | `function` | — | Sort callback |\n| `onFilter` | `function` | — | Filter callback |\n| `onFetch` | `function` | — | Server-side fetch (setData, setLoading, filters, page, limit, sort) |\n| `onFilterChange` | `function` | — | Filter change callback |\n| `onSelectionChange` | `function` | — | Selected rows callback |\n| `onRowClick` | `function` | — | Row click callback |\n| `hasDetails` | `boolean` or `(row, index) => boolean` | `false` | Expandable row details. Pass a predicate to give only matching rows an expand toggle; non-matching rows render an empty placeholder cell so columns stay aligned. |\n| `DetailsComponent` | `Component` | — | Component for expanded content |\n| `withAction` | `boolean` | `true` | Show actions column |\n| `onAction` | `function` | — | Action menu callback |\n| `actions` | `Array` or `(row, index) => Array` | — | Row actions. Entries are `{ name, label, icon, render?, onClick?, hidden?, disabled?, inline?, tone? }`; `tone: 'destructive'` (or `destructive: true`) styles a menu action in the error tone; `label`, `icon`, `hidden` and `disabled` each accept a value or `(row, index) => value`. `inline: true` renders the action as an icon button in the actions cell instead of inside the kebab menu; the kebab is hidden when no menu actions remain. Entries are merged onto the built-in Edit/Delete/View set by `name` (so `{ name: 'edit', onClick }` keeps the default label and icon, and `{ name: 'delete', hidden: true }` removes a default); unmatched entries are appended. Set `replaceDefaultActions` to opt out. |\n| `actionMenuPresentation` | `string` | `\"auto\"` | One of `auto`, `popover`, `sheet`. How the row kebab menu opens. `popover`: a floating menu portaled to `document.body` that flips/shifts to stay on screen (never clipped by the table's overflow). `sheet`: an iOS-style action sheet from the bottom with a separate Cancel button. `auto` uses the sheet on small touch screens (`(pointer: coarse) and (max-width: 639px)`) and the popover elsewhere. |\n| `replaceDefaultActions` | `boolean` | `false` | Replace the built-in Edit/Delete/View actions entirely instead of patching them by `name`. |\n| `showSerial` | `boolean` | `true` | Show row number column |\n| `cellClass` | `string` \\| `function` | — | Cell className |\n| `rowClass` | `string` \\| `function` | — | Row className |\n| `globalSearch` | `boolean` | `false` | Global search box. Works with or without `filterable`. Client-side, keeps rows where any value outside a `searchable: false` column contains the term (case-insensitive). In `serverSide` mode it only reports `filters.global`. |\n| `limitOptions` | `number[]` | `[10,25,50,100]` | Page size options |\n| `showLimitSelector` | `boolean` | `true` | Show page size selector |\n| `showReloadButton` | `boolean` | `true` | Show reload button. It is named \"Reload\" (`aria-label`) because its text label is hidden below the `sm` breakpoint |\n| `onReload` | `function` | — | Reload click handler |\n| `stripedRows` | `boolean` | `true` | Striped row background |\n| `theme` | `object` | see below | `stripedColors`, `rowHover`, `accentColor`, `menuBg`, `menuItemHover`. Defaults are dark-mode aware: `{ stripedColors: ['bg-surface', 'bg-background'], rowHover: 'bg-background', accentColor: 'bg-background', menuBg: 'bg-surface', menuItemHover: 'hover:bg-background' }`. A `stripedColors` entry may be a Tailwind `bg-*` class (applied as a class) or a CSS colour such as `'#f7fafc'` (applied as an inline background). `rowHover` is a `bg-*` token or a full hover class string. In the pagination controls, the shipped tokens (`bg-background`, `bg-surface`, `bg-gray-50`, `bg-gray-100`, `bg-primary-50`, `bg-primary-100`) map to static `hover:` literals; for any other colour pass the full class (e.g. `'hover:bg-gray-200'`) so your Tailwind build generates it — a bare unknown token is still prefixed with `hover:` at runtime for backward compatibility, but Tailwind can purge such a class. Pagination buttons grow to a 44×44 px touch target on coarse pointers. |\n| `serverSide` | `boolean` | `false` | Server-side pagination/sort/filter |\n| `totalRecords` | `number` | `0` | Total count (serverSide). Drives the page count and the \"start - end of total\" counter; when omitted the counter falls back to the rows received. |\n| `pageNumber` | `number` | — | Controlled page. Client-side, a page past the last one is clamped to the last page once rows exist. |\n| `onPageChange` | `(page: number) => void` | — | Called with the new page number when the user changes page, and when the table clamps a page that no longer exists (client-side). Not called for the reset to page 1 on a filter or page-size change. Consumed by the table — it is **not** forwarded onto the `<table>`. |\n| `responsiveBreakpoint` | `number` | `768` | px breakpoint for mobile cards: card mode while the viewport is narrower than this. Read with an SSR-safe media query (`not all and (min-width: <n>px)`), so client renders pick the right layout on the first render and follow resizes live. |\n| `defaultMobileView` | `boolean` | `false` | Layout used on the server and while hydrating, before the media query can be read. Set `true` on phone-first pages so server-rendered HTML starts as cards. |\n| `mobileFiltersPresentation` | `string` | `\"auto\"` | Card-mode filters (with `filterable` / `globalSearch`). `sheet`: a \"Filters\" button (with an active-filter count) opening a bottom `Sheet`; edits are a draft until **Apply**, **Clear** empties the draft. `inline`: the collapsible inline panel (the previous behaviour, live filtering). `auto` uses the sheet on small touch screens (`(pointer: coarse) and (max-width: 639px)`) and the inline panel elsewhere. |\n| `swipeActions` | `(row, index) => { leading?, trailing? } \\| null` | — | Swipe actions on the mobile cards, built on [`SwipeableListItem`](SwipeableListItem.md). Each side is an array of `{ key, label, icon?, tone?, disabled?, onSelect }`; swipe right for `leading`, left for `trailing`, a full swipe commits the first action of that side. Only one card is open at a time. Every action is also reachable without a gesture through the card's \"Swipe actions\" button. Return `null` (or empty arrays) for rows without swipe actions. |\n| `stickyHeader` | `boolean` \\| `string` \\| `number` | `false` | Keep the desktop header visible while the body scrolls. `true` sticks within the table's own scroll container; a CSS length (`\"24rem\"`) or a number (px) also caps that container's height so the rows scroll under the header. |\n| `showToolbar` | `boolean` | `true` | Show the toolbar above the table (reload, page size, result count, pager header) and the column picker. `false` hides all of it; the pager footer stays, and in `interactive` mode the add-row / Save / Clear controls still render. `globalSearch` is its own opt-in and is unaffected. |\n| `showColumnPicker` | `boolean` | `true` | Show the column-picker button in the header (accessible name \"Choose columns\", with `aria-expanded`). `false` also removes its (otherwise empty) column from every row. |\n| `wrapCells` | `boolean` | `false` | Let cell text wrap instead of staying on one line. A column's own `wrap` overrides it. |\n| `mobileMaxFields` | `number` | — | Cap the fields shown on each mobile card. Applied in column order, after `hideOnMobile` columns are removed. |\n\nOnly DOM-valid attributes in `...rest` (e.g. `id`, `aria-*`, `data-*`) are\nforwarded to the root `<table>`; the table's own control props (`onPageChange`,\n`serverSide`, `pagination`, the `on*` callbacks, …) are consumed and never leak\nto the DOM as unknown attributes.\n\n## Examples\n\n### Basic table\n\n```jsx\nconst columns = [\n { key: 'name', label: 'Name' },\n { key: 'email', label: 'Email' },\n];\nconst data = [\n { id: 1, name: 'Alice', email: 'alice@example.com' },\n { id: 2, name: 'Bob', email: 'bob@example.com' },\n];\n<Table data={data} columns={columns} />\n```\n\n### With pagination and selection\n\n```jsx\n<Table\n data={data}\n columns={columns}\n pagination\n pageSize={10}\n selectable\n onSelectionChange={(selected) => console.log(selected)}\n/>\n```\n\n### Server-side data\n\n```jsx\n<Table\n data={data}\n columns={columns}\n serverSide\n pagination\n totalRecords={total}\n onFetch={async ({ setData, setLoading, page, limit, sort }) => {\n setLoading(true);\n const res = await fetch(`/api/users?page=${page}&limit=${limit}`);\n const json = await res.json();\n setData(json.rows);\n setLoading(false);\n }}\n/>\n```\n\n### Filtering and page clamping (client-side)\n\nWithout `serverSide`, the table filters the rows itself:\n\n- **Global search** (`globalSearch`) applies on Enter or the search button,\n with or without `filterable`. Columns with `searchable: false` are skipped.\n- **Column filters** (`filterable`) apply as you type. Each one matches only its\n own column's raw value (not a `render` output).\n- All matches are case-insensitive \"contains\" tests, and every active filter\n must match (AND).\n- `onFilter` / `onFilterChange` still receive the whole `filters` object\n (`{ global?, [columnKey]: text }`) on every change.\n\nWhen the rows shrink (filtering, deleted rows, a new `data` prop) and the\ncurrent page no longer exists, the table shows the last page that does and\ncalls `onPageChange` with it. A parent that drives `pageNumber` can simply\nstore that value, and no `clampPage` helper is needed. The clamp waits while\nthere are no rows, so a page restored from the URL survives until the data\narrives.\n\nWith `serverSide`, none of this runs locally: filters go to `onFetch` /\n`onFilter` / `onFilterChange`, the rows shown are exactly the rows you pass,\nand keeping `pageNumber` within `totalRecords` is up to you.\n\n### Compact, read-only table\n\nHide the chrome for a dashboard card, keep id columns out of the search, let\nlong text wrap, and keep phone cards short.\n\n```jsx\nconst columns = [\n { key: 'id', label: 'ID', searchable: false, hideOnMobile: true },\n { key: 'client', label: 'Client' },\n { key: 'status', label: 'Status' },\n { key: 'notes', label: 'Notes', wrap: true },\n { key: 'owner', label: 'Owner' },\n];\n\n<Table\n data={rows}\n columns={columns}\n showToolbar={false}\n showSerial={false}\n withAction={false}\n mobileMaxFields={3}\n/>\n```\n\nOn a phone each card shows Client, Status and Notes. With no serial, selection,\nexpand toggle or actions, the card has no header strip either.\n\n`showToolbar={false}` does not change pagination: with `pagination` on, the pager\nfooter still renders. Hide only the picker with `showColumnPicker={false}`.\n\n### Conditional row details\n\n`hasDetails` accepts a predicate so only rows that actually have something to\nshow get an expand toggle. Rows that don't qualify still render the (empty)\ntoggle cell, so the columns stay aligned.\n\n```jsx\nconst ErrorDetails = ({ row }) => <pre>{row.stackTrace}</pre>;\n\n<Table\n data={jobs}\n columns={columns}\n hasDetails={(row) => row.status === 'failed'}\n DetailsComponent={ErrorDetails}\n/>\n```\n\n### Mobile cards, filters and swipe actions\n\nBelow `responsiveBreakpoint` rows render as cards. Card controls (expand\ntoggle, selection checkbox, inline actions, kebab) have 44×44 px hit areas.\nWith `filterable` on a phone, the filters open in a bottom sheet with an\nApply / Clear footer; pass `mobileFiltersPresentation=\"inline\"` to keep the\ninline panel.\n\n```jsx\n<Table\n data={messages}\n columns={columns}\n filterable\n defaultMobileView // server-rendered HTML starts as cards\n swipeActions={(row) => ({\n leading: [{ key: 'read', label: 'Read', icon: MailOpen, tone: 'primary', onSelect: () => markRead(row) }],\n trailing: [\n { key: 'delete', label: 'Delete', icon: Trash2, tone: 'destructive', onSelect: () => remove(row) },\n { key: 'flag', label: 'Flag', icon: Flag, tone: 'warning', onSelect: () => flag(row) },\n ],\n })}\n/>\n```\n\n### Sticky header\n\n```jsx\n<Table data={rows} columns={columns} stickyHeader=\"28rem\" />\n```\n\n### Row action menu\n\nThe kebab button (`aria-haspopup=\"menu\"`) opens the row's menu actions. On\ndesktop it is a `role=\"menu\"` popover: focus moves to the first action,\nArrowUp/ArrowDown (wrapping), Home/End and first-letter typeahead move between\nactions, Escape or Tab closes it and returns focus to the kebab, and a press\noutside or a page scroll closes it (pressing the kebab again closes it too). On phones (`actionMenuPresentation=\"auto\"`,\nthe default) the same actions open as an action sheet: 48px rows in a rounded\ncard, destructive actions in red, and a Cancel button below. Pass\n`actionMenuPresentation=\"popover\"` to keep the desktop menu everywhere.\n\n```jsx\n<Table\n data={users}\n columns={columns}\n actions={[{ name: 'delete', tone: 'destructive' }]}\n actionMenuPresentation=\"auto\"\n/>\n```\n\n### Conditional and inline actions\n\n`hidden` and `disabled` accept a `(row, index)` predicate, `label` and `icon`\naccept a `(row, index)` producer, and `inline: true` promotes an action out of\nthe kebab menu into the row itself. When no menu actions remain for a row, the\nkebab button is not rendered at all.\n\n```jsx\n<Table\n data={jobs}\n columns={columns}\n replaceDefaultActions\n actions={[\n { name: 'view', label: 'View', icon: <Eye size={16} />, inline: true },\n { name: 'edit', label: 'Edit', icon: <Edit size={16} />,\n disabled: (row) => row.locked },\n { name: 'retry', label: (row) => `Retry (${row.tries})`,\n hidden: (row) => row.status !== 'failed' },\n ]}\n onAction={({ action, row }) => handle(action.name, row)}\n/>\n```\n\nFor a large table, wrap a function-valued `actions` in `useCallback` — a fresh\narrow on every render defeats the per-row memoization.\n\n```jsx\nconst actions = useCallback(\n (row) => (row.role === 'admin' ? adminActions : userActions),\n []\n);\n```\n\nNote that because `icon` may be a `(row, index) => node` producer, passing a\nbare component reference (`icon: Edit`) calls it rather than rendering it. Pass\nan element instead: `icon: <Edit size={16} />`.\n\n### Patching the default actions\n\nEntries you supply are merged onto the built-in Edit/Delete/View set by `name`,\nso you can swap one handler without restating the rest. Unmatched names are\nappended after the defaults, and `hidden: true` removes a default outright.\n\n```jsx\n<Table\n data={users}\n columns={columns}\n actions={[\n { name: 'edit', onClick: ({ row }) => edit(row) }, // keeps default label + icon\n { name: 'delete', hidden: (row) => !row.canDelete },\n { name: 'invite', label: 'Invite', icon: <Mail size={16} /> }, // appended\n ]}\n/>\n```\n\nPass `replaceDefaultActions` to skip merging and use only your own set:\n\n```jsx\n<Table\n data={users}\n columns={columns}\n replaceDefaultActions\n actions={[{ name: 'approve', label: 'Approve' }]}\n/>\n```\n\n### Interactive Table Mode\n\nPass `interactive={true}` to enable inline cell editing (double-click a cell or press standard `'e'` key on a focused active cell to enter edit mode), instant record addition (at top or bottom), keyboard navigation (`Tab`/`Shift+Tab` to move between cells, `Escape` to exit edit mode, `Enter` to save modified changes), and constraint validation before saving.\n\n#### Interactive Props & Options\n\n- `interactive`: `boolean` (default: `false`) — Enables interactive mode.\n- `editKeyBinding`: `string | ((e) => boolean)` (default: `\"e\"`) — Standard key binding to enter cell edit mode on active focused cell.\n- `newRowPosition`: `\"bottom\" | \"top\"` (default: `\"bottom\"`) — Position where empty new rows are inserted (at the bottom or top of table dataset).\n- `addRowRef`: `React.MutableRefObject` — External ref populated with `(position?: \"top\" | \"bottom\") => void` to trigger adding a new row from external buttons.\n- `onInitAddRow`: `(addRowFn: (position?: \"top\" | \"bottom\") => void) => void` — Callback receiving the add-row function on component initialization.\n- `renderAddRowButton`: `({ addRow, newRowPosition }) => ReactNode` — Render prop for rendering a custom external add-row button in the table toolbar.\n\n#### Interactive Usage Example\n\n```jsx\nconst columns = [\n { key: 'name', label: 'Name', type: 'text', required: true },\n { key: 'role', label: 'Role', type: 'select', options: ['Admin', 'User'], required: true },\n { key: 'notes', label: 'Notes', type: 'textarea' },\n];\n\nconst addRowRef = useRef();\n\nreturn (\n <div>\n <button onClick={() => addRowRef.current?.(\"top\")}>\n Add Row at Top\n </button>\n\n <Table\n interactive\n editKeyBinding=\"e\"\n newRowPosition=\"top\"\n addRowRef={addRowRef}\n columns={columns}\n data={users}\n onSave={async ({ updatedRows, newRows, allData }) => {\n await api.saveUsers({ updatedRows, newRows });\n }}\n />\n </div>\n);\n```\n\n[← Component overview](README.md)\n",
|
|
6526
|
+
"Tabs": "# Tabs\n\nTabbed content with list and panels. Use subcomponents: `Tabs.List`, `Tabs.Tab`, `Tabs.Panels`, `Tabs.Panel`. Supports controlled (`index` + `onChange`) or uncontrolled (`defaultIndex`), keyboard navigation, and variants.\n\n**Accessibility and SSR:** tab and panel ids come from `React.useId()`, so server-rendered markup hydrates without mismatches and two Tabs on one page never collide. Each tab's `aria-controls` and each panel's `aria-labelledby` point at elements that exist. Panels mount lazily, so only the selected tab carries `aria-controls`.\n\n## Import\n\n```jsx\nimport { Tabs } from '@dreamtree-org/twreact-ui';\n```\n\n## Props (Tabs root)\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Must include `Tabs.List` and `Tabs.Panels`. |\n| `defaultIndex` | `number` | `0` | Uncontrolled initial active tab index. |\n| `index` | `number` | — | Controlled active index. |\n| `onChange` | `(index: number) => void` | — | Called when tab changes. |\n| `orientation` | `\"horizontal\"` \\| `\"vertical\"` | `\"horizontal\"` | Layout direction. |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Tab size. Other `Size` values (`xs`, `xl`, `2xl`) fall back to `md`. |\n| `variant` | `\"line\"` \\| `\"pills\"` \\| `\"segmented\"` \\| `\"unstyled\"` | `\"line\"` | Tab style. `segmented` is an iOS-style segmented control: equal-width segments on a tinted `bg-line` track (same as `SegmentedControl`, follows dark mode) with a sliding surface thumb. |\n| `animated` | `boolean` | `true` | Fade-in (`animate-fade-in`) on the newly active panel; skipped under `prefers-reduced-motion`. |\n| `swipeable` | `boolean` | `false` | Swipe the panels left/right (touch or pen, at least 50 px and mostly horizontal) to move to the next/previous enabled tab. Vertical scrolling is never blocked (`touch-action: pan-y`); mouse drags are ignored. |\n| `className` | `string` | `\"\"` | Wrapper CSS classes. |\n\n**Scrolling and indicator:** a horizontal `Tabs.List` scrolls sideways with scroll-snap and a hidden scrollbar, and keeps the active tab scrolled into view. For `line` and `segmented`, the active underline / thumb is a single indicator that slides between tabs (`duration-base`, no transition under `prefers-reduced-motion`). It is measured after mount, so server-rendered markup shows the static active style until then. Horizontal tabs draw their focus ring inset so the scroll container doesn't clip it.\n\n`Tabs.List` forwards a `ref` and spreads extra props (`aria-label`, `data-*`, …) onto the `role=\"tablist\"` element.\n\n## Tabs.Tab props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Tab label |\n| `leftIcon` | `ReactNode` | — | Icon before the label. Renamed from `iconLeft` in 2.0.0 ([migration](MIGRATION-2.0.md#tabstab-iconleft--iconright)) |\n| `rightIcon` | `ReactNode` | — | Icon after the label. Renamed from `iconRight` in 2.0.0 |\n| `disabled` | `boolean` | `false` | Skip the tab in keyboard navigation and ignore clicks |\n| `tabClassName` | `string` | `\"\"` | Extra classes for the tab button |\n\n## Usage\n\n```jsx\n<Tabs defaultIndex={0} variant=\"pills\" size=\"md\">\n <Tabs.List>\n <Tabs.Tab>Home</Tabs.Tab>\n <Tabs.Tab>Profile</Tabs.Tab>\n <Tabs.Tab leftIcon={<Settings className=\"h-4 w-4\" />}>Settings</Tabs.Tab>\n </Tabs.List>\n <Tabs.Panels>\n <Tabs.Panel>Home content</Tabs.Panel>\n <Tabs.Panel>Profile content</Tabs.Panel>\n <Tabs.Panel>Settings content</Tabs.Panel>\n </Tabs.Panels>\n</Tabs>\n```\n\nControlled:\n\n```jsx\nconst [index, setIndex] = useState(0);\n<Tabs index={index} onChange={setIndex}>\n ...\n</Tabs>\n```\n\n[← Component overview](README.md)\n",
|
|
5395
6527
|
"TextToSpeech": "# TextToSpeech\n\nText-to-speech using the browser’s Speech Synthesis API. Renders a styled \"Speak\" / \"Stop\" button (matching `SpeechToText`) or a custom button via `renderButton`. Toggling speak while playing stops playback.\n\n## Import\n\n```jsx\nimport { TextToSpeech } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `text` | `string` | `\"\"` | Text to speak. The **current** value is spoken on each click, so later prop changes are honoured. |\n| `rate` | `number` | `1` | Speech rate (0.1–10). |\n| `pitch` | `number` | `0.5` | Pitch (0–2). |\n| `onSpeak` | `(text: string) => void` | `() => {}` | Called when speech starts (with the text spoken). |\n| `onError` | `(error: Error) => void` | `() => {}` | Called when synthesis fails. Intentional stops (`interrupted` / `canceled`) are not reported. |\n| `disabled` | `boolean` | `false` | Disables the button. |\n| `renderButton` | `(props: { onClick, isSpeaking, isSupported, disabled, speak, stop }) => ReactNode` | — | Custom button. `onClick` toggles; `isSupported` is `false` when the browser has no `speechSynthesis`. |\n| `className` | `string` | — | Merged (via `cn`) onto the default button. |\n| `...rest` | `ButtonHTMLAttributes` | — | Forwarded to the default `<button>` (`aria-*`, `data-*`, `id`, …). |\n\n`ref` is forwarded to the default `<button>` element.\n\n## Behavior\n\n- Clicking the button toggles: if not speaking, speaks the current `text` with a new `SpeechSynthesisUtterance`; if speaking, calls `speechSynthesis.cancel()`.\n- `onSpeak` is called when speech starts; the button returns to \"Speak\" when the utterance ends **or errors**.\n- **Unsupported browsers:** when `window.speechSynthesis` is missing the default button renders disabled with a `title` (and screen-reader text) explaining that text-to-speech is not supported. `renderButton` receives `isSupported: false` / `disabled: true`.\n- **Unmount:** if this instance is speaking when it unmounts, playback is cancelled.\n- The default button is rendered directly (no wrapper `<div>`), with `aria-pressed` reflecting the speaking state and a `focus-visible` ring.\n\n## Example\n\n```jsx\n<TextToSpeech text=\"Hello, world!\" rate={1} onSpeak={(t) => console.log('Speaking:', t)} />\n\n<TextToSpeech\n text={content}\n renderButton={({ onClick, isSpeaking }) => (\n <Button onClick={onClick}>{isSpeaking ? 'Stop' : 'Play'} audio</Button>\n )}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
5396
|
-
"Textarea": "# Textarea\n\nMulti-line text input that mirrors `Input`'s API: label, error, helper text, required marker, character count, plus optional auto-resize.\n\n## Import\n\n```jsx\nimport { Textarea } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `label` | `ReactNode` | — | Label rendered above the field (linked with `htmlFor`) |\n| `error` | `ReactNode` | — | Error message; sets `aria-invalid` and is shown with an icon |\n| `helperText` | `ReactNode` | — | Hint below the field; wired to `aria-describedby` |\n| `required` | `boolean` | `false` | Marks the field required (native `required` + visual `*`) |\n| `disabled` | `boolean` | `false` | Disables the field |\n| `showCount` | `boolean` | `false` | Show `current/maxLength` (needs `maxLength`) |\n| `maxLength` | `number` | — | Native max length |\n| `autoResize` | `boolean` | `false` | Grow with the content; disables the manual resize handle |\n| `rows` | `number` | `3` | Initial (minimum) rows |\n| `maxRows` | `number` | — | With `autoResize`, cap the height and scroll beyond it |\n| `value` | `string` | — | Controlled value |\n| `defaultValue` | `string` | — | Uncontrolled initial value |\n| `onChange` | `function` | — | Native change handler |\n| `id` | `string` | — | Field id (auto-generated otherwise) |\n| `className` | `string` | — | Extra classes for the `<textarea>` (merged with `cn`) |\n| `...rest` | `TextareaHTMLAttributes` | — | Forwarded to the `<textarea>`; `ref` points at it |\n\nWorks controlled (`value` + `onChange`) or uncontrolled (`defaultValue`); the character count tracks both.\n\n## Examples\n\n### Basic\n\n```jsx\n<Textarea label=\"Description\" placeholder=\"What needs to happen?\" helperText=\"Plain text only.\" />\n```\n\n### Count + validation\n\n```jsx\n<Textarea label=\"Reason\" required showCount maxLength={280} error={errors.reason} value={reason} onChange={(e) => setReason(e.target.value)} />\n```\n\n### Auto-resize\n\n```jsx\n<Textarea label=\"Notes\" autoResize maxRows={8} value={notes} onChange={(e) => setNotes(e.target.value)} />\n```\n\n## Accessibility\n\n| Behavior | Detail |\n|----------|--------|\n| Labelling | `label` is a real `<label htmlFor>` |\n| Description | `error` and `helperText` ids (plus any `aria-describedby` you pass) are joined into `aria-describedby` |\n| Error | `aria-invalid=\"true\"`; the message is `aria-live=\"polite\"` and paired with an icon (not color alone) |\n| Focus ring | `focus:ring-2 focus:ring-primary-500` (error ring when invalid) |\n| Dark mode | Field, border, label and helper text use `dark:` variants |\n\n[← Component overview](README.md)\n",
|
|
5397
|
-
"ThreeDotPopover": "# ThreeDotPopover\n\
|
|
5398
|
-
"Toast": "# Toast\n\nToast notification component, container and app-level provider. Wrap your app in `ToastProvider` and call `useToast()` anywhere below it — every caller shares one queue and the provider renders it. Without a provider, `useToast` keeps a component-local queue that you render with `ToastContainer` (the original behavior). Toasts have a status
|
|
5399
|
-
"Tooltip": "# Tooltip\n\nTooltip shown on hover, focus, or click. Supports placement (auto/top/bottom/left/right), delay, offset, and optional portal. Content can be string or render prop `({ close }) => ReactNode
|
|
5400
|
-
"Tour": "# Tour\n\nStep-based onboarding tour (coach marks): dims the page, spotlights each step's target element and shows a step card beside it with Back / Next / Skip / Finish. Generalises the `SpotlightTour` that TaskPro and ProcurementPro each hand-rolled. A step without a target — or whose target never renders — shows as a centred card, so the tour never stalls.\n\n## Import\n\n```jsx\nimport { Tour } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `open` | `boolean` | `false` | Show the tour |\n| `steps` | `array` | `[]` | `TourStep[]`: `{ target, title, content, placement }` (see below) |\n| `onClose` | `function` | — | `(reason) => void`; reason is `skip`, `escape`, `close` or `finish`. Set `open` to false here |\n| `onFinish` | `function` | — | Called by the Finish button on the last step (before `onClose('finish')`) |\n| `step` | `number` | — | Controlled step index |\n| `defaultStep` | `number` | `0` | Uncontrolled starting step; the tour restarts here every time it opens |\n| `onStepChange` | `function` | — | `(index, step) => void`; use it to navigate routes before a step shows |\n| `labels` | `object` | — | Override `next`, `back`, `skip`, `finish`, `close` and `progress(current, total)` |\n| `targetTimeout` | `number` | `3000` | How long (ms) to wait for a step's target before falling back to a centred card |\n| `spotlightPadding` | `number` | `6` | Space in px between the target and the spotlight ring |\n| `className` | `string` | — | Extra classes for the step card (merged with `cn`) |\n| `...rest` | `HTMLAttributes` | — | Forwarded to the step card; `ref` points at it |\n\n## Step shape\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `target` | `string, ref, element or function` | CSS selector (e.g. `[data-tour=\"new-task\"]`), a React ref, a DOM element, or `() => element`. Omit for a centred intro/outro card |\n| `title` | `ReactNode` | Card heading; labels the dialog |\n| `content` | `ReactNode` | Card body; describes the dialog |\n| `placement` | `string` | `top`, `bottom` (default), `left`, `right` or `center`; flips when there is no room |\n\n## Examples\n\n### Basic\n\n```jsx\nconst steps = [\n { title: 'Welcome', content: 'A 30-second tour of your workspace.' },\n { target: '[data-tour=\"new-task\"]', title: 'Capture work', content: 'New task opens a quick form.', placement: 'bottom' },\n { target: searchRef, title: 'Jump anywhere', content: 'Search tasks and projects.', placement: 'right' },\n];\n\nconst [open, setOpen] = useState(false);\n\n<Button onClick={() => setOpen(true)}>Take the tour</Button>\n<Tour open={open} steps={steps} onClose={() => setOpen(false)} onFinish={() => markTourSeen()} />\n```\n\n### Route-aware (react-router)\n\n```jsx\n<Tour\n open={open}\n steps={steps}\n onStepChange={(i, step) => step.route && navigate(step.route)}\n onClose={() => setOpen(false)}\n/>\n```\n\nExtra fields on a step (like `route`) are passed back to you untouched. The tour waits up to `targetTimeout` for the next screen's target to render.\n\n## Accessibility & keyboard\n\n| Behavior | Detail |\n|----------|--------|\n| Role | The **step card** is `role=\"dialog\"` + `aria-modal=\"true\"`, labelled by the step title and described by its content (the full-screen layer has no role, so global `[role=dialog]` styles never paint over the page) |\n| Focus | Moves to the card on open and on every step change; `Tab` is trapped in the card; focus returns to the opener on close |\n| `Escape` | Closes with reason `escape` |\n| `ArrowRight` / `ArrowLeft` | Next / previous step (ignored while typing in a field inside the card) |\n| Scroll into view | The target is scrolled into view through its nearest scrolling ancestor only (never `overflow: hidden` shells); smooth unless `prefers-reduced-motion` |\n| Reduced motion | Spotlight and card transitions are disabled under `prefers-reduced-motion` |\n| Scroll lock | Body scroll is locked while open |\n| Progress | \"Step 2 of 5\" text is visible and read with the card |\n| Dark mode | Card uses `dark:` variants; the spotlight ring switches to `primary-400` |\n\n[← Component overview](README.md)\n",
|
|
5401
|
-
"useApi": "# useApi\n\nThin axios wrapper hook. Sends requests by URL (`fetchData`) or by key from an `apiMap` (`fetchApi` / `fetchApiWith`) and tracks `data`, `error`, `loading` and `lastRequest`. It is not a cache/dedup layer — bring React Query or SWR for that.\n\n## Import\n\n```jsx\nimport { useApi } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\nOptions object passed to `useApi(options)`.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `BASE_URL` | `string` | `null` | axios `baseURL` |\n| `DEFAULT_HEADERS` | `object` | `{}` | Headers sent with every request |\n| `apiMap` | `Record<string, (...args) => { url, method, headers?, data? }>` | `{}` | Named request builders. Entries may take any number of positional arguments |\n| `createRequestInstance` | `function` | `axios.create` | `(baseURL, defaultHeaders) => AxiosInstance` |\n| `sendRequest` | `function` | — | `(instance, method, url, data, headers) => Promise<AxiosResponse>`; GET/HEAD/DELETE send no body |\n\n## Returns\n\n| Field | Type | Description |\n|------|------|-------------|\n| `data` | `any` | Last successful `response.data` |\n| `error` | `Error \\| null` | Last error |\n| `loading` | `boolean` | True while a request with `needLoading` is in flight |\n| `lastRequest` | `string \\| null` | Key of the last `fetchApi` / `fetchApiWith` call |\n| `fetchData(url, method, data?, headers?, { needLoading? })` | `Promise` | Send a request directly |\n| `fetchApi(apiKey, data?, needLoading = true)` | `Promise` | Call `apiMap[apiKey](data)` — **exactly one argument** — and send the result |\n| `fetchApiWith(apiKey, args = [], { needLoading = true })` | `Promise` | Call `apiMap[apiKey](...args)` — every element of `args` is a positional argument — and send the result |\n\n### fetchApi vs fetchApiWith\n\n`fetchApi` always calls the map entry with ONE argument. An entry written as `(id, body) => ({...})` called through `fetchApi('updateUser', id)` gets `body === undefined`, so nothing is sent. Use `fetchApiWith` for multi-argument entries:\n\n```jsx\nfetchApiWith('updateUser', [id, body]); // apiMap.updateUser(id, body)\nfetchApiWith('updateUser', [id, body], { needLoading: false });\n```\n\n`fetchApi` is unchanged; existing single-argument callers keep working.\n\n## Examples\n\n```jsx\nconst apiMap = {\n getUser: (id) => ({ url: `/users/${id}`, method: 'GET' }),\n updateUser: (id, body) => ({ url: `/users/${id}`, method: 'PUT', data: body }),\n};\n\nfunction Profile({ id }) {\n const { data, loading, error, fetchApi, fetchApiWith } = useApi({\n BASE_URL: 'https://api.example.com',\n DEFAULT_HEADERS: { Authorization: `Bearer ${token}` },\n apiMap,\n });\n\n useEffect(() => { fetchApi('getUser', id); }, [id]);\n\n const save = (body) => fetchApiWith('updateUser', [id, body]);\n // …\n}\n```\n\n[← Component overview](README.md)\n"
|
|
6528
|
+
"Textarea": "# Textarea\n\nMulti-line text input that mirrors `Input`'s API: label, error, helper text, required marker, character count, plus optional auto-resize.\n\n## Import\n\n```jsx\nimport { Textarea } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `label` | `ReactNode` | — | Label rendered above the field (linked with `htmlFor`) |\n| `error` | `ReactNode` | — | Error message; sets `aria-invalid` and is shown with an icon |\n| `helperText` | `ReactNode` | — | Hint below the field; wired to `aria-describedby` |\n| `required` | `boolean` | `false` | Marks the field required (native `required` + visual `*`) |\n| `disabled` | `boolean` | `false` | Disables the field |\n| `showCount` | `boolean` | `false` | Show `current/maxLength` (needs `maxLength`) |\n| `maxLength` | `number` | — | Native max length |\n| `autoResize` | `boolean` | `false` | Grow with the content; disables the manual resize handle |\n| `rows` | `number` | `3` | Initial (minimum) rows |\n| `maxRows` | `number` | — | With `autoResize`, cap the height and scroll beyond it |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Padding and text size (`md` is the original look); `rows` still sets the height |\n| `value` | `string` | — | Controlled value |\n| `defaultValue` | `string` | — | Uncontrolled initial value |\n| `onChange` | `function` | — | Native change handler |\n| `id` | `string` | — | Field id (auto-generated otherwise) |\n| `className` | `string` | — | Extra classes for the `<textarea>` (merged with `cn`) |\n| `...rest` | `TextareaHTMLAttributes` | — | Forwarded to the `<textarea>`; `ref` points at it |\n\nWorks controlled (`value` + `onChange`) or uncontrolled (`defaultValue`); the character count tracks both.\n\nThe focus ring is `focus-visible` only (no ring left after a mouse click), and colours use role tokens (`bg-surface`, `text-foreground`) with `dark:` counterparts.\n\n## Examples\n\n### Basic\n\n```jsx\n<Textarea label=\"Description\" placeholder=\"What needs to happen?\" helperText=\"Plain text only.\" />\n```\n\n### Count + validation\n\n```jsx\n<Textarea label=\"Reason\" required showCount maxLength={280} error={errors.reason} value={reason} onChange={(e) => setReason(e.target.value)} />\n```\n\n### Auto-resize\n\n```jsx\n<Textarea label=\"Notes\" autoResize maxRows={8} value={notes} onChange={(e) => setNotes(e.target.value)} />\n```\n\n## Accessibility\n\n| Behavior | Detail |\n|----------|--------|\n| Labelling | `label` is a real `<label htmlFor>` |\n| Description | `error` and `helperText` ids (plus any `aria-describedby` you pass) are joined into `aria-describedby` |\n| Error | `aria-invalid=\"true\"`; the message is `aria-live=\"polite\"` and paired with an icon (not color alone) |\n| Focus ring | `focus:ring-2 focus:ring-primary-500` (error ring when invalid) |\n| Dark mode | Field, border, label and helper text use `dark:` variants |\n\n[← Component overview](README.md)\n",
|
|
6529
|
+
"ThreeDotPopover": "# ThreeDotPopover\n\nAction menu triggered by a three-dot (kebab) button. Use for row actions (Edit, Delete, etc.). On desktop it opens a floating menu; on phones it opens an iOS-style action sheet. Closes on outside press, Escape and (by default) on item select.\n\n## Import\n\n```jsx\nimport { ThreeDotPopover } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `items` | `Array` | `[]` | Menu items: `{ key, label, icon?, onClick?, disabled?, destructive?, tone? }`. `icon` can be a component (e.g. `Edit2`) or a node. |\n| `presentation` | `string` | `\"auto\"` | One of `auto`, `popover`, `sheet`. `popover`: floating menu next to the trigger. `sheet`: action sheet from the bottom of the screen. `auto`: the sheet on small touch screens (`(pointer: coarse) and (max-width: 639px)`), the popover elsewhere and on the server. |\n| `trigger` | `ReactElement` | — | Custom trigger; default is three-dot button. It receives `ref`, `onClick`, `onKeyDown`, `aria-haspopup=\"menu\"`, `aria-expanded` and `aria-controls`. |\n| `className` | `string` | `\"\"` | CSS classes for the **default** trigger button (merged via `cn()`). |\n| `wrapperClassName` | `string` | `\"\"` | CSS classes merged (via `cn()`) onto the root container element. |\n| `menuClass` | `string` | `\"\"` | Classes for the popover menu container (popover mode only). |\n| `menuItemClass` | `string` | `\"\"` | Classes for every menu item (popover and sheet). |\n| `closeOnSelect` | `boolean` | `true` | Close menu when an item is selected. |\n| `ariaLabel` | `string` | `\"More options\"` | Trigger button aria-label; also names the menu / sheet. |\n| `cancelLabel` | `ReactNode` | `\"Cancel\"` | Text of the action sheet's Cancel button (sheet mode only). |\n\n## Item shape\n\n- `key` (string): Unique key.\n- `label` (string): Display text (also used for typeahead).\n- `icon` (optional): React component or node.\n- `onClick(item)` (optional): Called when item is clicked.\n- `disabled` (boolean): Disable the item (skipped by keyboard navigation).\n- `destructive` (boolean) or `tone: 'destructive'`: Style in the error tone (e.g. red for Delete). The two are equivalent.\n\n## Example\n\n```jsx\nimport { Edit2, Eye, Trash } from 'lucide-react';\n\n<ThreeDotPopover\n items={[\n { key: 'edit', label: 'Edit', icon: Edit2, onClick: () => edit(row) },\n { key: 'view', label: 'View', icon: Eye, onClick: () => view(row) },\n { key: 'delete', label: 'Delete', tone: 'destructive', onClick: () => remove(row) },\n ]}\n closeOnSelect\n/>\n```\n\nKeep the desktop menu on every device:\n\n```jsx\n<ThreeDotPopover presentation=\"popover\" items={items} />\n```\n\n## Behavior\n\n- **Popover mode.** The menu is portaled to `document.body` and positioned\n with flip/shift, so it is never clipped by an `overflow: hidden` parent\n (such as a scrolling table) or the screen edge. It stacks on the `z-popover`\n layer, above dialogs and sheets.\n- **Keyboard.** Opening focuses the first enabled item (ArrowDown / ArrowUp on\n the closed trigger also open it, focusing the first / last item).\n ArrowDown / ArrowUp move between items and wrap, Home / End jump to the ends,\n and typing a letter jumps to the next item starting with it. Escape closes the\n menu and returns focus to the trigger; Tab closes it and continues from the\n trigger.\n- **Sheet mode.** Items are 48px rows in a rounded card, destructive items in\n the error tone, with a separate Cancel button below. Escape, the backdrop,\n a downward drag or Cancel close it, and focus returns to the trigger.\n- **Trigger.** `aria-haspopup=\"menu\"`, `aria-expanded`, a focus-visible ring\n and a 44×44px touch target on coarse pointers.\n\n## Notes\n\n- Forwards `ref` to the root container element and spreads unknown props\n (`...rest`) onto it.\n- The menu is only in the DOM while open.\n\n[← Component overview](README.md)\n",
|
|
6530
|
+
"Toast": "# Toast\n\nToast notification component, container and app-level provider. Wrap your app in `ToastProvider` and call `useToast()` anywhere below it — every caller shares one queue and the provider renders it. Without a provider, `useToast` keeps a component-local queue that you render with `ToastContainer` (the original behavior). Toasts have a status icon, message body, action buttons and a bottom progress bar; they swipe away horizontally, pause while hovered, focused or touched, sit bottom-center above the FootNav on phones, and collapse into a Sonner-style stack past three.\n\n## Import\n\n```jsx\nimport { Toast, ToastContainer, ToastProvider, useToast } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\nProps of the `Toast` card. `ToastContainer` and `ToastProvider` props are listed below.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `id` | `string` | — | Unique id (for key/onClose) |\n| `title` | `string` | — | Toast title |\n| `message` | `string` | — | Toast message body (alias for `description`) |\n| `description` | `string` | — | Toast body text |\n| `type` | `string` | `\"info\"` | Toast status type: `success`, `error`, `warning`, `info`, `default` |\n| `duration` | `number` | `5000` | Auto-close timer in ms (0 = no auto-close) |\n| `statusIcon` | `ReactNode` | — | Custom status icon (overrides the icon for `type`) |\n| `actions` | `Array` | `[]` | Action button definitions `[{ label, onClick, variant: 'primary' or 'secondary', dismiss, className }]` |\n| `onClose` | `function` | — | Called with `id` once the toast has closed (after its exit animation) |\n| `position` | `string` | `\"top-right\"` | Position key: `top-right`, `top-left`, `bottom-right`, `bottom-left`, `top-center`, `bottom-center` |\n| `portal` | `boolean` | `true` | Render through a portal at a fixed position (`ToastContainer` sets `false`) |\n| `paused` | `boolean` | `false` | Pause the auto-dismiss timer from outside (the stack does this while the user has it expanded) |\n| `swipeToDismiss` | `boolean` | `true` | A horizontal swipe past 64px, or a quick flick, dismisses the toast. Vertical page scroll is untouched (`touch-action: pan-y`) |\n| `mobilePosition` | `string` or `false` | `\"bottom-center\"` | `bottom-center` or `top-center` on phones (coarse pointer, under 640px): full width inside a 1rem gutter. `false` keeps `position`. Applies to a portaled toast |\n| `mobileOffset` | `number` or `string` | `\"5rem\"` | Gap above the bottom edge on phones (px or any CSS length), plus the safe area. Sets `--dt-toast-mobile-offset`; the default clears the FootNav |\n| `closeLabel` | `string` | `\"Close notification\"` | Accessible name of the close button |\n| `color` | `string` | — | Custom progress-bar colour (any CSS colour) |\n| `footerNote` | `ReactNode` | — | Footer text under the body (shown while `duration > 0`) |\n| `showFooterNote` | `boolean` | `false` | Show the default \"This message will close in N seconds. Click to stop.\" footer |\n| `className` | `string` | — | Classes merged onto the toast root |\n\nOther props (`aria-*`, `data-*`, event handlers) are forwarded to the root; your `onMouseEnter`, `onFocus`, `onPointerDown`… run alongside the toast's own handlers.\n\n### Behavior\n\n- **Timer pause**: the auto-dismiss timer pauses while the pointer is over the toast, while focus is inside it, while a finger is pressed on it, during a swipe, and after \"Click to stop.\".\n- **Swipe to dismiss**: drag left or right. Past 64px (or on a flick) the toast slides out that way and closes; otherwise it springs back. A press only becomes a swipe after a few pixels, so taps on the action and close buttons still click.\n- **Phones**: on a coarse-pointer screen under 640px the toast (and the stack) moves to `mobilePosition`, full width with a 1rem gutter, above `env(safe-area-inset-bottom)` and `mobileOffset`. Set `mobileOffset={16}` when the app has no FootNav, or set `--dt-toast-mobile-offset` in CSS.\n- **Theming**: role tokens (`bg-surface`, `text-foreground`, `text-muted`, `border-line`) and the status palettes, so dark mode and re-theming follow `--dt-*` variables.\n\n### Accessibility\n\n- Each toast is a live region: `type=\"error\"` uses `role=\"alert\"` + `aria-live=\"assertive\"`; every other type uses `role=\"status\"` + `aria-live=\"polite\"` (both `aria-atomic`). Your own `role` / `aria-live` props win.\n- The stack is a `role=\"region\"` landmark named \"Notifications\".\n- The close button has an `aria-label` (`closeLabel`), a focus-visible ring and a 44×44px hit area.\n- The enter, exit, swipe and stack animations are off under `prefers-reduced-motion`.\n\n## ToastContainer\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `toasts` | `Array` | `[]` | Array of toast objects `{ id, title, message, description, type, actions, ... }` |\n| `position` | `string` | `\"top-right\"` | Stacking position on screen |\n| `onRemove` | `function` | — | Callback invoked when a toast is dismissed (passes toast `id`) |\n| `className` | `string` | — | Classes for the stacking container |\n| `expand` | `boolean` | — | Unset: collapse into a peek stack once there are more than `visibleToasts`, expanding on hover, focus or tap. `true`: always list every toast. `false`: collapse whenever there is more than one |\n| `visibleToasts` | `number` | `3` | Toasts listed before the stack collapses, and the number that peek while collapsed |\n| `mobilePosition` | `string` or `false` | `\"bottom-center\"` | Stack placement on phones: `bottom-center` or `top-center`; `false` keeps `position` |\n| `mobileOffset` | `number` or `string` | `\"5rem\"` | Gap above the bottom edge on phones (sets `--dt-toast-mobile-offset`) |\n\nThe stack's height is capped at `100dvh - 2rem` and scrolls without a visible scrollbar. While collapsed, the newest toast sits in front and older ones peek behind it, slightly smaller; hovering, focusing (keyboard) or tapping the stack expands it and pauses every toast's timer until the pointer leaves, focus leaves, or you tap elsewhere.\n\nUnder a `ToastProvider`, a `ToastContainer` handed the provider's own `toasts` renders nothing (the provider already renders them), so existing screens don't double-render while you migrate.\n\n## ToastProvider\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Your app |\n| `position` | `string` | `\"top-right\"` | Stacking position of the provider's container |\n| `className` | `string` | — | Classes for the provider's stacking container |\n| `expand` | `boolean` | — | Same as `ToastContainer` |\n| `visibleToasts` | `number` | `3` | Same as `ToastContainer` |\n| `mobilePosition` | `string` or `false` | `\"bottom-center\"` | Same as `ToastContainer` |\n| `mobileOffset` | `number` or `string` | `\"5rem\"` | Same as `ToastContainer` |\n\n## useToast\n\nReturns `{ toast, toasts, addToast, removeToast, clearToasts }`. `toast(options)` or `toast.success|error|warning|info|default(title, message?, options?)` enqueue a toast and return its id.\n\n- **Inside `<ToastProvider>`**: every `useToast()` call shares the provider's queue.\n- **Outside a provider**: each hook call owns its own queue — render it with `<ToastContainer toasts={toasts} onRemove={removeToast} />`. (Unlike `useTheme`, `useToast` does not throw outside its provider; this keeps existing code working.)\n\n## Examples\n\n### App-level provider (recommended)\n\n```jsx\n// main.jsx\n<ToastProvider position=\"top-right\">\n <App />\n</ToastProvider>\n\n// anywhere below it\nfunction SaveButton() {\n const { toast } = useToast();\n return <Button onClick={() => toast.success('Saved', 'Your changes are saved.')}>Save</Button>;\n}\n```\n\n### Stack and phone options\n\n```jsx\n// Always list every toast; on phones sit 1rem above the bottom (no FootNav).\n<ToastProvider position=\"bottom-right\" expand mobileOffset={16}>\n <App />\n</ToastProvider>\n\n// Collapse as soon as two toasts are showing, and keep the desktop corner on phones.\n<ToastProvider expand={false} mobilePosition={false}>\n <App />\n</ToastProvider>\n```\n\n### Standalone (no provider)\n\n```jsx\nfunction App() {\n const { toast, toasts, removeToast } = useToast();\n\n const handleShowToast = () => {\n toast.info('System Update', 'A new update is available for download.', {\n duration: 6000,\n actions: [\n { label: 'Update Now', variant: 'primary', onClick: () => alert('Updating...') },\n { label: 'Later', variant: 'secondary', onClick: () => alert('Postponed') },\n ],\n });\n };\n\n return (\n <>\n <Button onClick={handleShowToast}>\n Show Notification\n </Button>\n <ToastContainer toasts={toasts} onRemove={removeToast} position=\"top-right\" />\n </>\n );\n}\n```\n\n[← Component overview](README.md)\n",
|
|
6531
|
+
"Tooltip": "# Tooltip\n\nTooltip shown on hover, focus, or click — and on long-press on touch screens. Supports placement (auto/top/bottom/left/right), delay, offset, and optional portal. Positioned by the shared floating engine (`position: fixed`, flips and shifts to stay in view) in the `z-tooltip` layer. Content can be string or render prop `({ close }) => ReactNode`. Tooltip has no sheet presentation: for interactive content on phones use [`Popover`](Popover.md).\n\n## Import\n\n```jsx\nimport { Tooltip } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `position` | `\"auto\"` \\| `\"top\"` \\| `\"bottom\"` \\| `\"left\"` \\| `\"right\"` | `\"auto\"` | Preferred side. `auto` means top; every side flips to the opposite side when there is no room and shifts along its axis to stay 8px inside the viewport. The resolved placement is on the tooltip as `data-placement`. |\n| `trigger` | `\"hover\"` \\| `\"click\"` \\| `\"focus\"` \\| `Array` | `\"hover\"` | How tooltip opens. `hover` also opens on keyboard focus (unless `click` is in the list), and on touch screens `hover` / `focus` open on a long-press (see **Touch**). |\n| `content` | `ReactNode` \\| `({ close }) => ReactNode` | — | Tooltip content. |\n| `customTrigger` | `ReactElement` | — | Element used directly as the trigger (gets the ref + `aria-describedby`; no wrapper span). Use it to avoid an extra tab stop around focusable components. |\n| `children` | `ReactNode` | — | Trigger content when `customTrigger` is not used. Wrapped in a `<span>`; see Tab stops below. |\n| `open` | `boolean` | — | Controlled open state. |\n| `defaultOpen` | `boolean` | `false` | Uncontrolled initial open. |\n| `onChange` | `(open: boolean) => void` | — | Called when open state changes. |\n| `delay` | `number` | `80` | Delay in ms before opening (hover). |\n| `offset` | `number` | `8` | Spacing in px between trigger and tooltip. |\n| `textClass` | `string` | `\"text-foreground\"` | Tailwind class for the tooltip text color (role token, flips in dark mode). |\n| `bgClass` | `string` | `\"bg-surface\"` | Tailwind class for the tooltip background (role token, flips in dark mode). |\n| `className` | `string` | `\"\"` | Tooltip box CSS classes. |\n| `portal` | `boolean` | `true` | Render tooltip in document.body. With `false` it renders inline; it is still `position: fixed`, so avoid ancestors with `transform`/`filter` (they become its containing block). |\n| `style` | `CSSProperties` | — | Merged over the computed position styles on the tooltip box. |\n| `id` | `string` | — | Tooltip id. If omitted, a `React.useId()`-based id is generated, stable across server rendering and hydration. |\n\n## Touch\n\nOn a touch pointer (`pointerType: \"touch\"`), `hover` and `focus` tooltips:\n\n- **open on a long-press** (500 ms, finger held still within 10px). The OS context menu / callout is suppressed for that press, and the click a long-press may produce is swallowed;\n- **ignore a plain tap**: the emulated `mouseenter` / focus after a tap do not open the tip, so the tap only activates the trigger's own `onClick`. A tap on the trigger while the tip is open closes it;\n- **never stick open**: the tip hides 1.5 s after the finger lifts, or at once on a tap anywhere outside the trigger and the tooltip.\n\n`trigger=\"click\"` tooltips keep tap-to-toggle.\n\n## Hover and dismissal\n\n- Leaving the trigger closes a hover tooltip after a 100 ms grace period, so the pointer can move onto the tooltip; hovering the tooltip keeps it open (WCAG 1.4.13).\n- `Escape` closes any open tooltip; a pointer-down outside closes it too.\n\n## Tab stops: `customTrigger` vs `children`\n\nWhen you pass `children`, Tooltip wraps them in a `<span>`. If that span were\nalways focusable, a tooltip around a button would give keyboard users **two**\ntab stops (span, then button) — painful in table rows, toolbars and grids.\n\n- **A single native focusable child** — an enabled `<button>`, `<a href>`,\n `<input>`, `<select>`, `<textarea>`, or any element with `tabIndex >= 0` — is\n detected and the span gets **no** `tabIndex`; the child itself receives\n `aria-describedby` (merged with any it already has). One tab stop.\n- **Anything else** (text, an icon, a disabled button, several children, or a\n *component* such as `<Button>` that Tooltip cannot inspect) keeps the\n `tabIndex=\"0\"` wrapper so the tooltip stays keyboard-reachable. This is what\n makes tooltips on disabled buttons work.\n- **For components (e.g. `<Button>`, `<Badge>`), prefer `customTrigger`.** It\n attaches the ref and `aria-describedby` to your element with no wrapper and\n no extra tab stop:\n\n```jsx\n// Grid / table row: one tab stop per action\n<Tooltip content=\"Edit row\" customTrigger={<Button size=\"sm\">Edit</Button>} />\n\n// Native button child: also a single tab stop\n<Tooltip content=\"Delete\">\n <button type=\"button\">Delete</button>\n</Tooltip>\n\n// Disabled control: wrapper stays focusable so the reason is reachable\n<Tooltip content=\"Uploads are not available yet\">\n <button type=\"button\" disabled>Add</button>\n</Tooltip>\n```\n\n## Example\n\n```jsx\n<Tooltip content=\"Save your changes\" customTrigger={<Button>Save</Button>} />\n\n<Tooltip trigger=\"click\" content={({ close }) => <span>Click to close <button onClick={close}>OK</button></span>}>\n <span>Click me</span>\n</Tooltip>\n\n<Tooltip position=\"bottom\" delay={200} content=\"Delayed tip\">\n <Input placeholder=\"Focus me\" />\n</Tooltip>\n```\n\n[← Component overview](README.md)\n",
|
|
6532
|
+
"Tour": "# Tour\n\nStep-based onboarding tour (coach marks): dims the page, spotlights each step's target element and shows a step card beside it with Back / Next / Skip / Finish. Generalises the `SpotlightTour` that TaskPro and ProcurementPro each hand-rolled. A step without a target — or whose target never renders — shows as a centred card, so the tour never stalls. On phones the card docks at the bottom of the screen and steps can be swiped.\n\n## Import\n\n```jsx\nimport { Tour } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `open` | `boolean` | `false` | Show the tour |\n| `steps` | `array` | `[]` | `TourStep[]`: `{ target, title, content, placement }` (see below) |\n| `onClose` | `function` | — | `(reason) => void`; reason is `skip`, `escape`, `close` or `finish`. Set `open` to false here |\n| `onFinish` | `function` | — | Called by the Finish button on the last step (before `onClose('finish')`) |\n| `step` | `number` | — | Controlled step index |\n| `defaultStep` | `number` | `0` | Uncontrolled starting step; the tour restarts here every time it opens |\n| `onStepChange` | `function` | — | `(index, step) => void`; use it to navigate routes before a step shows |\n| `labels` | `object` | — | Override `next`, `back`, `skip`, `finish`, `close` and `progress(current, total)` |\n| `targetTimeout` | `number` | `3000` | How long (ms) to wait for a step's target before falling back to a centred card |\n| `spotlightPadding` | `number` | `6` | Space in px between the target and the spotlight ring |\n| `presentation` | `\"auto\"`, `\"popover\"` or `\"sheet\"` | `\"auto\"` | `sheet` docks the step card at the bottom of the screen; `popover` anchors it beside the target; `auto` docks on small touch screens (`(pointer: coarse) and (max-width: 639px)`) and anchors elsewhere (anchored on the server and while hydrating) |\n| `className` | `string` | — | Extra classes for the step card (merged with `cn`) |\n| `...rest` | `HTMLAttributes` | — | Forwarded to the step card; `ref` points at it. Your `onPointerDown` / `onPointerUp` / `onPointerCancel` / `onClickCapture` run before the swipe handling |\n\n## Step shape\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `target` | `string, ref, element or function` | CSS selector (e.g. `[data-tour=\"new-task\"]`), a React ref, a DOM element, or `() => element`. Omit for a centred intro/outro card |\n| `title` | `ReactNode` | Card heading; labels the dialog |\n| `content` | `ReactNode` | Card body; describes the dialog |\n| `placement` | `string` | `top`, `bottom` (default), `left`, `right` (each optionally `-start` / `-end`) or `center`; flips when there is no room and shifts to stay in view. Ignored while docked |\n\n## Examples\n\n### Basic\n\n```jsx\nconst steps = [\n { title: 'Welcome', content: 'A 30-second tour of your workspace.' },\n { target: '[data-tour=\"new-task\"]', title: 'Capture work', content: 'New task opens a quick form.', placement: 'bottom' },\n { target: searchRef, title: 'Jump anywhere', content: 'Search tasks and projects.', placement: 'right' },\n];\n\nconst [open, setOpen] = useState(false);\n\n<Button onClick={() => setOpen(true)}>Take the tour</Button>\n<Tour open={open} steps={steps} onClose={() => setOpen(false)} onFinish={() => markTourSeen()} />\n```\n\n### Route-aware (react-router)\n\n```jsx\n<Tour\n open={open}\n steps={steps}\n onStepChange={(i, step) => step.route && navigate(step.route)}\n onClose={() => setOpen(false)}\n/>\n```\n\nExtra fields on a step (like `route`) are passed back to you untouched. The tour waits up to `targetTimeout` for the next screen's target to render.\n\n### Phones (docked card)\n\n```jsx\n// Default: docked on phones, anchored on desktop\n<Tour open={open} steps={steps} onClose={() => setOpen(false)} />\n\n// Always docked (e.g. a mobile-only shell)\n<Tour presentation=\"sheet\" open={open} steps={steps} onClose={() => setOpen(false)} />\n```\n\nWhile docked:\n\n- the card spans the screen width minus a 1rem gutter, sits 1rem above the bottom edge with `pb-safe` for the home indicator, and scrolls inside `max-h-[60dvh]`;\n- the spotlight stays on the target; the card carries `data-placement=\"docked\"` and `data-presentation=\"sheet\"`;\n- swiping the card **left goes to the next step, right goes back** (at least 50px, clearly more horizontal than vertical; vertical drags still scroll the card). A swipe that ends on a button does not also press it;\n- the close button is a 44 × 44px touch target on coarse pointers.\n\nThe anchored card is capped at `calc(100dvh - 2rem)` (dynamic viewport units, so the mobile browser toolbar never clips it).\n\n## Accessibility & keyboard\n\n| Behavior | Detail |\n|----------|--------|\n| Role | The **step card** is `role=\"dialog\"` + `aria-modal=\"true\"`, labelled by the step title and described by its content (the full-screen layer has no role, so global `[role=dialog]` styles never paint over the page) |\n| Focus | Moves to the card on open and on every step change; `Tab` is trapped in the card; focus returns to the opener on close |\n| `Escape` | Closes with reason `escape` |\n| `ArrowRight` / `ArrowLeft` | Next / previous step (ignored while typing in a field inside the card) |\n| Scroll into view | The target is scrolled into view through its nearest scrolling ancestor only (never `overflow: hidden` shells); smooth unless `prefers-reduced-motion` |\n| Reduced motion | Spotlight and card transitions are disabled under `prefers-reduced-motion` |\n| Scroll lock | Body scroll is locked while open |\n| Progress | \"Step 2 of 5\" text is visible and read with the card |\n| Dark mode | Card uses `dark:` variants; the spotlight ring switches to `primary-400` |\n\n[← Component overview](README.md)\n",
|
|
6533
|
+
"useApi": "# useApi\n\nThin axios wrapper hook. Sends requests by URL (`fetchData`) or by key from an `apiMap` (`fetchApi` / `fetchApiWith`) and tracks `data`, `error`, `loading` and `lastRequest`. It is not a cache/dedup layer — bring React Query or SWR for that.\n\n## Import\n\n```jsx\nimport { useApi } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\nOptions object passed to `useApi(options)`.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `BASE_URL` | `string` | `null` | axios `baseURL` |\n| `DEFAULT_HEADERS` | `object` | `{}` | Headers sent with every request |\n| `apiMap` | `Record<string, (...args) => { url, method, headers?, data? }>` | `{}` | Named request builders. Entries may take any number of positional arguments |\n| `createRequestInstance` | `function` | `axios.create` | `(baseURL, defaultHeaders) => AxiosInstance` |\n| `sendRequest` | `function` | — | `(instance, method, url, data, headers) => Promise<AxiosResponse>`; GET/HEAD/DELETE send no body |\n\n## Returns\n\n| Field | Type | Description |\n|------|------|-------------|\n| `data` | `any` | Last successful `response.data` |\n| `error` | `Error \\| null` | Last error |\n| `loading` | `boolean` | True while a request with `needLoading` is in flight |\n| `lastRequest` | `string \\| null` | Key of the last `fetchApi` / `fetchApiWith` call |\n| `fetchData(url, method, data?, headers?, { needLoading? })` | `Promise` | Send a request directly |\n| `fetchApi(apiKey, data?, needLoading = true)` | `Promise` | Call `apiMap[apiKey](data)` — **exactly one argument** — and send the result |\n| `fetchApiWith(apiKey, args = [], { needLoading = true })` | `Promise` | Call `apiMap[apiKey](...args)` — every element of `args` is a positional argument — and send the result |\n\n### fetchApi vs fetchApiWith\n\n`fetchApi` always calls the map entry with ONE argument. An entry written as `(id, body) => ({...})` called through `fetchApi('updateUser', id)` gets `body === undefined`, so nothing is sent. Use `fetchApiWith` for multi-argument entries:\n\n```jsx\nfetchApiWith('updateUser', [id, body]); // apiMap.updateUser(id, body)\nfetchApiWith('updateUser', [id, body], { needLoading: false });\n```\n\n`fetchApi` is unchanged; existing single-argument callers keep working.\n\n## Examples\n\n```jsx\nconst apiMap = {\n getUser: (id) => ({ url: `/users/${id}`, method: 'GET' }),\n updateUser: (id, body) => ({ url: `/users/${id}`, method: 'PUT', data: body }),\n};\n\nfunction Profile({ id }) {\n const { data, loading, error, fetchApi, fetchApiWith } = useApi({\n BASE_URL: 'https://api.example.com',\n DEFAULT_HEADERS: { Authorization: `Bearer ${token}` },\n apiMap,\n });\n\n useEffect(() => { fetchApi('getUser', id); }, [id]);\n\n const save = (body) => fetchApiWith('updateUser', [id, body]);\n // …\n}\n```\n\n[← Component overview](README.md)\n",
|
|
6534
|
+
"useHaptics": "# useHaptics\n\nThin, opt-in wrapper around `navigator.vibrate` with named patterns. It does nothing where vibration is unsupported (iOS Safari, desktop Safari), on the server, while hydrating, and while the user asks for reduced motion, so you can call it unconditionally.\n\n## Import\n\n```jsx\nimport { useHaptics } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\nOptions object passed to `useHaptics(options)`.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `enabled` | `boolean` | `true` | Gates every `trigger` call; pass your own opt-in flag here |\n\n## Returns\n\n| Field | Type | Description |\n|------|------|-------------|\n| `trigger(pattern = 'selection')` | `(pattern?: HapticPattern) => boolean` | Requests a vibration. `pattern` is a name (below), a duration in ms, or a vibrate/pause array. Returns `true` when a vibration was requested, `false` when it was a no-op (unsupported, reduced motion, `enabled: false`, unknown name, or the browser refused) |\n| `supported` | `boolean` | `true` when the device exposes `navigator.vibrate`. Always `false` on the server and during hydration |\n| `reducedMotion` | `boolean` | Mirrors `prefers-reduced-motion` live; `trigger` is a no-op while it is `true` |\n\n### Patterns\n\n| Name | Pattern (ms) | Use for |\n|------|--------------|---------|\n| `selection` | `10` | A value changed: toggles, checkboxes, snap points |\n| `success` | `[10, 40, 10]` | An action completed |\n| `warning` | `[20, 60, 20]` | About to do something destructive |\n| `error` | `[30, 40, 30, 40, 30]` | An action failed |\n\n## Behavior\n\n- **Never on by default.** Components that use it (`Switch`, `Checkbox`, `ConfirmDialog`, `Sheet`) take a `haptics` prop that defaults to `false`.\n- **Never the only signal.** Haptics confirm what the UI already shows; most devices (all of iOS) never vibrate.\n- Browsers ignore `vibrate()` before the first user interaction on the page; `trigger` then returns `false`.\n\n## Examples\n\n```jsx\nfunction SaveButton({ onSave }) {\n const { trigger } = useHaptics();\n return (\n <Button\n onClick={async () => {\n try {\n await onSave();\n trigger('success');\n } catch {\n trigger('error');\n }\n }}\n >\n Save\n </Button>\n );\n}\n```\n\n```jsx\n// Built-in opt-in on components\n<Switch haptics rightLabel=\"Wi-Fi\" />\n<ConfirmDialog haptics destructive isOpen={open} title=\"Delete invoice?\" confirmLabel=\"Delete\" onConfirm={remove} onCancel={close} />\n```\n\n[← Component overview](README.md)\n"
|
|
5402
6535
|
},
|
|
5403
|
-
"skill": "# Dreamtree UI — AI assistant reference\n\n> Skill installed by `npx @dreamtree-org/twreact-ui init --ai <provider>`.\n> Source of truth: [`@dreamtree-org/twreact-ui`](https://www.npmjs.com/package/@dreamtree-org/twreact-ui).\n> Re-run the installer to refresh this block when the library updates.\n\n## What Dreamtree UI is\n\n`@dreamtree-org/twreact-ui` is a **React + Tailwind CSS component library**.\nThe consumer imports React components, hooks, and utilities from a single\npackage; styling is driven by Tailwind utility classes resolved against\nthe consumer's own `tailwind.config.js`. Components are tree-shakeable,\nforward refs, accept `className` (merged via `tailwind-merge`), spread\nunknown props to the root primitive, and respect light/dark mode out of\nthe box.\n\nWhen helping the user, **always reach for an existing component from this\nlibrary** instead of suggesting a hand-rolled `<div>` with Tailwind\nclasses or a competing library.\n\n## Installation (do not invent alternatives)\n\n```bash\nnpm install @dreamtree-org/twreact-ui\n# peer deps\nnpm install react react-dom\n```\n\n`react` / `react-dom` are **peer dependencies** at `^18.3.1`. React 19\nis not yet supported.\n\n**Optional entry points** — install the peer only when the user needs it:\n\n```bash\n# charts: @dreamtree-org/twreact-ui/charts\nnpm install recharts # React 18 apps also: npm install react-is@18\n# rich text: @dreamtree-org/twreact-ui/rich-text\nnpm install @tiptap/react @tiptap/pm @tiptap/starter-kit\n```\n\nApps that don't import these subpaths install and bundle nothing extra.\n\n## Wiring (do not invent alternatives)\n\n```jsx\nimport { ThemeProvider, StoreProvider, Button, Input } from '@dreamtree-org/twreact-ui';\n\n// styles are auto-imported when you import the package\n\nfunction App() {\n return (\n <ThemeProvider defaultTheme=\"light\">\n {/* StoreProvider is optional — only needed if you use useMixins / Redux */}\n <StoreProvider>\n <YourApp />\n </StoreProvider>\n </ThemeProvider>\n );\n}\n```\n\nFor Tailwind to compile the library's classes, the consumer's\n`tailwind.config.js` `content[]` MUST include the package:\n\n```js\n// consumer's tailwind.config.js\nmodule.exports = {\n content: [\n './src/**/*.{js,jsx,ts,tsx}',\n './node_modules/@dreamtree-org/twreact-ui/dist/**/*.{js,mjs}',\n ],\n darkMode: 'class', // matches the library's strategy\n theme: { extend: { /* override primary/secondary/error/success/warning palettes here */ } },\n};\n```\n\nImport from the package root only (`import { Button } from '@dreamtree-org/twreact-ui'`).\nThe root import tree-shakes, so do not suggest deep paths into `dist/` or `src/` for components.\n\n**Next.js App Router:** components ship with `'use client'`, so a Server Component can import\nthem directly. Do not generate a `\"use client\"` wrapper file that re-exports the library.\nCompound members (`Card.Header`, `Tabs.List`, `Dialog.Footer`) and function props (`onClick`)\nstill require a `'use client'` file. `cn`, `Helpers` and `Emitter` also work on the server.\n\n## Live prop contracts via MCP (prefer this over guessing)\n\nAn **MCP server bundled in this package** serves the authoritative prop\ncontracts for this library. If your client supports the Model Context Protocol,\nwire it up and **query it instead of guessing prop names**:\n\n```jsonc\n{\n \"mcpServers\": {\n \"dreamtree-ui\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@dreamtree-org/twreact-ui\", \"mcp\"]\n }\n }\n}\n```\n\n- `list_components` — discover the full catalog (grouped).\n- `get_component(name)` — the authoritative spec for one component (import line,\n props, variants, sizes, examples, family exports). **Call this before using a\n component.** Accepts a family-export name (`useToast` → `Toast`).\n- `search_components(query)` — keyword search by capability.\n\nResources: `dreamtree://skill` (this guide) and `dreamtree://docs/<Component>`.\nPrompt: `compose_ui`. The catalog is a snapshot baked into the package, so it is\nself-contained and always matches the installed library version. The lists below\nare the fallback when the MCP server isn't connected.\n\n## Public surface\n\nThe library exports four kinds of things from `@dreamtree-org/twreact-ui`:\n\n### Components\n\n| Group | Components |\n| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **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` |\n| **Navigation** | `Sidebar`, `Navbar`, `FootNav`, `Breadcrumbs` |\n| **Feedback** | `Dialog`, `Toast` (+ `ToastProvider`, `ToastContainer`, `useToast`), `Alert`, `Drawer` (side sheet; `isOpen`/`onClose` like Dialog), `Tour` (onboarding spotlight) |\n| **Utility** | `Badge`, `Avatar`, `Card`, `Pagination`, `Stepper`, `FileUpload`, `Condition`, `Carousel`, `Sortable` (drag + keyboard reorder) |\n| **Blocks** | `AppShell`, `PageHeader`, `StatCard`, `StatusPill`, `EmptyState`, `ErrorState`, `LoadingState`, `ConfirmDialog` — page-level patterns; build pages from these before composing layouts by hand |\n| **Charts** (`@dreamtree-org/twreact-ui/charts`, peer `recharts`) | `LineChart`, `BarChart`, `AreaChart`, `DonutChart`, `Sparkline` — NOT exported from the root |\n| **Rich text** (`@dreamtree-org/twreact-ui/rich-text`, peers `@tiptap/react @tiptap/pm @tiptap/starter-kit`) | `RichTextEditor` — NOT exported from the root |\n\n### Hooks\n\n| Hook | Purpose |\n| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |\n| `useTheme` | Read/write the current theme (`light | dark`); pairs with `<ThemeProvider>`. Throws if used outside the provider. |\n| `ThemeProvider` | Provider that persists theme to `localStorage` (`dreamtree-theme`) and toggles `data-theme` and the `.dark` class on `<html>`. |\n| `useApi` | Thin axios wrapper. Returns `{data, error, loading, lastRequest, fetchData, fetchApi, fetchApiWith}`. Accepts `BASE_URL`, `DEFAULT_HEADERS`, `apiMap`. |\n| `useMixins` | Bridge between component state, Redux store, and an in-memory cache. Power-user hook — prefer local React state for new code. |\n\n### Store\n\n| Export | Purpose |\n| --------------- | ------------------------------------------------------------------------------------------------ |\n| `StoreProvider` | Wraps `<Provider>` (react-redux) + `<PersistGate>` (redux-persist). Boots a slice + listener mw. |\n\n### Utils\n\n| Export | Signature | Use for |\n| --------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ |\n| `cn` | `cn(...inputs: ClassValue[]): string` | Merge Tailwind class strings (`twMerge(clsx(...))` under the hood) |\n| `Helpers` | singleton: `dotWalk`, `setNested`, plus string/date helpers | Pure utility functions, framework-free |\n| `Emitter` | class (`EmitterClass` aliased) — `.on`, `.off`, `.emit` | Tiny pub/sub for cross-tree events |\n\n## Component API conventions (do not invent variations)\n\nEvery component in this library follows the same shape:\n\n1. **`forwardRef`** when wrapping a single DOM element. Consumers may attach refs.\n2. **`className` is merged** via `cn(...)` — caller classes win on conflict.\n3. **`...rest` props pass through** to the root primitive (button, input, …).\n4. **Defaults via destructuring**, e.g. `variant = 'primary', size = 'md'`.\n5. **Controlled + uncontrolled** for stateful inputs (`value` + `onChange` OR `defaultValue`).\n6. **Theming is class-based**, never via a `color` prop. Re-skin via Tailwind config.\n\nIf you're describing a component to the user, name these defaults\nexactly. Do not propose a different shape (e.g. inventing a `color` prop\non `Button` or suggesting `tw` prop merging).\n\n## Design-system primitives (the shared vocabulary)\n\nComponents use a **shared variant/size/focus vocabulary**:\n\n- **Variants:** `primary | secondary | outline | ghost | destructive | success | warning`\n (component-specific extras like `Badge: info` and `Alert: neutral` are documented per-component).\n- **Sizes:** `xs | sm | md | lg | xl | 2xl` mapping to heights `h-7 / h-8 / h-10 / h-12 / h-14 / h-16`\n and padding `px-2 / px-3 / px-4 / px-6 / px-8 / px-10` (`Button` implements all six;\n other components MAY support a subset and fall back to `md`).\n- **Focus ring:** `focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500`\n (the color token swaps for destructive / etc.).\n- **Dark mode:** Tailwind `darkMode: 'class'`. Every color choice has a `dark:` counterpart.\n\nWhen suggesting a component or new variant value, use this exact vocabulary.\n\n## Page blocks and design tokens\n\n**Blocks** are page-level patterns extracted from the products, so screens stop re-inventing them:\n\n- `AppShell` — sidebar + top bar + mobile drawer; pass `nav` items and `linkAs={Link}` in Next.js.\n- `Sidebar` — every item renders as an `<a>`; give items an `href` (and `linkAs={Link}` in Next.js) so Ctrl/Cmd-click opens a new tab. Parents with `href` navigate + expand; the chevron toggles. Mark the current page `active: true` and its group opens on load (control groups with `expandedIds`/`defaultExpandedIds`/`onExpandedChange`). Top-level `group: 'Name'` adds section headings; `badge` (+ `badgeLabel: '3 unread'`) shows counts; `footer` pins links above the user block; `stickyOffset={64}` when a top bar sits above it. The mobile drawer (`isMobileOpen`/`setIsMobileOpen`) is inert when shut and closes on Escape.\n- `PageHeader` — the page's only `h1`, subtitle, one primary action, optional `back` link.\n- `StatCard` — KPI tile (`label`, `value`, `icon`, `tone`, `change`).\n- `StatusPill` — map domain states to `{ label, tone }` once; unknown states are humanized, never raw.\n- `EmptyState`, `ErrorState`, `LoadingState` — every data card needs all three.\n- `ConfirmDialog` — use instead of `window.confirm()`; title is the question, button is the verb.\n\n**Interaction primitives** — reach for these before hand-rolling the pattern:\n\n- `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`.\n- `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.\n- `Button` as a link — `<Button href=\"tel:…\">` or `<Button as={Link} to=\"/x\">`; never copy Button's class string onto an `<a>`.\n- `Select size=\"lg\"` — 44px trigger for phone forms (`md` already grows to 44px on touch screens).\n- `Stepper` steps take `title` (or its alias `label`). `Radio` follows the primary token by default — don't pass `radioColor` to match the brand.\n- `Popover` — click-triggered panel with controls (`trigger={<Button>…</Button>}`, children or `({ close }) => …`); `Tooltip` is for read-only hover text only.\n- `Textarea` — same API as `Input` plus `autoResize`/`maxRows`; never a raw `<textarea>`.\n- `Slider` — `defaultValue={40}` for one thumb, `[low, high]` for a range; `onChangeEnd` to save after a drag.\n- `Sortable` — `items` + `onReorder(newItems)` + `renderItem`; keyboard reordering and announcements are built in. Pass `handle` and spread `handleProps` on a grip for touch screens.\n- 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.\n- `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.\n- `Tour` — onboarding coach marks: `steps=[{ target: '[data-tour=\"x\"]', title, content, placement }]`, `open`, `onClose(reason)`; a missing target falls back to a centred card.\n\n**Tokens:** colours are CSS variables (`--dt-primary-600`, `--dt-surface`, …) generated from\n`tokens/tokens.json`. Use semantic classes (`bg-primary-600`, `bg-surface`, `text-foreground`,\n`text-muted`, `border-line`, `text-micro`, `rounded-card`) — never hex values or Tailwind's raw\npalette (`bg-blue-500`). A product's brand lives in its own repo as `dreamtree.brand.json`;\n`npx @dreamtree-org/twreact-ui brand build --out app/brand.css` turns it into the CSS to load after\ntwreact-ui's CSS. To change a product's colours, edit that file, never the components.\nThe full rule set for screens is `DESIGN.md` in the package root.\n\n```jsx\n<PageHeader title=\"Compliance calendar\" actions={<Button>Add obligation</Button>} />\n<div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <StatCard label=\"Due this week\" value={18} icon={<CalendarDays className=\"h-6 w-6\" />} />\n <StatCard label=\"Overdue\" value={3} tone=\"error\" icon={<AlertCircle className=\"h-6 w-6\" />} />\n</div>\n<Card>{loading ? <LoadingState /> : error ? <ErrorState onRetry={retry} /> : rows.length ? <Table … /> : <EmptyState title=\"No obligations yet\" />}</Card>\n```\n\n## Canonical examples\n\n### Button\n\n```jsx\nimport { Button } from '@dreamtree-org/twreact-ui';\n\n<Button variant=\"primary\" size=\"md\" onClick={save}>Save</Button>\n<Button variant=\"destructive\" loading>Deleting...</Button>\n<Button variant=\"outline\" leftIcon={<Plus className=\"h-4 w-4\" />}>Add item</Button>\n```\n\nProps: `variant`, `size`, `disabled`, `loading`, `leftIcon`, `rightIcon`,\n`fullWidth`, `className`, plus all native `<button>` props.\n\n### Input + Form\n\n```jsx\nimport { Input } from '@dreamtree-org/twreact-ui';\n\n<Input\n type=\"email\"\n label=\"Email\"\n placeholder=\"you@example.com\"\n required\n clearable\n error={errors.email?.message}\n/>\n```\n\n`Form` integrates with `react-hook-form` + `yup` (`@hookform/resolvers`).\nValidation rules live in consumer code.\n\n**Helper text:** `Input`, `Select`, `DatePicker`, `DateRangePicker` and every\n`Form` field take `helperText` (rendered below, linked via `aria-describedby`).\nWhile `error` is set it **replaces** the helper text — don't render both. Don't\nhand-roll a `<p>` hint under a field.\n\n```jsx\n<Select label=\"Role\" helperText=\"Controls access.\" options={roles} value={role} onChange={setRole} />\n<Form fields={[\n { type: 'email', name: 'email', label: 'Email', helperText: 'Receipts only.' },\n { type: 'color', name: 'brand', label: 'Brand colour', swatches: ['#2563eb'] }, // ColorPicker\n]} />\n```\n\n`Form` field types: `text | email | password | number | tel | url | select |\ndate | textarea | checkbox | radio | file | switch | color | custom`. Use\n`type: 'color'` rather than a `custom` field wrapping `ColorPicker`.\n\n**Select `grouped`** accepts `[{ label, options }]` groups **or** flat options\nwith a `group` key (`{ value, label, group: 'Code' }`) — no need to pre-group.\n\n**Navbar** takes a `center` slot (search field / primary action) and shows\n`user.name` initials when `user.avatar` is missing; it fits a 390px phone.\n\n**Tooltip** around a component (e.g. `<Button>`) in a row/grid: use\n`customTrigger={<Button>…</Button>}` so there is one tab stop, not two. A lone\nnative `<button>` / `<a href>` / `<input>` child is detected automatically.\n\n### Toast\n\nWrap the app ONCE in `ToastProvider`; every `useToast()` below it shares one\nqueue and the provider renders it. (Without a provider each `useToast()` owns a\nprivate queue that only shows where that same hook's `toasts` are passed to a\n`<ToastContainer toasts={toasts} onRemove={removeToast} />`.)\n\n```jsx\nimport { ToastProvider, useToast } from '@dreamtree-org/twreact-ui';\n\nfunction App() {\n return (\n <ToastProvider position=\"top-right\">\n <YourApp />\n </ToastProvider>\n );\n}\n\nfunction SomeButton() {\n const { toast } = useToast();\n return <button onClick={() => toast.success('Saved!')}>Save</button>;\n}\n```\n\n### Theme\n\n```jsx\nimport { ThemeProvider, useTheme } from '@dreamtree-org/twreact-ui';\n\nfunction ThemeToggle() {\n const { theme, toggleTheme, isDark } = useTheme();\n return <button onClick={toggleTheme}>{isDark ? '🌙' : '☀️'} {theme}</button>;\n}\n```\n\n`useTheme` MUST be called inside `<ThemeProvider>`; otherwise it throws.\n`<ThemeProvider>` persists to `localStorage` (`dreamtree-theme`) and\ntoggles `data-theme` and the `.dark` class on `<html>`.\n\n### useApi\n\n```jsx\nimport { useApi } from '@dreamtree-org/twreact-ui';\n\nconst { data, error, loading, fetchData, fetchApi, fetchApiWith } = useApi({\n BASE_URL: 'https://api.example.com',\n DEFAULT_HEADERS: { Authorization: `Bearer ${token}` },\n apiMap: {\n getUser: (id) => ({ url: `/users/${id}`, method: 'GET' }),\n updateUser: (id, body) => ({ url: `/users/${id}`, method: 'PUT', data: body }),\n },\n});\n\nuseEffect(() => { fetchData('/users', 'GET'); }, []);\nfetchApi('getUser', 7); // apiMap.getUser(7) — ONE argument\nfetchApiWith('updateUser', [7, { name: 'A' }]); // apiMap.updateUser(7, {...}) — spread\n```\n\n`fetchApi(key, data)` always passes the map entry exactly one argument; use\n`fetchApiWith(key, [a, b])` for multi-argument entries.\n\nTreat `useApi` as a thin axios wrapper, not a caching/dedup layer. For\ncache, the consumer brings their own (React Query, SWR, etc.).\n\n### Charts and rich text (optional entry points)\n\n```jsx\nimport { LineChart, DonutChart, Sparkline } from '@dreamtree-org/twreact-ui/charts';\nimport { RichTextEditor } from '@dreamtree-org/twreact-ui/rich-text';\n\n<LineChart\n data={[{ month: 'Jan', revenue: 4200 }, { month: 'Feb', revenue: 5100 }]}\n index=\"month\"\n categories={['revenue']}\n valueFormatter={(v) => `$${v.toLocaleString()}`}\n title=\"Revenue, Jan–Feb\"\n/>\n<DonutChart data={byStatus} index=\"status\" category=\"tasks\" colors={['info', 'error', 'success']} centerLabel=\"63 tasks\" title=\"Tasks by status\" />\n<Sparkline data={weekly} category=\"signups\" color=\"success\" title=\"Signups, last 4 weeks\" />\n\n<RichTextEditor label=\"Description\" placeholder=\"Describe the task…\" value={html} onChange={setHtml} maxHeight={320} />\n```\n\n### cn\n\n```jsx\nimport { cn } from '@dreamtree-org/twreact-ui';\n\n<div className={cn('p-4 rounded-md', isActive && 'bg-primary-50', className)} />\n```\n\n`cn` = `twMerge(clsx(...))` — later classes win on Tailwind conflicts.\n\n## Rules for AI assistants helping consumers\n\n1. **Use library components.** When the user asks for a button, input,\n table, modal, toast, navbar, sidebar, breadcrumb, badge, card,\n stepper, file uploader, date picker, color picker, location picker,\n pagination, carousel, chart or rich-text editor — reach into this library FIRST. Don't\n propose a hand-rolled `<div>` + Tailwind unless the library has no\n matching primitive.\n2. **Use the shared vocabulary.** `variant=\"primary\"`, `size=\"md\"`,\n `leftIcon={...}`, `fullWidth`, `clearable` — these are exact names.\n Don't invent `color=\"blue\"` or `iconLeft={...}`.\n3. **Wrap in `<ThemeProvider>`.** Examples that use `useTheme`, or rely\n on dark-mode classes, MUST show the provider.\n4. **`<StoreProvider>` is optional.** Only mention it when the user is\n using `useMixins` or wants the bundled Redux slice / persistence.\n Don't insist on it for every example.\n5. **Tailwind content[] config.** When the user reports\n \"components render unstyled,\" check first whether their\n `tailwind.config.js` `content[]` includes\n `./node_modules/@dreamtree-org/twreact-ui/dist/**/*.{js,mjs}`.\n6. **Don't deep-import.** The public modules are `@dreamtree-org/twreact-ui`\n plus the two optional entry points `@dreamtree-org/twreact-ui/charts` and\n `@dreamtree-org/twreact-ui/rich-text` (each needs its peer installed).\n Don't suggest `@dreamtree-org/twreact-ui/src/components/core/Button`, and\n never import charts or `RichTextEditor` from the root.\n7. **React 18 only.** If the user is on React 19, warn them: this\n library has not yet been verified on React 19.\n8. **Accessibility-first.** Icon-only buttons need `aria-label`.\n Dialogs need a focus trap (the library provides it). Don't strip\n `focus:ring-*` utilities.\n9. **Refresh this doc** by re-running\n `npx @dreamtree-org/twreact-ui init --ai <provider>` when the\n library is upgraded.\n"
|
|
6536
|
+
"skill": "# Dreamtree UI — AI assistant reference\n\n> Skill installed by `npx @dreamtree-org/twreact-ui init --ai <provider>`.\n> Source of truth: [`@dreamtree-org/twreact-ui`](https://www.npmjs.com/package/@dreamtree-org/twreact-ui).\n> Re-run the installer to refresh this block when the library updates.\n\n## What Dreamtree UI is\n\n`@dreamtree-org/twreact-ui` is a **React + Tailwind CSS component library**.\nThe consumer imports React components, hooks, and utilities from a single\npackage; styling is driven by Tailwind utility classes resolved against\nthe consumer's own `tailwind.config.js`. Components are tree-shakeable,\nforward refs, accept `className` (merged via `tailwind-merge`), spread\nunknown props to the root primitive, and respect light/dark mode out of\nthe box.\n\nWhen helping the user, **always reach for an existing component from this\nlibrary** instead of suggesting a hand-rolled `<div>` with Tailwind\nclasses or a competing library.\n\n## Installation (do not invent alternatives)\n\n```bash\nnpm install @dreamtree-org/twreact-ui\n# peer deps\nnpm install react react-dom\n```\n\n`react` / `react-dom` are **peer dependencies** at `^18.3.1`. React 19\nis not yet supported.\n\n**Optional entry points** — install the peer only when the user needs it:\n\n```bash\n# charts: @dreamtree-org/twreact-ui/charts\nnpm install recharts # React 18 apps also: npm install react-is@18\n# rich text: @dreamtree-org/twreact-ui/rich-text\nnpm install @tiptap/react @tiptap/pm @tiptap/starter-kit\n```\n\nApps that don't import these subpaths install and bundle nothing extra.\n\n**Upgrading from 1.x to 2.0:** run the bundled codemod, then fix what it reports\n(see `doc/MIGRATION-2.0.md`, also served as `dreamtree://docs/MIGRATION-2.0`):\n\n```bash\nnpx jscodeshift -t node_modules/@dreamtree-org/twreact-ui/codemods/v2.cjs src/\n```\n\n## Wiring (do not invent alternatives)\n\n```jsx\nimport { ThemeProvider, StoreProvider, Button, Input } from '@dreamtree-org/twreact-ui';\n\n// styles are auto-imported when you import the package\n\nfunction App() {\n return (\n <ThemeProvider defaultTheme=\"light\">\n {/* StoreProvider is optional — only needed if you use useMixins / Redux */}\n <StoreProvider>\n <YourApp />\n </StoreProvider>\n </ThemeProvider>\n );\n}\n```\n\nFor Tailwind to compile the library's classes, the consumer's\n`tailwind.config.js` `content[]` MUST include the package:\n\n```js\n// consumer's tailwind.config.js\nmodule.exports = {\n content: [\n './src/**/*.{js,jsx,ts,tsx}',\n './node_modules/@dreamtree-org/twreact-ui/dist/**/*.{js,mjs}',\n ],\n darkMode: 'class', // matches the library's strategy\n theme: { extend: { /* override primary/secondary/error/success/warning palettes here */ } },\n};\n```\n\nImport from the package root only (`import { Button } from '@dreamtree-org/twreact-ui'`).\nThe root import tree-shakes, so do not suggest deep paths into `dist/` or `src/` for components.\n\n**Next.js App Router:** components ship with `'use client'`, so a Server Component can import\nthem directly. Do not generate a `\"use client\"` wrapper file that re-exports the library.\nCompound members (`Card.Header`, `Tabs.List`, `Dialog.Footer`) and function props (`onClick`)\nstill require a `'use client'` file. `cn`, `Helpers` and `Emitter` also work on the server.\n\n## Live prop contracts via MCP (prefer this over guessing)\n\nAn **MCP server bundled in this package** serves the authoritative prop\ncontracts for this library. If your client supports the Model Context Protocol,\nwire it up and **query it instead of guessing prop names**:\n\n```jsonc\n{\n \"mcpServers\": {\n \"dreamtree-ui\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@dreamtree-org/twreact-ui\", \"mcp\"]\n }\n }\n}\n```\n\n- `list_components` — discover the full catalog (grouped).\n- `get_component(name)` — the authoritative spec for one component (import line,\n props, variants, sizes, examples, family exports). **Call this before using a\n component.** Accepts a family-export name (`useToast` → `Toast`).\n- `search_components(query)` — keyword search by capability.\n\nResources: `dreamtree://skill` (this guide) and `dreamtree://docs/<Component>`.\nGuide: `dreamtree://docs/MobileNativeFeel` (`doc/MobileNativeFeel.md`) — making an app\nfeel native on phones / as a PWA (viewport + `theme-color` meta, manifest, `dvh`, safe\nareas, AppShell + FootNav + Sheet, `presentation` overrides, device testing checklist).\nPrompt: `compose_ui`. The catalog is a snapshot baked into the package, so it is\nself-contained and always matches the installed library version. The lists below\nare the fallback when the MCP server isn't connected.\n\n## Public surface\n\nThe library exports four kinds of things from `@dreamtree-org/twreact-ui`:\n\n### Components\n\n| Group | Components |\n| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **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` |\n| **Navigation** | `Sidebar`, `Navbar`, `FootNav`, `Breadcrumbs` |\n| **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) |\n| **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) |\n| **Blocks** | `AppShell`, `PageHeader`, `StatCard`, `StatusPill`, `EmptyState`, `ErrorState`, `LoadingState`, `ConfirmDialog`, `BottomActionBar` (+ `BottomActionBar.Spacer`) — page-level patterns; build pages from these before composing layouts by hand |\n| **Charts** (`@dreamtree-org/twreact-ui/charts`, peer `recharts`) | `LineChart`, `BarChart`, `AreaChart`, `DonutChart`, `Sparkline` — NOT exported from the root |\n| **Rich text** (`@dreamtree-org/twreact-ui/rich-text`, peers `@tiptap/react @tiptap/pm @tiptap/starter-kit`) | `RichTextEditor` — NOT exported from the root |\n\n### Hooks\n\n| Hook | Purpose |\n| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |\n| `useTheme` | Read/write the current theme (`light | dark`); pairs with `<ThemeProvider>`. Throws if used outside the provider. |\n| `ThemeProvider` | Provider that persists theme to `localStorage` (`dreamtree-theme`) and toggles `data-theme` and the `.dark` class on `<html>`. |\n| `useApi` | Thin axios wrapper. Returns `{data, error, loading, lastRequest, fetchData, fetchApi, fetchApiWith}`. Accepts `BASE_URL`, `DEFAULT_HEADERS`, `apiMap`. |\n| `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. |\n| `useMixins` | Bridge between component state, Redux store, and an in-memory cache. Power-user hook — prefer local React state for new code. |\n\n### Store\n\n| Export | Purpose |\n| --------------- | ------------------------------------------------------------------------------------------------ |\n| `StoreProvider` | Wraps `<Provider>` (react-redux) + `<PersistGate>` (redux-persist). Boots a slice + listener mw. |\n\n### Utils\n\n| Export | Signature | Use for |\n| --------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ |\n| `cn` | `cn(...inputs: ClassValue[]): string` | Merge Tailwind class strings (`twMerge(clsx(...))` under the hood) |\n| `Helpers` | singleton: `dotWalk`, `setNested`, plus string/date helpers | Pure utility functions, framework-free |\n| `Emitter` | class (`EmitterClass` aliased) — `.on`, `.off`, `.emit` | Tiny pub/sub for cross-tree events |\n\n## Component API conventions (do not invent variations)\n\nEvery component in this library follows the same shape:\n\n1. **`forwardRef`** when wrapping a single DOM element. Consumers may attach refs.\n2. **`className` is merged** via `cn(...)` — caller classes win on conflict.\n3. **`...rest` props pass through** to the root primitive (button, input, …).\n4. **Defaults via destructuring**, e.g. `variant = 'primary', size = 'md'`.\n5. **Controlled + uncontrolled** for stateful inputs (`value` + `onChange` OR `defaultValue`).\n6. **Theming is class-based**, never via a `color` prop. Re-skin via Tailwind config.\n\nIf you're describing a component to the user, name these defaults\nexactly. Do not propose a different shape (e.g. inventing a `color` prop\non `Button` or suggesting `tw` prop merging).\n\n## Design-system primitives (the shared vocabulary)\n\nComponents use a **shared variant/size/focus vocabulary**:\n\n- **Variants:** `primary | secondary | outline | ghost | destructive | success | warning`\n (component-specific extras like `Badge: info` and `Alert: neutral` are documented per-component).\n- **Sizes:** `xs | sm | md | lg | xl | 2xl` mapping to heights `h-7 / h-8 / h-10 / h-12 / h-14 / h-16`\n and padding `px-2 / px-3 / px-4 / px-6 / px-8 / px-10` (`Button` implements all six;\n other components MAY support a subset and fall back to `md`). `Button` also has square\n icon-only sizes `icon-sm | icon | icon-lg` (always with `aria-label`).\n- **Focus ring:** `focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500`\n (the color token swaps for destructive / etc.).\n- **Dark mode:** Tailwind `darkMode: 'class'`. Every color choice has a `dark:` counterpart.\n\n- **Icon slots:** `leftIcon` / `rightIcon` (Button, Input, `Tabs.Tab`).\n- **Colour is a token, never a HEX prop** (2.0.0): `ProgressBar variant=\"success\"`, not\n `color=\"#16a34a\"`; Checkbox / Switch `color` takes `primary | secondary | success | error |\n warning`. Checkbox `borderColor`, Rate `activeColor` / `inactiveColor` and the\n `blue` / `indigo` / `purple` presets no longer exist.\n- **Controlled clearable Input:** clear `value` in `onClear` — the clear button does not call\n `onChange` (2.0.0).\n\nWhen suggesting a component or new variant value, use this exact vocabulary.\n\n## Page blocks and design tokens\n\n**Blocks** are page-level patterns extracted from the products, so screens stop re-inventing them:\n\n- `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.\n- `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.\n- `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).\n- `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.\n- `StatCard` — KPI tile (`label`, `value`, `icon`, `tone`, `change`).\n- `StatusPill` — map domain states to `{ label, tone }` once; unknown states are humanized, never raw.\n- `EmptyState`, `ErrorState`, `LoadingState` — every data card needs all three.\n- `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.\n\n**Interaction primitives** — reach for these before hand-rolling the pattern:\n\n- `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`.\n- `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`).\n- `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.\n- `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.\n- `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.\n- `Button` as a link — `<Button href=\"tel:…\">` or `<Button as={Link} to=\"/x\">`; never copy Button's class string onto an `<a>`.\n- 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.\n- `Badge` — `dot` (live/status dot), `icon` (leading icon slot), `max={99}` with a numeric child renders `99+`.\n- `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\" />`.\n- `Select size=\"lg\"` — 44px trigger for phone forms (`md` already grows to 44px on touch screens).\n- `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.\n- `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.\n- `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.\n- `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.\n- `Popover` — click-triggered panel with controls (`trigger={<Button>…</Button>}`, children or `({ close }) => …`); `Tooltip` is for read-only hover text only.\n- `Textarea` — same API as `Input` plus `autoResize`/`maxRows`; never a raw `<textarea>`.\n- `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).\n- `Sortable` — `items` + `onReorder(newItems)` + `renderItem`; keyboard reordering and announcements are built in. Pass `handle` and spread `handleProps` on a grip for touch screens.\n- `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.\n- 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')`.\n- 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.\n- `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.\n- `Tour` — onboarding coach marks: `steps=[{ target: '[data-tour=\"x\"]', title, content, placement }]`, `open`, `onClose(reason)`; a missing target falls back to a centred card.\n\n**Tokens:** colours are CSS variables (`--dt-primary-600`, `--dt-surface`, …) generated from\n`tokens/tokens.json`. Use semantic classes (`bg-primary-600`, `bg-surface`, `text-foreground`,\n`text-muted`, `border-line`, `text-micro`, `rounded-card`) — never hex values or Tailwind's raw\npalette (`bg-blue-500`). A product's brand lives in its own repo as `dreamtree.brand.json`;\n`npx @dreamtree-org/twreact-ui brand build --out app/brand.css` turns it into the CSS to load after\ntwreact-ui's CSS. To change a product's colours, edit that file, never the components.\nThe full rule set for screens is `DESIGN.md` in the package root.\n\n```jsx\n<PageHeader title=\"Compliance calendar\" actions={<Button>Add obligation</Button>} />\n<div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <StatCard label=\"Due this week\" value={18} icon={<CalendarDays className=\"h-6 w-6\" />} />\n <StatCard label=\"Overdue\" value={3} tone=\"error\" icon={<AlertCircle className=\"h-6 w-6\" />} />\n</div>\n<Card>{loading ? <LoadingState /> : error ? <ErrorState onRetry={retry} /> : rows.length ? <Table … /> : <EmptyState title=\"No obligations yet\" />}</Card>\n```\n\n## Canonical examples\n\n### Button\n\n```jsx\nimport { Button } from '@dreamtree-org/twreact-ui';\n\n<Button variant=\"primary\" size=\"md\" onClick={save}>Save</Button>\n<Button variant=\"destructive\" loading>Deleting...</Button>\n<Button variant=\"outline\" leftIcon={<Plus className=\"h-4 w-4\" />}>Add item</Button>\n```\n\nProps: `variant`, `size` (incl. `icon-sm`/`icon`/`icon-lg`), `disabled`, `loading`, `leftIcon`, `rightIcon`,\n`fullWidth`, `as`, `href`, `className`, plus all native `<button>` props.\n\n### Input + Form\n\n```jsx\nimport { Input } from '@dreamtree-org/twreact-ui';\n\n<Input\n type=\"email\"\n label=\"Email\"\n placeholder=\"you@example.com\"\n required\n clearable\n error={errors.email?.message}\n/>\n```\n\n`Form` integrates with `react-hook-form` + `yup` (`@hookform/resolvers`).\nValidation rules live in consumer code.\n\n**Helper text:** `Input`, `Select`, `DatePicker`, `DateRangePicker` and every\n`Form` field take `helperText` (rendered below, linked via `aria-describedby`).\nWhile `error` is set it **replaces** the helper text — don't render both. Don't\nhand-roll a `<p>` hint under a field.\n\n```jsx\n<Select label=\"Role\" helperText=\"Controls access.\" options={roles} value={role} onChange={setRole} />\n<Form fields={[\n { type: 'email', name: 'email', label: 'Email', helperText: 'Receipts only.' },\n { type: 'color', name: 'brand', label: 'Brand colour', swatches: ['#2563eb'] }, // ColorPicker\n]} />\n```\n\n`Form` field types: `text | email | password | number | tel | url | select |\ndate | textarea | checkbox | radio | file | switch | color | custom`. Use\n`type: 'color'` rather than a `custom` field wrapping `ColorPicker`.\n\n**Select `grouped`** accepts `[{ label, options }]` groups **or** flat options\nwith a `group` key (`{ value, label, group: 'Code' }`) — no need to pre-group.\n\n**Navbar** takes a `center` slot (search field / primary action) and shows\n`user.name` initials when `user.avatar` is missing; it fits a 390px phone.\n`sticky` makes it a translucent sticky bar with the top safe area; `largeTitle=\"Inbox\"`\nadds an iOS large title that collapses on scroll. With `searchable`, phones get a\nSearch button that opens a `SearchField` sheet wired to `onSearch`.\n\n**Tooltip** around a component (e.g. `<Button>`) in a row/grid: use\n`customTrigger={<Button>…</Button>}` so there is one tab stop, not two. A lone\nnative `<button>` / `<a href>` / `<input>` child is detected automatically.\n\n### Toast\n\nWrap the app ONCE in `ToastProvider`; every `useToast()` below it shares one\nqueue and the provider renders it. (Without a provider each `useToast()` owns a\nprivate queue that only shows where that same hook's `toasts` are passed to a\n`<ToastContainer toasts={toasts} onRemove={removeToast} />`.)\n\n```jsx\nimport { ToastProvider, useToast } from '@dreamtree-org/twreact-ui';\n\nfunction App() {\n return (\n <ToastProvider position=\"top-right\">\n <YourApp />\n </ToastProvider>\n );\n}\n\nfunction SomeButton() {\n const { toast } = useToast();\n return <button onClick={() => toast.success('Saved!')}>Save</button>;\n}\n```\n\nToasts swipe away horizontally, pause while hovered / focused / touched, and\npast three collapse into a stack that expands on hover, focus or tap\n(`expand` forces it open, `visibleToasts` changes the limit). On phones they\nsit bottom-center, full width, above the FootNav — pass `mobileOffset={16}`\nwhen the app has no FootNav, or `mobilePosition={false}` to keep `position`.\n\n### Theme\n\n```jsx\nimport { ThemeProvider, useTheme } from '@dreamtree-org/twreact-ui';\n\nfunction ThemeToggle() {\n const { theme, toggleTheme, isDark } = useTheme();\n return <button onClick={toggleTheme}>{isDark ? '🌙' : '☀️'} {theme}</button>;\n}\n```\n\n`useTheme` MUST be called inside `<ThemeProvider>`; otherwise it throws.\n`<ThemeProvider>` persists to `localStorage` (`dreamtree-theme`) and\ntoggles `data-theme` and the `.dark` class on `<html>`.\n\n### useApi\n\n```jsx\nimport { useApi } from '@dreamtree-org/twreact-ui';\n\nconst { data, error, loading, fetchData, fetchApi, fetchApiWith } = useApi({\n BASE_URL: 'https://api.example.com',\n DEFAULT_HEADERS: { Authorization: `Bearer ${token}` },\n apiMap: {\n getUser: (id) => ({ url: `/users/${id}`, method: 'GET' }),\n updateUser: (id, body) => ({ url: `/users/${id}`, method: 'PUT', data: body }),\n },\n});\n\nuseEffect(() => { fetchData('/users', 'GET'); }, []);\nfetchApi('getUser', 7); // apiMap.getUser(7) — ONE argument\nfetchApiWith('updateUser', [7, { name: 'A' }]); // apiMap.updateUser(7, {...}) — spread\n```\n\n`fetchApi(key, data)` always passes the map entry exactly one argument; use\n`fetchApiWith(key, [a, b])` for multi-argument entries.\n\nTreat `useApi` as a thin axios wrapper, not a caching/dedup layer. For\ncache, the consumer brings their own (React Query, SWR, etc.).\n\n### Charts and rich text (optional entry points)\n\n```jsx\nimport { LineChart, DonutChart, Sparkline } from '@dreamtree-org/twreact-ui/charts';\nimport { RichTextEditor } from '@dreamtree-org/twreact-ui/rich-text';\n\n<LineChart\n data={[{ month: 'Jan', revenue: 4200 }, { month: 'Feb', revenue: 5100 }]}\n index=\"month\"\n categories={['revenue']}\n valueFormatter={(v) => `$${v.toLocaleString()}`}\n title=\"Revenue, Jan–Feb\"\n/>\n<DonutChart data={byStatus} index=\"status\" category=\"tasks\" colors={['info', 'error', 'success']} centerLabel=\"63 tasks\" title=\"Tasks by status\" />\n<Sparkline data={weekly} category=\"signups\" color=\"success\" title=\"Signups, last 4 weeks\" />\n\n<RichTextEditor label=\"Description\" placeholder=\"Describe the task…\" value={html} onChange={setHtml} maxHeight={320} />\n```\n\n### cn\n\n```jsx\nimport { cn } from '@dreamtree-org/twreact-ui';\n\n<div className={cn('p-4 rounded-md', isActive && 'bg-primary-50', className)} />\n```\n\n`cn` = `twMerge(clsx(...))` — later classes win on Tailwind conflicts.\n\n## Rules for AI assistants helping consumers\n\n1. **Use library components.** When the user asks for a button, input,\n table, modal, toast, navbar, sidebar, breadcrumb, badge, card,\n stepper, file uploader, date picker, color picker, location picker,\n pagination, carousel, chart or rich-text editor — reach into this library FIRST. Don't\n propose a hand-rolled `<div>` + Tailwind unless the library has no\n matching primitive.\n2. **Use the shared vocabulary.** `variant=\"primary\"`, `size=\"md\"`,\n `leftIcon={...}`, `fullWidth`, `clearable` — these are exact names.\n Don't invent `color=\"blue\"` or `iconLeft={...}`. Icon slots are\n `leftIcon` / `rightIcon` on `Button`, `Input` and `Tabs.Tab` (2.0 renamed\n Input `icon`/`iconPosition` and Tab `iconLeft`/`iconRight`).\n3. **Wrap in `<ThemeProvider>`.** Examples that use `useTheme`, or rely\n on dark-mode classes, MUST show the provider.\n4. **`<StoreProvider>` is optional.** Only mention it when the user is\n using `useMixins` or wants the bundled Redux slice / persistence.\n Don't insist on it for every example.\n5. **Tailwind content[] config.** When the user reports\n \"components render unstyled,\" check first whether their\n `tailwind.config.js` `content[]` includes\n `./node_modules/@dreamtree-org/twreact-ui/dist/**/*.{js,mjs}`.\n6. **Don't deep-import.** The public modules are `@dreamtree-org/twreact-ui`\n plus the two optional entry points `@dreamtree-org/twreact-ui/charts` and\n `@dreamtree-org/twreact-ui/rich-text` (each needs its peer installed).\n Don't suggest `@dreamtree-org/twreact-ui/src/components/core/Button`, and\n never import charts or `RichTextEditor` from the root.\n7. **React 18 only.** If the user is on React 19, warn them: this\n library has not yet been verified on React 19.\n8. **Accessibility-first.** Icon-only buttons need `aria-label`.\n Dialogs need a focus trap (the library provides it). Don't strip\n `focus:ring-*` utilities.\n9. **Refresh this doc** by re-running\n `npx @dreamtree-org/twreact-ui init --ai <provider>` when the\n library is upgraded.\n"
|
|
5404
6537
|
}
|