division-ds 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +23 -3
- package/README.md +11 -0
- package/division-ds-0.2.1.tgz +0 -0
- package/package.json +2 -2
- package/public/docs/components/accordion.md +207 -0
- package/public/docs/components/ai-label.md +90 -0
- package/public/docs/components/alert-dialog.md +292 -0
- package/public/docs/components/alert.md +175 -0
- package/public/docs/components/approval-request.md +148 -0
- package/public/docs/components/auth-form.md +132 -0
- package/public/docs/components/avatar-group.md +152 -0
- package/public/docs/components/avatar.md +179 -0
- package/public/docs/components/badge.md +178 -0
- package/public/docs/components/breadcrumbs.md +112 -0
- package/public/docs/components/button.md +204 -0
- package/public/docs/components/card.md +257 -0
- package/public/docs/components/carousel.md +130 -0
- package/public/docs/components/cart.md +172 -0
- package/public/docs/components/chart-panel.md +358 -0
- package/public/docs/components/chat-message.md +157 -0
- package/public/docs/components/chat-thread.md +244 -0
- package/public/docs/components/checkbox.md +149 -0
- package/public/docs/components/checkout-form.md +128 -0
- package/public/docs/components/command-palette.md +147 -0
- package/public/docs/components/data-table.md +401 -0
- package/public/docs/components/description-list.md +103 -0
- package/public/docs/components/dialog.md +197 -0
- package/public/docs/components/divider.md +97 -0
- package/public/docs/components/dropdown-menu.md +222 -0
- package/public/docs/components/empty-state.md +145 -0
- package/public/docs/components/footer.md +397 -0
- package/public/docs/components/form-field.md +216 -0
- package/public/docs/components/icon-button.md +127 -0
- package/public/docs/components/input.md +200 -0
- package/public/docs/components/kanban-board.md +300 -0
- package/public/docs/components/kbd.md +72 -0
- package/public/docs/components/link.md +98 -0
- package/public/docs/components/list-item.md +241 -0
- package/public/docs/components/loading-animation.md +72 -0
- package/public/docs/components/multi-select.md +202 -0
- package/public/docs/components/navbar.md +144 -0
- package/public/docs/components/newsletter-form.md +112 -0
- package/public/docs/components/page-header.md +151 -0
- package/public/docs/components/pagination.md +212 -0
- package/public/docs/components/product-card.md +172 -0
- package/public/docs/components/progress.md +101 -0
- package/public/docs/components/prompt-input.md +151 -0
- package/public/docs/components/quantity-stepper.md +140 -0
- package/public/docs/components/radio.md +177 -0
- package/public/docs/components/rating.md +106 -0
- package/public/docs/components/score-ring.md +95 -0
- package/public/docs/components/search.md +116 -0
- package/public/docs/components/section-heading.md +113 -0
- package/public/docs/components/segmented-control.md +126 -0
- package/public/docs/components/select.md +224 -0
- package/public/docs/components/sheet.md +145 -0
- package/public/docs/components/sidebar.md +193 -0
- package/public/docs/components/skeleton.md +135 -0
- package/public/docs/components/slider.md +114 -0
- package/public/docs/components/sparkline.md +128 -0
- package/public/docs/components/spinner.md +98 -0
- package/public/docs/components/stat.md +106 -0
- package/public/docs/components/status-tracker.md +145 -0
- package/public/docs/components/stepper.md +147 -0
- package/public/docs/components/switch.md +153 -0
- package/public/docs/components/tabs.md +180 -0
- package/public/docs/components/tag.md +105 -0
- package/public/docs/components/textarea.md +148 -0
- package/public/docs/components/timeline.md +152 -0
- package/public/docs/components/toast.md +194 -0
- package/public/docs/components/tool-call.md +107 -0
- package/public/docs/components/tooltip.md +108 -0
- package/public/manifest/components.json +198 -15
- package/public/manifest/tokens.json +75 -5
- package/scripts/division-ds.mjs +78 -42
- package/scripts/lib/agent-docs.mjs +80 -6
- package/scripts/lib/ai-copy.mjs +120 -0
- package/scripts/lib/button-spacing.mjs +5 -5
- package/scripts/lib/check-text.mjs +57 -0
- package/scripts/lib/contrast.mjs +111 -0
- package/scripts/lib/host-inline.mjs +59 -0
- package/scripts/lib/mcp-server.mjs +116 -0
- package/scripts/lib/mcp-tools.mjs +266 -0
- package/scripts/lib/props-validator.mjs +3 -2
- package/scripts/lib/rules-installer.mjs +42 -4
- package/scripts/lib/token-discipline.mjs +10 -3
- package/skills/division-ds/SKILL.md +90 -0
- package/skills/division-ds/commands/add.md +25 -0
- package/skills/division-ds/commands/build.md +46 -0
- package/skills/division-ds/commands/fix.md +28 -0
- package/skills/division-ds/commands/review.md +25 -0
- package/skills/division-ds/topics/ai.md +8 -0
- package/skills/division-ds/topics/colour.md +11 -0
- package/skills/division-ds/topics/feedback.md +9 -0
- package/skills/division-ds/topics/forms.md +22 -0
- package/skills/division-ds/topics/layout.md +36 -0
- package/skills/division-ds/topics/motion.md +10 -0
- package/skills/division-ds/topics/overlays.md +9 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# AlertDialog
|
|
2
|
+
|
|
3
|
+
> Asks for confirmation before something with real consequences. It shows a question, says what will happen, and offers Cancel and the action. Screen readers announce it as an alert, a click outside won't dismiss it, and focus starts on Cancel. If the action returns a promise (a value that finishes later, such as a server call), the dialog stays open and shows it's busy until that work is done.
|
|
4
|
+
|
|
5
|
+
- **Best for:** Confirming an action that matters and can't be undone, such as a delete.
|
|
6
|
+
- **Not for:** Something that can be undone: just do it and offer Undo in a Toast. A form or a task: use Dialog.
|
|
7
|
+
- Organism. Source: `src/components/organisms/alert-dialog/AlertDialog.tsx`
|
|
8
|
+
- Rendered docs page: `/components/alert-dialog`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogFooter, AlertDialogTrigger } from 'src/components/organisms/alert-dialog/AlertDialog'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
|
|
17
|
+
|
|
18
|
+
## Props
|
|
19
|
+
|
|
20
|
+
| Prop | Type | Default | Description |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `open (AlertDialog)` | `boolean` | | Whether it is open, when your code controls that (controlled mode). |
|
|
23
|
+
| `defaultOpen (AlertDialog)` | `boolean` | `false` | Whether it starts open, when the component tracks its own state (uncontrolled mode). |
|
|
24
|
+
| `onOpenChange (AlertDialog)` | `(open: boolean) => void` | | Called on every open and close. It isn't called when someone tries to close it while the action is busy. |
|
|
25
|
+
| `asChild (Trigger)` | `boolean` | | Uses its single child as the trigger instead of adding its own button. Pass a Button. |
|
|
26
|
+
| `ref (Trigger)` | `Ref<HTMLButtonElement>` | | The trigger button. |
|
|
27
|
+
| `title (Content)` | `string` | | Required. The question, and the name screen readers say. |
|
|
28
|
+
| `description (Content)` | `string` | | Required. What confirming does. Screen readers read it as the accessible description. |
|
|
29
|
+
| `size (Content)` | `'sm' \| 'md'` | `'md'` | Maximum width: 24rem or 32rem (rem is a unit based on the root text size, usually 16 pixels). |
|
|
30
|
+
| `className (Content)` | `string` | | Added to the panel classes. Where two Tailwind utility classes clash, the later one wins. |
|
|
31
|
+
| `ref (Content)` | `Ref<HTMLDivElement>` | | The element with the role alertdialog. |
|
|
32
|
+
| `onPointerDownOutside / onInteractOutside (Content)` | `—` | | Not available: a click outside never dismisses an alert dialog. |
|
|
33
|
+
| `className (Footer)` | `string` | | Added to the button row classes. |
|
|
34
|
+
| `children (Cancel)` | `ReactNode` | `'Cancel'` | The label. Name what the person keeps, for example Keep editing. |
|
|
35
|
+
| `ref (Cancel)` | `Ref<HTMLButtonElement>` | | The Cancel button, which gets focus when the dialog opens. |
|
|
36
|
+
| `tone (Action)` | `'hostile' \| 'default'` | `'hostile'` | hostile (the danger button) for something that can't be undone. default (the primary button) for a confirm that's safe but still matters. |
|
|
37
|
+
| `onClick (Action)` | `(event) => void \| Promise<unknown>` | | Runs on confirm, then closes the dialog. If it returns a promise, the dialog stays open and busy until the promise settles: success closes it, failure keeps it open. Calling event.preventDefault() also keeps it open. |
|
|
38
|
+
| `loading (Action)` | `boolean` | `false` | Marks it busy from outside, for work you track yourself. It holds the dialog open the same way a pending promise does. |
|
|
39
|
+
| `loadingLabel (Action)` | `string` | `'Working'` | Text you can't see that screen readers announce while it's busy. |
|
|
40
|
+
| `ref (Action)` | `Ref<HTMLButtonElement>` | | The action button. |
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
45
|
+
|
|
46
|
+
### Delete while the request is still running: the action button shows it's busy and the dialog waits
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
const request = (fail = false) =>
|
|
50
|
+
new Promise<void>((resolve, reject) =>
|
|
51
|
+
window.setTimeout(() => (fail ? reject(new Error('The server did not answer.')) : resolve()), 1000)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
function DeleteExample() {
|
|
55
|
+
const { toast } = useToast()
|
|
56
|
+
return (
|
|
57
|
+
<AlertDialog>
|
|
58
|
+
<AlertDialogTrigger asChild>
|
|
59
|
+
<Button variant="danger">Delete depot</Button>
|
|
60
|
+
</AlertDialogTrigger>
|
|
61
|
+
<AlertDialogContent
|
|
62
|
+
title="Delete Brasov north?"
|
|
63
|
+
description="Its 12 open manifests move to the unassigned queue. The depot cannot be restored."
|
|
64
|
+
>
|
|
65
|
+
<AlertDialogFooter>
|
|
66
|
+
<AlertDialogCancel />
|
|
67
|
+
<AlertDialogAction
|
|
68
|
+
loadingLabel="Deleting depot"
|
|
69
|
+
onClick={() => request().then(() => toast({ tone: 'ok', title: 'Depot deleted' }))}
|
|
70
|
+
>
|
|
71
|
+
Delete depot
|
|
72
|
+
</AlertDialogAction>
|
|
73
|
+
</AlertDialogFooter>
|
|
74
|
+
</AlertDialogContent>
|
|
75
|
+
</AlertDialog>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
<DeleteExample />
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### A failed request: the dialog stays open and a toast says why
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
const request = (fail = false) =>
|
|
86
|
+
new Promise<void>((resolve, reject) =>
|
|
87
|
+
window.setTimeout(() => (fail ? reject(new Error('The server did not answer.')) : resolve()), 1000)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
function FailureExample() {
|
|
91
|
+
const { toast } = useToast()
|
|
92
|
+
return (
|
|
93
|
+
<AlertDialog>
|
|
94
|
+
<AlertDialogTrigger asChild>
|
|
95
|
+
<Button variant="secondary">Revoke API key</Button>
|
|
96
|
+
</AlertDialogTrigger>
|
|
97
|
+
<AlertDialogContent
|
|
98
|
+
title="Revoke the warehouse key?"
|
|
99
|
+
description="Scripts that use it stop working at once. This demo always fails, to show the dialog staying open."
|
|
100
|
+
size="sm"
|
|
101
|
+
>
|
|
102
|
+
<AlertDialogFooter>
|
|
103
|
+
<AlertDialogCancel>Keep key</AlertDialogCancel>
|
|
104
|
+
<AlertDialogAction
|
|
105
|
+
loadingLabel="Revoking key"
|
|
106
|
+
onClick={() =>
|
|
107
|
+
request(true).catch((error: Error) => {
|
|
108
|
+
toast({ tone: 'hostile', title: 'Key not revoked', description: error.message })
|
|
109
|
+
throw error
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
>
|
|
113
|
+
Revoke key
|
|
114
|
+
</AlertDialogAction>
|
|
115
|
+
</AlertDialogFooter>
|
|
116
|
+
</AlertDialogContent>
|
|
117
|
+
</AlertDialog>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
<FailureExample />
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### A safe confirm that still has consequences, with the default tone
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
function DefaultToneExample() {
|
|
128
|
+
return (
|
|
129
|
+
<AlertDialog>
|
|
130
|
+
<AlertDialogTrigger asChild>
|
|
131
|
+
<Button>Publish prices</Button>
|
|
132
|
+
</AlertDialogTrigger>
|
|
133
|
+
<AlertDialogContent
|
|
134
|
+
title="Publish the autumn prices?"
|
|
135
|
+
description="Customers see them on every replica page as soon as you publish."
|
|
136
|
+
size="sm"
|
|
137
|
+
>
|
|
138
|
+
<AlertDialogFooter>
|
|
139
|
+
<AlertDialogCancel>Not yet</AlertDialogCancel>
|
|
140
|
+
<AlertDialogAction tone="default">Publish</AlertDialogAction>
|
|
141
|
+
</AlertDialogFooter>
|
|
142
|
+
</AlertDialogContent>
|
|
143
|
+
</AlertDialog>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
<DefaultToneExample />
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Type the name to confirm, for the costliest actions
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
const request = (fail = false) =>
|
|
154
|
+
new Promise<void>((resolve, reject) =>
|
|
155
|
+
window.setTimeout(() => (fail ? reject(new Error('The server did not answer.')) : resolve()), 1000)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
const ACCOUNT = 'northfield-supply'
|
|
159
|
+
|
|
160
|
+
function TypeToConfirmExample() {
|
|
161
|
+
const [typed, setTyped] = useState('')
|
|
162
|
+
const matches = typed.trim() === ACCOUNT
|
|
163
|
+
return (
|
|
164
|
+
<AlertDialog onOpenChange={() => setTyped('')}>
|
|
165
|
+
<AlertDialogTrigger asChild>
|
|
166
|
+
<Button variant="danger">Delete account</Button>
|
|
167
|
+
</AlertDialogTrigger>
|
|
168
|
+
<AlertDialogContent
|
|
169
|
+
title="Delete the account?"
|
|
170
|
+
description="Its 212 orders, saved kit lists and team access are removed for good."
|
|
171
|
+
>
|
|
172
|
+
<FormField label="Account name" help={`Type ${ACCOUNT} to enable Delete account.`}>
|
|
173
|
+
<Input
|
|
174
|
+
value={typed}
|
|
175
|
+
onChange={event => setTyped(event.target.value)}
|
|
176
|
+
autoComplete="off"
|
|
177
|
+
spellCheck={false}
|
|
178
|
+
className="font-mono"
|
|
179
|
+
/>
|
|
180
|
+
</FormField>
|
|
181
|
+
<AlertDialogFooter>
|
|
182
|
+
<AlertDialogCancel />
|
|
183
|
+
<AlertDialogAction disabled={!matches} loadingLabel="Deleting account" onClick={() => request()}>
|
|
184
|
+
Delete account
|
|
185
|
+
</AlertDialogAction>
|
|
186
|
+
</AlertDialogFooter>
|
|
187
|
+
</AlertDialogContent>
|
|
188
|
+
</AlertDialog>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
<TypeToConfirmExample />
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Keyboard
|
|
196
|
+
|
|
197
|
+
| Keys | Result |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `Tab` | Moves between Cancel, the action and any field, looping around inside the dialog. |
|
|
200
|
+
| `Enter` | Presses the focused button. Focus starts on Cancel, so an accidental Enter cancels. |
|
|
201
|
+
| `Esc` | Cancels and sends focus back to whatever opened it. It waits while the action is busy. |
|
|
202
|
+
|
|
203
|
+
## Do
|
|
204
|
+
|
|
205
|
+
- Write the title as the question, with the thing it's about: 'Delete Brasov north?', not 'Are you sure?'.
|
|
206
|
+
- Say in the description exactly what will be lost or changed, with numbers where you have them.
|
|
207
|
+
- Label the action with the verb and the thing: 'Delete depot', never 'OK' or 'Yes'.
|
|
208
|
+
- Return the promise from onClick for anything that calls a server.
|
|
209
|
+
- For something very costly, add a field asking for the name, and disable the action until it matches (see the last example).
|
|
210
|
+
|
|
211
|
+
## Don't
|
|
212
|
+
|
|
213
|
+
- Don't use it for something that can be undone. Do the thing and offer Undo in a toast.
|
|
214
|
+
- Don't use it for a form or a task. That's a Dialog.
|
|
215
|
+
- Don't add a close button or close it on a click outside. The only choices are Cancel or the action.
|
|
216
|
+
- Don't open one from a dropdown menu item. The item is gone by the time the dialog closes, so focus has nowhere to go back to.
|
|
217
|
+
|
|
218
|
+
## Common mistakes
|
|
219
|
+
|
|
220
|
+
Wrong:
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
<Dialog>
|
|
224
|
+
<DialogContent title="Delete depot?">
|
|
225
|
+
<DialogFooter>
|
|
226
|
+
<DialogClose asChild><Button variant="secondary">Cancel</Button></DialogClose>
|
|
227
|
+
<Button variant="danger" onClick={remove}>Delete</Button>
|
|
228
|
+
</DialogFooter>
|
|
229
|
+
</DialogContent>
|
|
230
|
+
</Dialog>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Right:
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
<AlertDialog>
|
|
237
|
+
<AlertDialogContent title="Delete depot?" description="Its open manifests move to the unassigned queue.">
|
|
238
|
+
<AlertDialogFooter>
|
|
239
|
+
<AlertDialogCancel />
|
|
240
|
+
<AlertDialogAction onClick={remove}>Delete depot</AlertDialogAction>
|
|
241
|
+
</AlertDialogFooter>
|
|
242
|
+
</AlertDialogContent>
|
|
243
|
+
</AlertDialog>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Dialog has the role dialog (the label screen readers use to describe it), closes when someone clicks the scrim behind it, and focuses the first control. A destructive confirm needs the role alertdialog, no dismissal from outside, and focus on Cancel. AlertDialog does all three.
|
|
247
|
+
|
|
248
|
+
Wrong:
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
const [busy, setBusy] = useState(false)
|
|
252
|
+
<AlertDialog open={open} onOpenChange={next => { if (!busy) setOpen(next) }}>
|
|
253
|
+
...
|
|
254
|
+
<AlertDialogAction loading={busy} onClick={async event => {
|
|
255
|
+
event.preventDefault(); setBusy(true); await remove(); setBusy(false); setOpen(false)
|
|
256
|
+
}}>Delete</AlertDialogAction>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Right:
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
<AlertDialogAction onClick={() => remove()}>Delete</AlertDialogAction>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Return the promise. The action goes busy, Cancel and Escape wait, and the dialog closes when the work succeeds or stays open when it fails. You don't need any state of your own.
|
|
266
|
+
|
|
267
|
+
Wrong:
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
<AlertDialogAction onClick={() => remove()}>Delete</AlertDialogAction>
|
|
271
|
+
// remove() rejects, and nothing tells the person
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Right:
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
<AlertDialogAction onClick={() => remove().catch(error => {
|
|
278
|
+
toast({ tone: 'hostile', title: 'Not deleted', description: error.message })
|
|
279
|
+
throw error
|
|
280
|
+
})}>Delete</AlertDialogAction>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
When the promise fails, the dialog stays open but shows nothing on its own. Report the failure, then throw the error again so the dialog stays open for a retry or a Cancel.
|
|
284
|
+
|
|
285
|
+
## Anatomy
|
|
286
|
+
|
|
287
|
+
- **Scrim**: The dimmed layer over the page, coloured surface.scrim. The page under it is inert (it can't be clicked or focused) and hidden from screen readers. Unlike Dialog, clicking it does nothing: the person has to choose.
|
|
288
|
+
- **Surface**: The same panel as Dialog: surface.base, a strong hairline border, the corner crosshair and the M7 motion for opening and closing. Its role is alertdialog.
|
|
289
|
+
- **Title**: Required. The question. It's an h2 (a second-level heading) and the dialog's accessible name, the name screen readers say for it.
|
|
290
|
+
- **Description**: Required. What confirming does. Screen readers announce it with the title.
|
|
291
|
+
- **Body**: Optional. A list of what will be lost, or a field to type a name into.
|
|
292
|
+
- **Footer**: Cancel first, then the action. There's no close button.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Alert
|
|
2
|
+
|
|
3
|
+
> A message that sits in the page. It shows its tone three ways, as an icon, a word and a colour, so it still reads on a greyscale screen and through a screen reader.
|
|
4
|
+
|
|
5
|
+
- **Best for:** A message that sits in the page and stays there: a warning about this screen, or a failed load with a retry.
|
|
6
|
+
- **Not for:** Something that will go away on its own: use Toast.
|
|
7
|
+
- Molecule. Source: `src/components/molecules/alert/Alert.tsx`
|
|
8
|
+
- Rendered docs page: `/components/alert`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Alert } from 'src/components/molecules/alert/Alert'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
|
|
17
|
+
|
|
18
|
+
## Props
|
|
19
|
+
|
|
20
|
+
| Prop | Type | Default | Description |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `tone` | `'info' \| 'ok' \| 'warning' \| 'hostile'` | `'info'` | Which colour, icon and word the alert carries. |
|
|
23
|
+
| `title` | `string` | | Required. Screen readers hear the tone word in front of it. |
|
|
24
|
+
| `children` | `ReactNode` | | The body text. |
|
|
25
|
+
| `live` | `boolean` | `false` | Only for an alert added after the page has loaded. It sets the role screen readers use to announce it: hostile and warning get role=alert (announced at once), info and ok get role=status (announced politely). |
|
|
26
|
+
| `onDismiss` | `() => void` | | Adds the dismiss button. It runs once the alert has faded and its height has collapsed (M7), not on the click itself, so remove the alert from the page here. With reduced motion it runs straight away. |
|
|
27
|
+
| `dismissLabel` | `string` | `'Dismiss'` | The name screen readers give the dismiss button. |
|
|
28
|
+
| `action` | `ReactNode` | | For example, a small Button. |
|
|
29
|
+
| `compact` | `boolean` | `false` | One line, for a toolbar, table header or card. The body sits inside the title paragraph, so pass plain inline text, not blocks. |
|
|
30
|
+
| `className` | `string` | | Merged onto the root. |
|
|
31
|
+
| `ref` | `Ref<HTMLDivElement>` | | Passed through to the root DOM element. |
|
|
32
|
+
| `...rest` | `Omit<HTMLAttributes<HTMLDivElement>, 'title' \| 'role'>` | | You can't set role yourself: live decides it. |
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
37
|
+
|
|
38
|
+
### Tones
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<div className="flex flex-col gap-4 w-full">
|
|
42
|
+
<Alert tone="info" title="Depot hours changed">
|
|
43
|
+
Weekend pickup now opens at 8am.
|
|
44
|
+
</Alert>
|
|
45
|
+
<Alert tone="ok" title="Manifest saved" />
|
|
46
|
+
<Alert tone="warning" title="Depot closes early">
|
|
47
|
+
The Brasov depot closes at 4pm today for maintenance.
|
|
48
|
+
</Alert>
|
|
49
|
+
<Alert tone="hostile" title="Sync failed">
|
|
50
|
+
The last manifest sync could not reach the server.
|
|
51
|
+
</Alert>
|
|
52
|
+
</div>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### With body and action
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
<Alert
|
|
59
|
+
title="Offline"
|
|
60
|
+
action={
|
|
61
|
+
<Button variant="secondary" size="sm">
|
|
62
|
+
Retry
|
|
63
|
+
</Button>
|
|
64
|
+
}
|
|
65
|
+
>
|
|
66
|
+
Changes are kept on this device.
|
|
67
|
+
</Alert>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Compact
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<div className="flex flex-col gap-3 w-full">
|
|
74
|
+
<Alert compact tone="info" title="Depot hours changed">
|
|
75
|
+
Weekend pickup opens at 8am.
|
|
76
|
+
</Alert>
|
|
77
|
+
<Alert
|
|
78
|
+
compact
|
|
79
|
+
tone="warning"
|
|
80
|
+
title="Low stock"
|
|
81
|
+
action={
|
|
82
|
+
<Button variant="secondary" size="sm">
|
|
83
|
+
Reorder
|
|
84
|
+
</Button>
|
|
85
|
+
}
|
|
86
|
+
>
|
|
87
|
+
Three replicas left.
|
|
88
|
+
</Alert>
|
|
89
|
+
</div>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Dismissible (it has a close button), without the live prop, because it was on the page from the start
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
function DismissibleExample() {
|
|
96
|
+
const [dismissed, setDismissed] = useState(false)
|
|
97
|
+
|
|
98
|
+
return dismissed ? (
|
|
99
|
+
<Button variant="secondary" size="sm" onClick={() => setDismissed(false)}>
|
|
100
|
+
Restore alert
|
|
101
|
+
</Button>
|
|
102
|
+
) : (
|
|
103
|
+
<Alert
|
|
104
|
+
tone="warning"
|
|
105
|
+
title="Depot closes early"
|
|
106
|
+
onDismiss={() => setDismissed(true)}
|
|
107
|
+
dismissLabel="Dismiss depot notice"
|
|
108
|
+
>
|
|
109
|
+
The Brasov depot closes at 4pm today for maintenance.
|
|
110
|
+
</Alert>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
<DismissibleExample />
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Live: it appears after a click, and a screen reader announces it straight away
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
function LiveExample() {
|
|
121
|
+
const [shown, setShown] = useState(false)
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="flex flex-col gap-3 w-full">
|
|
125
|
+
<Button
|
|
126
|
+
variant="secondary"
|
|
127
|
+
size="sm"
|
|
128
|
+
wrapperClassName="self-start"
|
|
129
|
+
onClick={() => setShown(true)}
|
|
130
|
+
>
|
|
131
|
+
Trigger a live alert
|
|
132
|
+
</Button>
|
|
133
|
+
{shown ? (
|
|
134
|
+
<Alert live tone="hostile" title="Sync failed" onDismiss={() => setShown(false)}>
|
|
135
|
+
The last manifest sync could not reach the server.
|
|
136
|
+
</Alert>
|
|
137
|
+
) : null}
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
<LiveExample />
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Keyboard
|
|
146
|
+
|
|
147
|
+
| Keys | Result |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `Tab` | Reaches the action, then the dismiss button. The alert itself isn't a stop. |
|
|
150
|
+
|
|
151
|
+
## Do
|
|
152
|
+
|
|
153
|
+
- Start the title with what happened, and the body with what to do.
|
|
154
|
+
- Use live only when the alert appears in response to something the person did.
|
|
155
|
+
- Name the dismiss button for what it closes when there's more than one alert on a page.
|
|
156
|
+
- Use compact where a full alert would push the content it describes out of view, and keep the body to a short phrase.
|
|
157
|
+
- After dismiss, move keyboard focus to the next sensible control. The alert's own dismiss button disappears with it, and focus would otherwise drop back to the top of the page.
|
|
158
|
+
|
|
159
|
+
## Don't
|
|
160
|
+
|
|
161
|
+
- Don't use an alert for something that will go away on its own. That's a Toast.
|
|
162
|
+
- Don't colour the body text in the tone.
|
|
163
|
+
- Don't put paragraphs or lists inside a compact alert. It's one line of text.
|
|
164
|
+
- Don't stack more than two alerts. The second one is usually detail for the first.
|
|
165
|
+
|
|
166
|
+
## Anatomy
|
|
167
|
+
|
|
168
|
+
- **Container**: A raised surface with a 1px (one pixel) border in the tone's text colour on all four sides. There's no stripe down one side.
|
|
169
|
+
- **Icon**: Shows the tone without relying on colour. Marked aria-hidden, so screen readers skip it.
|
|
170
|
+
- **Tone word**: 'Warning:' before the title. It's hidden on screen and read out by screen readers.
|
|
171
|
+
- **Title**: Required.
|
|
172
|
+
- **Body**: In secondary text, not in the tone colour.
|
|
173
|
+
- **Action**: Optional, usually a small Button.
|
|
174
|
+
- **Dismiss**: Optional. A ghost IconButton (an icon-only button with no fill).
|
|
175
|
+
- **Compact layout**: One line: a 16px icon, then the title and body as one sentence, then the action and dismiss button. Long text wraps onto a new line instead of being cut off.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# ApprovalRequest
|
|
2
|
+
|
|
3
|
+
> A pause before an action that changes something in the real world (a side effect). It shows a preview of exactly what will happen, Approve and Reject buttons, and then a status line that takes keyboard focus.
|
|
4
|
+
|
|
5
|
+
- **Best for:** An assistant is about to send, change or spend something outside the conversation.
|
|
6
|
+
- **Not for:** Confirming a destructive action on an ordinary screen: use AlertDialog.
|
|
7
|
+
- Molecule. Source: `src/components/molecules/approval-request/ApprovalRequest.tsx`
|
|
8
|
+
- Rendered docs page: `/components/approval-request`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { ApprovalRequest } from 'src/components/molecules/approval-request/ApprovalRequest'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
|
|
17
|
+
|
|
18
|
+
## Requires
|
|
19
|
+
|
|
20
|
+
- A parent that holds the state and sets it to approved or rejected in onApprove and onReject.
|
|
21
|
+
|
|
22
|
+
## Props
|
|
23
|
+
|
|
24
|
+
| Prop | Type | Default | Description |
|
|
25
|
+
|---|---|---|---|
|
|
26
|
+
| `title` | `string` | | Required. The action as a question. It also names the section. |
|
|
27
|
+
| `state` | `'pending' \| 'approved' \| 'rejected'` | | Required. Pending shows the buttons; approved or rejected shows the status line. |
|
|
28
|
+
| `onApprove` | `() => void` | | Required. Run the action, then set state to approved. |
|
|
29
|
+
| `onReject` | `() => void` | | Required. Set state to rejected. Nothing else happens. |
|
|
30
|
+
| `approveLabel` | `string` | `'Approve'` | The approve button. Say the action, like "Send". |
|
|
31
|
+
| `rejectLabel` | `string` | `'Reject'` | The reject button, like "Don't send". |
|
|
32
|
+
| `approvedLabel` | `string` | `'Approved by you'` | The status line once approved. |
|
|
33
|
+
| `rejectedLabel` | `string` | `'Rejected'` | The status line once rejected. |
|
|
34
|
+
| `className` | `string` | | Goes on the section. |
|
|
35
|
+
|
|
36
|
+
## Types and helpers
|
|
37
|
+
|
|
38
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
export type ApprovalState = 'pending' | 'approved' | 'rejected'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Examples
|
|
45
|
+
|
|
46
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
47
|
+
|
|
48
|
+
### Pending: approve or reject, and the keyboard focus then moves to the result
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
function LiveApproval() {
|
|
52
|
+
const [state, setState] = useState<ApprovalState>('pending')
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex w-full max-w-xl flex-col items-start gap-3">
|
|
55
|
+
<ApprovalRequest
|
|
56
|
+
title="Send this reorder to the supplier?"
|
|
57
|
+
state={state}
|
|
58
|
+
onApprove={() => setState('approved')}
|
|
59
|
+
onReject={() => setState('rejected')}
|
|
60
|
+
approveLabel="Send reorder"
|
|
61
|
+
rejectLabel="Don't send"
|
|
62
|
+
className="w-full"
|
|
63
|
+
>
|
|
64
|
+
<p>To: Arad replica supply</p>
|
|
65
|
+
<p>10 × K-41 field radio, 8 × trail pack. Delivery to Depot 4 by Thursday 22 October.</p>
|
|
66
|
+
</ApprovalRequest>
|
|
67
|
+
{state === 'pending' ? null : (
|
|
68
|
+
<Button size="sm" variant="ghost" onClick={() => setState('pending')}>
|
|
69
|
+
Reset the example
|
|
70
|
+
</Button>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
<LiveApproval />
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Resolved
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<div className="flex w-full max-w-xl flex-col gap-3">
|
|
83
|
+
<ApprovalRequest title="Move the Friday pickup to Thursday?" state="approved" onApprove={() => {}} onReject={() => {}}>
|
|
84
|
+
Pickup at Depot 4, Thursday 08:00 instead of Friday 08:00.
|
|
85
|
+
</ApprovalRequest>
|
|
86
|
+
<ApprovalRequest title="Cancel order ORD-1182?" state="rejected" onApprove={() => {}} onReject={() => {}}>
|
|
87
|
+
Two replicas, already packed.
|
|
88
|
+
</ApprovalRequest>
|
|
89
|
+
</div>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Keyboard
|
|
93
|
+
|
|
94
|
+
| Keys | Result |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `Tab` | Moves to Approve, then Reject. |
|
|
97
|
+
| `Enter` `Space` | Approves or rejects. Focus then moves to the status line that replaces the buttons. |
|
|
98
|
+
|
|
99
|
+
## Do
|
|
100
|
+
|
|
101
|
+
- Ask before every action that reaches outside the conversation: a message sent, a record changed, money spent.
|
|
102
|
+
- Name the buttons for the action ("Send", "Don't send") when the title alone leaves any doubt.
|
|
103
|
+
- Keep the decided request in the thread. It's the record of what was agreed.
|
|
104
|
+
|
|
105
|
+
## Don't
|
|
106
|
+
|
|
107
|
+
- Don't run the action before onApprove. The preview is a promise of what will happen, not a report.
|
|
108
|
+
- Don't focus the Approve button when the request appears: someone in the middle of typing would approve it with their next Enter.
|
|
109
|
+
- Don't summarise the preview. Show the email, the change, the amount.
|
|
110
|
+
|
|
111
|
+
## Common mistakes
|
|
112
|
+
|
|
113
|
+
Wrong:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
<ApprovalRequest title="Send the email?" ...>An email to the depot lead.</ApprovalRequest>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Right:
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
<ApprovalRequest title="Send this email to Ilse Varga?" ...><p>Subject: Thursday pickup</p><p>Hi Ilse, ...</p></ApprovalRequest>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
A person can only approve what they can see. The preview is the content itself, not a summary of it.
|
|
126
|
+
|
|
127
|
+
Wrong:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
{state === "pending" && <ApprovalRequest ... />}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Right:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
<ApprovalRequest state={state} ... />
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Removing it from the page once it's decided drops focus back to the page and wipes the record of what was decided. The component swaps its own buttons for a status line instead.
|
|
140
|
+
|
|
141
|
+
## Anatomy
|
|
142
|
+
|
|
143
|
+
- **Section**: Named by the title. Outlined in the signal colour while pending, and a thin line once it's decided.
|
|
144
|
+
- **Label**: "Needs your approval" with a shield icon while pending, then "Approval".
|
|
145
|
+
- **Title**: The action as a question: "Send this order to Arad depot?"
|
|
146
|
+
- **Preview**: Exactly what will happen, beside a thin line.
|
|
147
|
+
- **Actions**: Approve (primary) and Reject (secondary). Neither has focus when the request appears.
|
|
148
|
+
- **Status line**: Takes the place of the buttons: "Approved by you" or "Rejected". It has tabIndex -1 so it can receive focus from code, and focus moves here when the decision was made here.
|