@tailor-platform/app-shell 1.15.0 → 1.16.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/dist/app-shell-core.css +1 -1
- package/dist/app-shell.d.ts +208 -114
- package/dist/app-shell.js +3449 -3446
- package/dist/{appshell-DLVBFo5J.js → appshell-B0wh5Lq0.js} +625 -512
- package/dist/style-entry.d.ts +116 -96
- package/dist/testing.d.ts +116 -96
- package/dist/testing.js +1 -1
- package/dist/vite-plugin.d.ts +116 -96
- package/package.json +8 -8
- package/skills/app-shell-patterns/SKILL.md +101 -14
- package/skills/app-shell-patterns/references/api/create-ai-gateway-client.md +115 -0
- package/skills/app-shell-patterns/references/api/create-typed-paths.md +117 -0
- package/skills/app-shell-patterns/references/api/define-i18n-labels.md +143 -0
- package/skills/app-shell-patterns/references/api/define-module.md +247 -0
- package/skills/app-shell-patterns/references/api/define-resource.md +307 -0
- package/skills/app-shell-patterns/references/api/guards/hidden.md +202 -0
- package/skills/app-shell-patterns/references/api/guards/overview.md +354 -0
- package/skills/app-shell-patterns/references/api/guards/pass.md +129 -0
- package/skills/app-shell-patterns/references/api/guards/redirect-to.md +307 -0
- package/skills/app-shell-patterns/references/api/router/use-location.md +240 -0
- package/skills/app-shell-patterns/references/api/router/use-navigate.md +138 -0
- package/skills/app-shell-patterns/references/api/router/use-params.md +218 -0
- package/skills/app-shell-patterns/references/api/use-ai-chat.md +110 -0
- package/skills/app-shell-patterns/references/api/use-app-shell-config.md +126 -0
- package/skills/app-shell-patterns/references/api/use-app-shell-data.md +146 -0
- package/skills/app-shell-patterns/references/api/use-app-shell.md +143 -0
- package/skills/app-shell-patterns/references/api/use-auth.md +145 -0
- package/skills/app-shell-patterns/references/api/use-override-breadcrumb.md +74 -0
- package/skills/app-shell-patterns/references/api/use-page-meta.md +96 -0
- package/skills/app-shell-patterns/references/api/use-register-command-palette-actions.md +90 -0
- package/skills/app-shell-patterns/references/api/use-resolved-locale.md +67 -0
- package/skills/app-shell-patterns/references/api/use-route-error.md +214 -0
- package/skills/app-shell-patterns/references/api/use-theme.md +108 -0
- package/skills/app-shell-patterns/references/api/use-time-zone.md +80 -0
- package/skills/app-shell-patterns/references/api/use-toast.md +107 -0
- package/skills/app-shell-patterns/references/components/action-panel.md +168 -0
- package/skills/app-shell-patterns/references/components/activity-card.md +142 -0
- package/skills/app-shell-patterns/references/components/ai-chat.md +315 -0
- package/skills/app-shell-patterns/references/components/alert.md +202 -0
- package/skills/app-shell-patterns/references/components/app-shell.md +530 -0
- package/skills/app-shell-patterns/references/components/appearance-switcher.md +87 -0
- package/skills/app-shell-patterns/references/components/attachment.md +138 -0
- package/skills/app-shell-patterns/references/components/autocomplete.md +278 -0
- package/skills/app-shell-patterns/references/components/avatar.md +80 -0
- package/skills/app-shell-patterns/references/components/badge.md +309 -0
- package/skills/app-shell-patterns/references/components/button.md +164 -0
- package/skills/app-shell-patterns/references/components/card.md +109 -0
- package/skills/app-shell-patterns/references/components/checkbox.md +169 -0
- package/skills/app-shell-patterns/references/components/combobox.md +318 -0
- package/skills/app-shell-patterns/references/components/command-palette.md +413 -0
- package/skills/app-shell-patterns/references/components/csv-importer.md +214 -0
- package/skills/app-shell-patterns/references/components/data-table.md +953 -0
- package/skills/app-shell-patterns/references/components/date-picker.md +248 -0
- package/skills/app-shell-patterns/references/components/default-header.md +75 -0
- package/skills/app-shell-patterns/references/components/default-sidebar.md +136 -0
- package/skills/app-shell-patterns/references/components/description-card.md +569 -0
- package/skills/app-shell-patterns/references/components/dialog.md +164 -0
- package/skills/app-shell-patterns/references/components/document-progress-card.md +154 -0
- package/skills/app-shell-patterns/references/components/form.md +378 -0
- package/skills/app-shell-patterns/references/components/global-header-layout.md +132 -0
- package/skills/app-shell-patterns/references/components/grid.md +184 -0
- package/skills/app-shell-patterns/references/components/input.md +83 -0
- package/skills/app-shell-patterns/references/components/layout.md +539 -0
- package/skills/app-shell-patterns/references/components/menu.md +218 -0
- package/skills/app-shell-patterns/references/components/metric-card.md +94 -0
- package/skills/app-shell-patterns/references/components/select.md +283 -0
- package/skills/app-shell-patterns/references/components/sheet.md +237 -0
- package/skills/app-shell-patterns/references/components/sidebar-group.md +466 -0
- package/skills/app-shell-patterns/references/components/sidebar-item.md +458 -0
- package/skills/app-shell-patterns/references/components/sidebar-layout.md +258 -0
- package/skills/app-shell-patterns/references/components/spinner.md +81 -0
- package/skills/app-shell-patterns/references/components/table.md +188 -0
- package/skills/app-shell-patterns/references/components/tabs.md +158 -0
- package/skills/app-shell-patterns/references/components/textarea.md +122 -0
- package/skills/app-shell-patterns/references/components/timeline.md +317 -0
- package/skills/app-shell-patterns/references/components/toolbar.md +138 -0
- package/skills/app-shell-patterns/references/components/tooltip.md +144 -0
- package/skills/app-shell-patterns/references/components/with-guard.md +445 -0
- package/skills/app-shell-patterns/references/concepts/authentication.md +301 -0
- package/skills/app-shell-patterns/references/concepts/custom-components.md +59 -0
- package/skills/app-shell-patterns/references/concepts/file-based-routing.md +350 -0
- package/skills/app-shell-patterns/references/{fundamental → concepts}/graphql.md +11 -4
- package/skills/app-shell-patterns/references/concepts/modules-and-resources.md +229 -0
- package/skills/app-shell-patterns/references/concepts/routing-navigation.md +387 -0
- package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +388 -0
- package/skills/app-shell-patterns/references/{fundamental/design-system.md → concepts/styling-theming.md} +258 -110
- package/skills/app-shell-patterns/references/migrations.md +34 -0
- package/skills/app-shell-patterns/references/pages/{detail.md → document-detail.md} +4 -27
- package/skills/app-shell-patterns/references/patterns/form-composer.md +43 -45
- package/skills/app-shell-patterns/references/patterns/form-modal.md +39 -100
- package/skills/app-shell-patterns/references/patterns/form-sectioned.md +12 -38
- package/skills/app-shell-patterns/references/patterns/form-single-page.md +11 -32
- package/skills/app-shell-patterns/references/patterns/form-wizard.md +12 -53
- package/skills/app-shell-patterns/references/patterns/interaction-confirm.md +11 -27
- package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +13 -27
- package/skills/app-shell-patterns/references/patterns/interaction-toast.md +10 -34
- package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +11 -86
- package/skills/app-shell-patterns/references/fundamental/components.md +0 -1009
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Menu
|
|
3
|
+
description: Dropdown menu with a compound component API, supporting checkbox/radio items, groups, separators, and nested sub-menus
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- GENERATED by docs-kit from docs-src/components/menu.docs.outline.md — do not edit by hand. -->
|
|
7
|
+
|
|
8
|
+
# Menu
|
|
9
|
+
|
|
10
|
+
The `Menu` component provides a dropdown menu with a compound component API. It is backed by Base UI's Menu primitive.
|
|
11
|
+
|
|
12
|
+
## Import
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { Menu } from "@tailor-platform/app-shell";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Basic Usage
|
|
19
|
+
|
|
20
|
+
<example-preview name="basic-usage"></example-preview>
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
function BasicUsage() {
|
|
24
|
+
return (
|
|
25
|
+
<Menu.Root>
|
|
26
|
+
<Menu.Trigger>Open menu</Menu.Trigger>
|
|
27
|
+
<Menu.Content>
|
|
28
|
+
<Menu.Item>Edit</Menu.Item>
|
|
29
|
+
<Menu.Item>Duplicate</Menu.Item>
|
|
30
|
+
<Menu.Separator />
|
|
31
|
+
<Menu.Item>Delete</Menu.Item>
|
|
32
|
+
</Menu.Content>
|
|
33
|
+
</Menu.Root>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Sub-components
|
|
39
|
+
|
|
40
|
+
| Sub-component | Description |
|
|
41
|
+
| ---------------------------- | --------------------------------------------------------- |
|
|
42
|
+
| `Menu.Root` | Manages menu open/close state |
|
|
43
|
+
| `Menu.Trigger` | Element that opens the menu when clicked |
|
|
44
|
+
| `Menu.Content` | The menu popup containing menu items |
|
|
45
|
+
| `Menu.Item` | An individual interactive item |
|
|
46
|
+
| `Menu.LinkItem` | A link item for navigation |
|
|
47
|
+
| `Menu.CheckboxItem` | A menu item that toggles a setting on or off |
|
|
48
|
+
| `Menu.CheckboxItemIndicator` | Indicates whether the checkbox item is ticked |
|
|
49
|
+
| `Menu.RadioItem` | A menu item that works like a radio button within a group |
|
|
50
|
+
| `Menu.RadioItemIndicator` | Indicates whether the radio item is selected |
|
|
51
|
+
| `Menu.RadioGroup` | Groups related radio items |
|
|
52
|
+
| `Menu.Group` | Groups related menu items |
|
|
53
|
+
| `Menu.GroupLabel` | An accessible label for a `Menu.Group` |
|
|
54
|
+
| `Menu.Separator` | A visual separator between menu items |
|
|
55
|
+
| `Menu.SubmenuRoot` | Groups all parts of a nested sub-menu |
|
|
56
|
+
| `Menu.SubmenuTrigger` | A menu item that opens a sub-menu |
|
|
57
|
+
|
|
58
|
+
## Props
|
|
59
|
+
|
|
60
|
+
### Menu.Root Props
|
|
61
|
+
|
|
62
|
+
| Prop | Type | Default | Description |
|
|
63
|
+
| -------------- | ------------------------- | ------- | --------------------------------- |
|
|
64
|
+
| `open` | `boolean` | - | Controlled open state |
|
|
65
|
+
| `defaultOpen` | `boolean` | `false` | Initial open state (uncontrolled) |
|
|
66
|
+
| `onOpenChange` | `(open: boolean) => void` | - | Callback when open state changes |
|
|
67
|
+
| `modal` | `boolean` | `true` | Whether the menu is modal |
|
|
68
|
+
| `disabled` | `boolean` | `false` | Disables the entire menu |
|
|
69
|
+
| `children` | `React.ReactNode` | - | Menu sub-components |
|
|
70
|
+
|
|
71
|
+
### Menu.Content Props
|
|
72
|
+
|
|
73
|
+
| Prop | Type | Default | Description |
|
|
74
|
+
| ----------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | ----------------------------------- |
|
|
75
|
+
| `position` | `{ side?: "top" \| "right" \| "bottom" \| "left"; align?: "start" \| "center" \| "end"; sideOffset?: number }` | `{ side: "bottom", align: "start", sideOffset: 4 }` | Placement of the popup |
|
|
76
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
77
|
+
| `children` | `React.ReactNode` | - | Menu items and other sub-components |
|
|
78
|
+
|
|
79
|
+
### Menu.Item Props
|
|
80
|
+
|
|
81
|
+
Accepts `className`, `disabled`, and all standard Base UI `Menu.Item` props.
|
|
82
|
+
|
|
83
|
+
### Menu.SubmenuRoot Props
|
|
84
|
+
|
|
85
|
+
| Prop | Type | Default | Description |
|
|
86
|
+
| -------------- | ------------------------- | ------- | --------------------------------- |
|
|
87
|
+
| `open` | `boolean` | - | Controlled open state |
|
|
88
|
+
| `defaultOpen` | `boolean` | `false` | Initial open state (uncontrolled) |
|
|
89
|
+
| `onOpenChange` | `(open: boolean) => void` | - | Callback when open state changes |
|
|
90
|
+
| `disabled` | `boolean` | `false` | Disables the entire sub-menu |
|
|
91
|
+
| `children` | `React.ReactNode` | - | Sub-menu sub-components |
|
|
92
|
+
|
|
93
|
+
## Grouped Items
|
|
94
|
+
|
|
95
|
+
Use `Menu.Group` and `Menu.GroupLabel` to visually group related items:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
<Menu.Root>
|
|
99
|
+
<Menu.Trigger>Actions</Menu.Trigger>
|
|
100
|
+
<Menu.Content>
|
|
101
|
+
<Menu.Group>
|
|
102
|
+
<Menu.GroupLabel>File</Menu.GroupLabel>
|
|
103
|
+
<Menu.Item>New</Menu.Item>
|
|
104
|
+
<Menu.Item>Open</Menu.Item>
|
|
105
|
+
</Menu.Group>
|
|
106
|
+
<Menu.Separator />
|
|
107
|
+
<Menu.Group>
|
|
108
|
+
<Menu.GroupLabel>Edit</Menu.GroupLabel>
|
|
109
|
+
<Menu.Item>Cut</Menu.Item>
|
|
110
|
+
<Menu.Item>Copy</Menu.Item>
|
|
111
|
+
<Menu.Item>Paste</Menu.Item>
|
|
112
|
+
</Menu.Group>
|
|
113
|
+
</Menu.Content>
|
|
114
|
+
</Menu.Root>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Checkbox Items
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
const [bold, setBold] = useState(false);
|
|
121
|
+
const [italic, setItalic] = useState(false);
|
|
122
|
+
|
|
123
|
+
<Menu.Root>
|
|
124
|
+
<Menu.Trigger>Format</Menu.Trigger>
|
|
125
|
+
<Menu.Content>
|
|
126
|
+
<Menu.CheckboxItem checked={bold} onCheckedChange={setBold}>
|
|
127
|
+
<Menu.CheckboxItemIndicator>✓</Menu.CheckboxItemIndicator>
|
|
128
|
+
Bold
|
|
129
|
+
</Menu.CheckboxItem>
|
|
130
|
+
<Menu.CheckboxItem checked={italic} onCheckedChange={setItalic}>
|
|
131
|
+
<Menu.CheckboxItemIndicator>✓</Menu.CheckboxItemIndicator>
|
|
132
|
+
Italic
|
|
133
|
+
</Menu.CheckboxItem>
|
|
134
|
+
</Menu.Content>
|
|
135
|
+
</Menu.Root>;
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Radio Items
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
const [align, setAlign] = useState("left");
|
|
142
|
+
|
|
143
|
+
<Menu.Root>
|
|
144
|
+
<Menu.Trigger>Align</Menu.Trigger>
|
|
145
|
+
<Menu.Content>
|
|
146
|
+
<Menu.RadioGroup value={align} onValueChange={setAlign}>
|
|
147
|
+
<Menu.RadioItem value="left">
|
|
148
|
+
<Menu.RadioItemIndicator>●</Menu.RadioItemIndicator>
|
|
149
|
+
Left
|
|
150
|
+
</Menu.RadioItem>
|
|
151
|
+
<Menu.RadioItem value="center">
|
|
152
|
+
<Menu.RadioItemIndicator>●</Menu.RadioItemIndicator>
|
|
153
|
+
Center
|
|
154
|
+
</Menu.RadioItem>
|
|
155
|
+
<Menu.RadioItem value="right">
|
|
156
|
+
<Menu.RadioItemIndicator>●</Menu.RadioItemIndicator>
|
|
157
|
+
Right
|
|
158
|
+
</Menu.RadioItem>
|
|
159
|
+
</Menu.RadioGroup>
|
|
160
|
+
</Menu.Content>
|
|
161
|
+
</Menu.Root>;
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Nested Sub-menus
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
<Menu.Root>
|
|
168
|
+
<Menu.Trigger>Edit</Menu.Trigger>
|
|
169
|
+
<Menu.Content>
|
|
170
|
+
<Menu.Item>Cut</Menu.Item>
|
|
171
|
+
<Menu.Item>Copy</Menu.Item>
|
|
172
|
+
<Menu.SubmenuRoot>
|
|
173
|
+
<Menu.SubmenuTrigger>Paste Special ›</Menu.SubmenuTrigger>
|
|
174
|
+
<Menu.Content>
|
|
175
|
+
<Menu.Item>Paste as Plain Text</Menu.Item>
|
|
176
|
+
<Menu.Item>Paste and Match Style</Menu.Item>
|
|
177
|
+
</Menu.Content>
|
|
178
|
+
</Menu.SubmenuRoot>
|
|
179
|
+
</Menu.Content>
|
|
180
|
+
</Menu.Root>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Examples
|
|
184
|
+
|
|
185
|
+
### Context Menu
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
function OrderActions({ order }: { order: Order }) {
|
|
189
|
+
return (
|
|
190
|
+
<Menu.Root>
|
|
191
|
+
<Menu.Trigger>
|
|
192
|
+
<Button variant="ghost" size="icon">
|
|
193
|
+
<EllipsisVerticalIcon />
|
|
194
|
+
</Button>
|
|
195
|
+
</Menu.Trigger>
|
|
196
|
+
<Menu.Content>
|
|
197
|
+
<Menu.Item onSelect={() => handleEdit(order)}>Edit</Menu.Item>
|
|
198
|
+
<Menu.Item onSelect={() => handleDuplicate(order)}>Duplicate</Menu.Item>
|
|
199
|
+
<Menu.Separator />
|
|
200
|
+
<Menu.Item onSelect={() => handleDelete(order)}>Delete</Menu.Item>
|
|
201
|
+
</Menu.Content>
|
|
202
|
+
</Menu.Root>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Accessibility
|
|
208
|
+
|
|
209
|
+
- Menu items are keyboard navigable with arrow keys
|
|
210
|
+
- Pressing `Escape` closes the menu
|
|
211
|
+
- Sub-menus open on arrow-right and close on arrow-left
|
|
212
|
+
- `Menu.GroupLabel` is automatically associated with its parent group
|
|
213
|
+
|
|
214
|
+
## Related Components
|
|
215
|
+
|
|
216
|
+
- [Dialog](./dialog.md) - For actions that require confirmation
|
|
217
|
+
- [Sheet](./sheet.md) - Slide-in panel for more complex workflows
|
|
218
|
+
- [Button](./button.md) - Common trigger element for menus
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: MetricCard
|
|
3
|
+
description: Compact card for dashboard KPI summaries with title, value, optional trend and description
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- GENERATED by docs-kit from docs-src/components/metric-card.docs.outline.md — do not edit by hand. -->
|
|
7
|
+
|
|
8
|
+
# MetricCard
|
|
9
|
+
|
|
10
|
+
`MetricCard` is a presentational card for displaying a single KPI (key performance indicator) on dashboards. It shows a small title, a prominent value, and optionally a trend indicator and supplementary description text. In v1 the component is static (no click handler or internal actions).
|
|
11
|
+
|
|
12
|
+
## Import
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { MetricCard } from "@tailor-platform/app-shell";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Basic Usage
|
|
19
|
+
|
|
20
|
+
<example-preview name="basic-usage"></example-preview>
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
function BasicUsage() {
|
|
24
|
+
return (
|
|
25
|
+
<MetricCard
|
|
26
|
+
title="Net total payment"
|
|
27
|
+
value="$1,500.00"
|
|
28
|
+
trend={{ direction: "up", value: "+5%" }}
|
|
29
|
+
description="vs last month"
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Props
|
|
36
|
+
|
|
37
|
+
_Generated from the type surface — first-party props only._
|
|
38
|
+
|
|
39
|
+
### MetricCardProps
|
|
40
|
+
|
|
41
|
+
| Prop | Type | Default | Description |
|
|
42
|
+
| -------------- | ----------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| `className?` | `string` | - | Additional CSS classes for the card container |
|
|
44
|
+
| `description?` | `string` | - | Optional supplementary description (e.g. "vs last period", "this week"). Empty strings are treated as absent and the description is not rendered in the meta row. |
|
|
45
|
+
| `icon?` | `ReactNode` | - | Optional leading icon |
|
|
46
|
+
| `title` | `string` | - | Metric title / header (e.g. "Net total payment") |
|
|
47
|
+
| `trend?` | `MetricCardTrend` | - | Optional trend indicator |
|
|
48
|
+
| `value` | `ReactNode` | - | Primary value (e.g. formatted currency or number) |
|
|
49
|
+
|
|
50
|
+
## Exports
|
|
51
|
+
|
|
52
|
+
- `MetricCard` — function
|
|
53
|
+
- `MetricCardProps` — interface
|
|
54
|
+
|
|
55
|
+
## Trend Directions
|
|
56
|
+
|
|
57
|
+
- **up** — Positive change (success styling, e.g. green).
|
|
58
|
+
- **down** — Negative change (destructive styling, e.g. red).
|
|
59
|
+
- **neutral** — No change or neutral (muted styling).
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<MetricCard
|
|
63
|
+
title="Revenue"
|
|
64
|
+
value="$2,400"
|
|
65
|
+
trend={{ direction: "up", value: "+12%" }}
|
|
66
|
+
description="vs last month"
|
|
67
|
+
/>
|
|
68
|
+
|
|
69
|
+
<MetricCard
|
|
70
|
+
title="Costs"
|
|
71
|
+
value="$800"
|
|
72
|
+
trend={{ direction: "down", value: "-5%" }}
|
|
73
|
+
description="vs last quarter"
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<MetricCard
|
|
77
|
+
title="Balance"
|
|
78
|
+
value="$0"
|
|
79
|
+
trend={{ direction: "neutral", value: "0%" }}
|
|
80
|
+
/>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## With Icon
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
<MetricCard title="Total orders" value="142" icon={<OrderIcon />} description="this week" />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Related
|
|
90
|
+
|
|
91
|
+
- [Grid](./grid.md) — Arrange MetricCards in responsive columns (e.g. KPI grids).
|
|
92
|
+
- [Layout](./layout.md) — Page layout for placing MetricCards in a grid.
|
|
93
|
+
- [DescriptionCard](./description-card.md) — Structured key-value cards for detail views.
|
|
94
|
+
- [Badge](./badge.md) — Status badges that can complement metric displays.
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Select
|
|
3
|
+
description: Single or multi-select dropdown with optional async data fetching
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- GENERATED by docs-kit from docs-src/components/select.docs.outline.md — do not edit by hand. -->
|
|
7
|
+
|
|
8
|
+
# Select
|
|
9
|
+
|
|
10
|
+
The `Select` component provides a fully assembled single or multi-select dropdown. Pass `items` and get a ready-to-use select out of the box. For async data fetching use `Select.Async`. For custom compositions use `Select.Parts`.
|
|
11
|
+
|
|
12
|
+
## Import
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { Select } from "@tailor-platform/app-shell";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Basic Usage
|
|
19
|
+
|
|
20
|
+
<example-preview name="basic-usage"></example-preview>
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
function BasicUsage() {
|
|
24
|
+
return (
|
|
25
|
+
<Select
|
|
26
|
+
items={["Apple", "Banana", "Cherry"]}
|
|
27
|
+
placeholder="Pick a fruit"
|
|
28
|
+
onValueChange={(value) => console.log(value)}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Props
|
|
35
|
+
|
|
36
|
+
### Select Props
|
|
37
|
+
|
|
38
|
+
| Prop | Type | Default | Description |
|
|
39
|
+
| ----------------- | -------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------- |
|
|
40
|
+
| `items` | `I[]` | - | Items to display. May be a flat array or an array of `ItemGroup<T>` |
|
|
41
|
+
| `placeholder` | `string` | - | Placeholder text shown when no value is selected |
|
|
42
|
+
| `multiple` | `true \| false \| undefined` | `false` | Enables multi-select mode |
|
|
43
|
+
| `value` | `T \| null` (single) or `T[]` (multiple) | - | Controlled value |
|
|
44
|
+
| `defaultValue` | `T \| null` (single) or `T[]` (multiple) | - | Initial value (uncontrolled) |
|
|
45
|
+
| `onValueChange` | `(value: T \| null) => void` (single) or `(value: T[]) => void` (multiple) | - | Called when the selected value changes |
|
|
46
|
+
| `renderValue` | `(value: T \| null \| T[]) => React.ReactNode` | - | Custom render function for the selected value display |
|
|
47
|
+
| `mapItem` | `(item: T) => MappedItem` | - | Map each item to its label, key, and optional custom render |
|
|
48
|
+
| `className` | `string` | - | Additional CSS classes for the root container |
|
|
49
|
+
| `disabled` | `boolean` | `false` | Disables the select |
|
|
50
|
+
| `aria-label` | `string` | - | Accessible name for the trigger. Use when there is no visible label |
|
|
51
|
+
| `aria-labelledby` | `string` | - | ID of the element(s) that label the trigger |
|
|
52
|
+
| `id` | `string` | - | ID applied to the combobox trigger element |
|
|
53
|
+
|
|
54
|
+
### MappedItem
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
interface MappedItem {
|
|
58
|
+
label: string; // Display text, used for filtering and a11y
|
|
59
|
+
key?: string; // React key. Defaults to label
|
|
60
|
+
render?: React.ReactNode; // Custom JSX to render in the dropdown
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### ItemGroup
|
|
65
|
+
|
|
66
|
+
Pass grouped items by wrapping them in `ItemGroup<T>` objects:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
interface ItemGroup<T> {
|
|
70
|
+
label: string;
|
|
71
|
+
items: T[];
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Grouped Items
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
const fruits = [
|
|
79
|
+
{ label: "Citrus", items: ["Orange", "Lemon", "Lime"] },
|
|
80
|
+
{ label: "Berries", items: ["Strawberry", "Blueberry"] },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
<Select items={fruits} placeholder="Pick a fruit" />;
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Object Items with mapItem
|
|
87
|
+
|
|
88
|
+
When items are objects, use `mapItem` to tell the component how to display them:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
type Fruit = { id: number; name: string };
|
|
92
|
+
|
|
93
|
+
const fruits: Fruit[] = [
|
|
94
|
+
{ id: 1, name: "Apple" },
|
|
95
|
+
{ id: 2, name: "Banana" },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
<Select
|
|
99
|
+
items={fruits}
|
|
100
|
+
mapItem={(fruit) => ({ label: fruit.name, key: String(fruit.id) })}
|
|
101
|
+
onValueChange={(fruit) => console.log(fruit?.id)}
|
|
102
|
+
/>;
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Multi-select
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
<Select
|
|
109
|
+
items={["Red", "Green", "Blue"]}
|
|
110
|
+
multiple
|
|
111
|
+
placeholder="Pick colors"
|
|
112
|
+
onValueChange={(colors) => console.log(colors)}
|
|
113
|
+
/>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Async Loading
|
|
117
|
+
|
|
118
|
+
Use `Select.Async` to load items from an API. The fetcher is called each time the dropdown is opened.
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import { type SelectAsyncFetcher } from "@tailor-platform/app-shell";
|
|
122
|
+
|
|
123
|
+
const fetcher: SelectAsyncFetcher<Fruit> = async ({ signal }) => {
|
|
124
|
+
const res = await fetch("/api/fruits", { signal });
|
|
125
|
+
return res.json();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
<Select.Async
|
|
129
|
+
fetcher={fetcher}
|
|
130
|
+
mapItem={(fruit) => ({ label: fruit.name, key: String(fruit.id) })}
|
|
131
|
+
placeholder="Pick a fruit"
|
|
132
|
+
onValueChange={(fruit) => console.log(fruit)}
|
|
133
|
+
/>;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Select.Async Props
|
|
137
|
+
|
|
138
|
+
Accepts all the same props as `Select` except `items`, plus:
|
|
139
|
+
|
|
140
|
+
| Prop | Type | Default | Description |
|
|
141
|
+
| ---------------------- | -------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
142
|
+
| `fetcher` | `SelectAsyncFetcher<T>` | - | Fetcher called each time the dropdown is opened |
|
|
143
|
+
| `loadingText` | `string` | `"Loading..."` | Text shown while loading |
|
|
144
|
+
| `errorText` | `string` | `"Couldn't load results."` | Message shown in the dropdown when the fetcher fails (with Retry) |
|
|
145
|
+
| `retryText` | `string` | `"Retry"` | Label for the retry button in the error state |
|
|
146
|
+
| `onFetchError` | `(error: unknown) => void` | - | Called once per outage when a fetch fails (logging/error tracking) |
|
|
147
|
+
| `modal` | `boolean` | `false` | Whether the select traps focus (modal behavior). Set to `true` when rendering inside a `Dialog` or `Sheet` to preserve focus-trap. |
|
|
148
|
+
| `alignItemWithTrigger` | `boolean` | `false` | Whether to align the selected item with the trigger when the dropdown opens. |
|
|
149
|
+
|
|
150
|
+
> **Note:** `Select.Async` does not support `ItemGroup<T>[]` — the fetcher must return a flat array.
|
|
151
|
+
|
|
152
|
+
> **Limitation:** `Select.Async` defaults to `modal={false}` and `alignItemWithTrigger={false}` to work around a Base UI scroll-lock bug with dynamically loaded items. As a result, it may not function correctly inside focus-trapping containers such as `Dialog` or `Sheet` — the focus trap can block interaction with the dropdown, or the portal may render outside the modal's visible area. If you need an async dropdown inside a `Dialog` or `Sheet`, prefer [`Combobox.Async`](./combobox.md) which uses Popover-based positioning and does not have this constraint.
|
|
153
|
+
|
|
154
|
+
### SelectAsyncFetcher
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
type SelectAsyncFetcher<T> = (options: { signal: AbortSignal }) => Promise<T[]>;
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Error handling
|
|
161
|
+
|
|
162
|
+
If the fetcher throws or rejects, `Select.Async` renders a built-in inline error state in the dropdown — the `errorText` message plus a **Retry** button that re-runs the fetch — instead of the misleading empty state. Aborted requests (the dropdown closed before the fetch settled) are ignored, and announcements are de-duped per outage. Pass `onFetchError` to run a side effect (logging, toast) — it fires once per outage and re-arms after the next successful fetch.
|
|
163
|
+
|
|
164
|
+
## Low-level Primitives
|
|
165
|
+
|
|
166
|
+
`Select.Parts` exposes the styled sub-components for fully custom compositions:
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
const { Root, Trigger, Value, Content, Item, Group, GroupLabel, Separator } = Select.Parts;
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Form submission
|
|
173
|
+
|
|
174
|
+
Inside a `Field.Root`, the field's `name` identifies the value and these props are unnecessary —
|
|
175
|
+
see [Form](./form.md). They matter for **native** submission: a plain `<form>`, `new FormData(form)`,
|
|
176
|
+
or a server action.
|
|
177
|
+
|
|
178
|
+
| Prop | Type | Default | Description |
|
|
179
|
+
| ------------------- | ----------------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
|
|
180
|
+
| `name` | `string` | - | Names the hidden input for native submission. **Ignored inside a `Field.Root`** — the field's name wins. |
|
|
181
|
+
| `form` | `string` | - | `id` of the owning form, when the control is rendered outside it. |
|
|
182
|
+
| `required` | `boolean` | `false` | Blocks submission until a value is chosen, surfacing the matching `Field.Error`. |
|
|
183
|
+
| `inputRef` | `React.Ref<HTMLInputElement>` | - | Ref to the hidden input (use for React Hook Form's `field.ref`). |
|
|
184
|
+
| `itemToStringValue` | `(item: T) => string` | - | Serialises a non-string item for submission. Items shaped `{ value, label }` use `value` automatically. |
|
|
185
|
+
|
|
186
|
+
## Examples
|
|
187
|
+
|
|
188
|
+
### Controlled Select
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
192
|
+
|
|
193
|
+
<Select
|
|
194
|
+
items={["Draft", "Pending", "Approved", "Rejected"]}
|
|
195
|
+
value={selected}
|
|
196
|
+
onValueChange={setSelected}
|
|
197
|
+
placeholder="Select status"
|
|
198
|
+
/>;
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Custom Render
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
<Select
|
|
205
|
+
items={statuses}
|
|
206
|
+
mapItem={(s) => ({
|
|
207
|
+
label: s.name,
|
|
208
|
+
key: s.id,
|
|
209
|
+
render: (
|
|
210
|
+
<span className="flex items-center gap-2">
|
|
211
|
+
<span className={`size-2 rounded-full bg-${s.color}`} />
|
|
212
|
+
{s.name}
|
|
213
|
+
</span>
|
|
214
|
+
),
|
|
215
|
+
})}
|
|
216
|
+
placeholder="Select status"
|
|
217
|
+
/>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Async with Parts (custom composition)
|
|
221
|
+
|
|
222
|
+
Combine `Select.useAsync` with `Select.Parts` for full control over layout and rendering:
|
|
223
|
+
|
|
224
|
+
`Select.useAsync` accepts the same options as `Select.Async` (including `onFetchError`) and returns:
|
|
225
|
+
|
|
226
|
+
| Property | Type | Description |
|
|
227
|
+
| --------- | ------------ | ------------------------------------------------------- |
|
|
228
|
+
| `items` | `T[]` | Currently loaded items |
|
|
229
|
+
| `loading` | `boolean` | Whether a fetch is in progress |
|
|
230
|
+
| `error` | `unknown` | The error thrown by the last fetch, if any |
|
|
231
|
+
| `retry` | `() => void` | Re-runs the last fetch (use to build a custom retry UI) |
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
type Fruit = { id: number; name: string };
|
|
235
|
+
|
|
236
|
+
const fruits = Select.useAsync({
|
|
237
|
+
fetcher: async ({ signal }) => {
|
|
238
|
+
const res = await fetch("/api/fruits", { signal });
|
|
239
|
+
return res.json() as Promise<Fruit[]>;
|
|
240
|
+
},
|
|
241
|
+
onFetchError: (error) => reportError(error),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
<Select.Parts.Root {...fruits} itemToStringLabel={(f) => f.name}>
|
|
245
|
+
<Select.Parts.Trigger>
|
|
246
|
+
<Select.Parts.Value placeholder="Pick a fruit" />
|
|
247
|
+
</Select.Parts.Trigger>
|
|
248
|
+
<Select.Parts.Content>
|
|
249
|
+
{fruits.error ? (
|
|
250
|
+
<div className="px-4 py-2 text-center text-sm">
|
|
251
|
+
Couldn't load. <button onClick={fruits.retry}>Retry</button>
|
|
252
|
+
</div>
|
|
253
|
+
) : fruits.loading ? (
|
|
254
|
+
<div className="px-4 py-2 text-center text-sm text-muted-foreground">Loading...</div>
|
|
255
|
+
) : (
|
|
256
|
+
fruits.items.map((f) => (
|
|
257
|
+
<Select.Parts.Item key={f.id} value={f}>
|
|
258
|
+
{f.name}
|
|
259
|
+
</Select.Parts.Item>
|
|
260
|
+
))
|
|
261
|
+
)}
|
|
262
|
+
</Select.Parts.Content>
|
|
263
|
+
</Select.Parts.Root>;
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Accessibility
|
|
267
|
+
|
|
268
|
+
- The trigger exposes the `combobox` role. When used inside a `Form`/`FormControl` it is labeled automatically.
|
|
269
|
+
- When used standalone (e.g. a table toolbar or list filter) there is no visible `<label>`, so give the trigger an accessible name with `aria-label` or `aria-labelledby`. Otherwise screen readers announce only the current value.
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
// Announced as "Direction filter, combobox" instead of just the value
|
|
273
|
+
<Select items={items} value={value} onValueChange={setValue} aria-label="Direction filter" />
|
|
274
|
+
|
|
275
|
+
// Or point at a visible label
|
|
276
|
+
<span id="dir-label">From</span>
|
|
277
|
+
<Select items={items} aria-labelledby="dir-label" />
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Related Components
|
|
281
|
+
|
|
282
|
+
- [Combobox](./combobox.md) - Searchable combobox with filtering
|
|
283
|
+
- [Autocomplete](./autocomplete.md) - Free-text input with suggestions
|