division-ds 0.1.1 → 0.2.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/AGENTS.md +16 -2
- package/README.md +11 -0
- package/division-ds-0.2.0.tgz +0 -0
- package/package.json +2 -2
- package/public/docs/components/accordion.md +205 -0
- package/public/docs/components/ai-label.md +88 -0
- package/public/docs/components/alert-dialog.md +290 -0
- package/public/docs/components/alert.md +173 -0
- package/public/docs/components/approval-request.md +146 -0
- package/public/docs/components/auth-form.md +130 -0
- package/public/docs/components/avatar-group.md +150 -0
- package/public/docs/components/avatar.md +177 -0
- package/public/docs/components/badge.md +176 -0
- package/public/docs/components/breadcrumbs.md +110 -0
- package/public/docs/components/button.md +202 -0
- package/public/docs/components/card.md +255 -0
- package/public/docs/components/carousel.md +128 -0
- package/public/docs/components/cart.md +170 -0
- package/public/docs/components/chart-panel.md +356 -0
- package/public/docs/components/chat-message.md +154 -0
- package/public/docs/components/chat-thread.md +242 -0
- package/public/docs/components/checkbox.md +147 -0
- package/public/docs/components/checkout-form.md +126 -0
- package/public/docs/components/command-palette.md +145 -0
- package/public/docs/components/data-table.md +399 -0
- package/public/docs/components/description-list.md +101 -0
- package/public/docs/components/dialog.md +195 -0
- package/public/docs/components/divider.md +95 -0
- package/public/docs/components/dropdown-menu.md +220 -0
- package/public/docs/components/empty-state.md +143 -0
- package/public/docs/components/footer.md +395 -0
- package/public/docs/components/form-field.md +213 -0
- package/public/docs/components/icon-button.md +125 -0
- package/public/docs/components/input.md +198 -0
- package/public/docs/components/kanban-board.md +298 -0
- package/public/docs/components/kbd.md +70 -0
- package/public/docs/components/link.md +96 -0
- package/public/docs/components/list-item.md +238 -0
- package/public/docs/components/loading-animation.md +70 -0
- package/public/docs/components/multi-select.md +200 -0
- package/public/docs/components/navbar.md +142 -0
- package/public/docs/components/newsletter-form.md +110 -0
- package/public/docs/components/page-header.md +149 -0
- package/public/docs/components/pagination.md +210 -0
- package/public/docs/components/product-card.md +169 -0
- package/public/docs/components/progress.md +99 -0
- package/public/docs/components/prompt-input.md +149 -0
- package/public/docs/components/quantity-stepper.md +138 -0
- package/public/docs/components/radio.md +140 -0
- package/public/docs/components/rating.md +104 -0
- package/public/docs/components/score-ring.md +93 -0
- package/public/docs/components/search.md +114 -0
- package/public/docs/components/section-heading.md +111 -0
- package/public/docs/components/segmented-control.md +124 -0
- package/public/docs/components/select.md +222 -0
- package/public/docs/components/sheet.md +143 -0
- package/public/docs/components/sidebar.md +191 -0
- package/public/docs/components/skeleton.md +133 -0
- package/public/docs/components/slider.md +112 -0
- package/public/docs/components/sparkline.md +126 -0
- package/public/docs/components/spinner.md +96 -0
- package/public/docs/components/stat.md +104 -0
- package/public/docs/components/status-tracker.md +143 -0
- package/public/docs/components/stepper.md +145 -0
- package/public/docs/components/switch.md +151 -0
- package/public/docs/components/tabs.md +178 -0
- package/public/docs/components/tag.md +103 -0
- package/public/docs/components/textarea.md +146 -0
- package/public/docs/components/timeline.md +150 -0
- package/public/docs/components/toast.md +192 -0
- package/public/docs/components/tool-call.md +105 -0
- package/public/docs/components/tooltip.md +106 -0
- package/public/manifest/components.json +189 -7
- package/public/manifest/tokens.json +39 -0
- package/scripts/division-ds.mjs +76 -37
- package/scripts/lib/agent-docs.mjs +59 -2
- package/scripts/lib/ai-copy.mjs +106 -0
- package/scripts/lib/button-spacing.mjs +5 -5
- package/scripts/lib/check-text.mjs +44 -0
- package/scripts/lib/contrast.mjs +111 -0
- package/scripts/lib/host-inline.mjs +59 -0
- package/scripts/lib/mcp-server.mjs +115 -0
- package/scripts/lib/mcp-tools.mjs +241 -0
- package/scripts/lib/props-validator.mjs +1 -1
- package/scripts/lib/rules-installer.mjs +42 -4
- 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 +7 -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 +19 -0
- package/skills/division-ds/topics/layout.md +34 -0
- package/skills/division-ds/topics/motion.md +10 -0
- package/skills/division-ds/topics/overlays.md +9 -0
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| `tone` | `'info' \| 'ok' \| 'warning' \| 'hostile'` | `'info'` | Which colour, icon and word the alert carries. |
|
|
21
|
+
| `title` | `string` | | Required. Screen readers hear the tone word in front of it. |
|
|
22
|
+
| `children` | `ReactNode` | | The body text. |
|
|
23
|
+
| `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). |
|
|
24
|
+
| `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. |
|
|
25
|
+
| `dismissLabel` | `string` | `'Dismiss'` | The name screen readers give the dismiss button. |
|
|
26
|
+
| `action` | `ReactNode` | | For example, a small Button. |
|
|
27
|
+
| `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. |
|
|
28
|
+
| `className` | `string` | | Merged onto the root. |
|
|
29
|
+
| `ref` | `Ref<HTMLDivElement>` | | Passed through to the root DOM element. |
|
|
30
|
+
| `...rest` | `Omit<HTMLAttributes<HTMLDivElement>, 'title' \| 'role'>` | | You can't set role yourself: live decides it. |
|
|
31
|
+
|
|
32
|
+
## Examples
|
|
33
|
+
|
|
34
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
35
|
+
|
|
36
|
+
### Tones
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
<div className="flex flex-col gap-4 w-full">
|
|
40
|
+
<Alert tone="info" title="Depot hours changed">
|
|
41
|
+
Weekend pickup now opens at 8am.
|
|
42
|
+
</Alert>
|
|
43
|
+
<Alert tone="ok" title="Manifest saved" />
|
|
44
|
+
<Alert tone="warning" title="Depot closes early">
|
|
45
|
+
The Brasov depot closes at 4pm today for maintenance.
|
|
46
|
+
</Alert>
|
|
47
|
+
<Alert tone="hostile" title="Sync failed">
|
|
48
|
+
The last manifest sync could not reach the server.
|
|
49
|
+
</Alert>
|
|
50
|
+
</div>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### With body and action
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<Alert
|
|
57
|
+
title="Offline"
|
|
58
|
+
action={
|
|
59
|
+
<Button variant="secondary" size="sm">
|
|
60
|
+
Retry
|
|
61
|
+
</Button>
|
|
62
|
+
}
|
|
63
|
+
>
|
|
64
|
+
Changes are kept on this device.
|
|
65
|
+
</Alert>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Compact
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
<div className="flex flex-col gap-3 w-full">
|
|
72
|
+
<Alert compact tone="info" title="Depot hours changed">
|
|
73
|
+
Weekend pickup opens at 8am.
|
|
74
|
+
</Alert>
|
|
75
|
+
<Alert
|
|
76
|
+
compact
|
|
77
|
+
tone="warning"
|
|
78
|
+
title="Low stock"
|
|
79
|
+
action={
|
|
80
|
+
<Button variant="secondary" size="sm">
|
|
81
|
+
Reorder
|
|
82
|
+
</Button>
|
|
83
|
+
}
|
|
84
|
+
>
|
|
85
|
+
Three replicas left.
|
|
86
|
+
</Alert>
|
|
87
|
+
</div>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Dismissible (it has a close button), without the live prop, because it was on the page from the start
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
function DismissibleExample() {
|
|
94
|
+
const [dismissed, setDismissed] = useState(false)
|
|
95
|
+
|
|
96
|
+
return dismissed ? (
|
|
97
|
+
<Button variant="secondary" size="sm" onClick={() => setDismissed(false)}>
|
|
98
|
+
Restore alert
|
|
99
|
+
</Button>
|
|
100
|
+
) : (
|
|
101
|
+
<Alert
|
|
102
|
+
tone="warning"
|
|
103
|
+
title="Depot closes early"
|
|
104
|
+
onDismiss={() => setDismissed(true)}
|
|
105
|
+
dismissLabel="Dismiss depot notice"
|
|
106
|
+
>
|
|
107
|
+
The Brasov depot closes at 4pm today for maintenance.
|
|
108
|
+
</Alert>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
<DismissibleExample />
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Live: it appears after a click, and a screen reader announces it straight away
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
function LiveExample() {
|
|
119
|
+
const [shown, setShown] = useState(false)
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className="flex flex-col gap-3 w-full">
|
|
123
|
+
<Button
|
|
124
|
+
variant="secondary"
|
|
125
|
+
size="sm"
|
|
126
|
+
wrapperClassName="self-start"
|
|
127
|
+
onClick={() => setShown(true)}
|
|
128
|
+
>
|
|
129
|
+
Trigger a live alert
|
|
130
|
+
</Button>
|
|
131
|
+
{shown ? (
|
|
132
|
+
<Alert live tone="hostile" title="Sync failed" onDismiss={() => setShown(false)}>
|
|
133
|
+
The last manifest sync could not reach the server.
|
|
134
|
+
</Alert>
|
|
135
|
+
) : null}
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
<LiveExample />
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Keyboard
|
|
144
|
+
|
|
145
|
+
| Keys | Result |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `Tab` | Reaches the action, then the dismiss button. The alert itself isn't a stop. |
|
|
148
|
+
|
|
149
|
+
## Do
|
|
150
|
+
|
|
151
|
+
- Start the title with what happened, and the body with what to do.
|
|
152
|
+
- Use live only when the alert appears in response to something the person did.
|
|
153
|
+
- Name the dismiss button for what it closes when there's more than one alert on a page.
|
|
154
|
+
- Use compact where a full alert would push the content it describes out of view, and keep the body to a short phrase.
|
|
155
|
+
- 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.
|
|
156
|
+
|
|
157
|
+
## Don't
|
|
158
|
+
|
|
159
|
+
- Don't use an alert for something that will go away on its own. That's a Toast.
|
|
160
|
+
- Don't colour the body text in the tone.
|
|
161
|
+
- Don't put paragraphs or lists inside a compact alert. It's one line of text.
|
|
162
|
+
- Don't stack more than two alerts. The second one is usually detail for the first.
|
|
163
|
+
|
|
164
|
+
## Anatomy
|
|
165
|
+
|
|
166
|
+
- **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.
|
|
167
|
+
- **Icon**: Shows the tone without relying on colour. Marked aria-hidden, so screen readers skip it.
|
|
168
|
+
- **Tone word**: 'Warning:' before the title. It's hidden on screen and read out by screen readers.
|
|
169
|
+
- **Title**: Required.
|
|
170
|
+
- **Body**: In secondary text, not in the tone colour.
|
|
171
|
+
- **Action**: Optional, usually a small Button.
|
|
172
|
+
- **Dismiss**: Optional. A ghost IconButton (an icon-only button with no fill).
|
|
173
|
+
- **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,146 @@
|
|
|
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
|
+
## Requires
|
|
17
|
+
|
|
18
|
+
- A parent that holds the state and sets it to approved or rejected in onApprove and onReject.
|
|
19
|
+
|
|
20
|
+
## Props
|
|
21
|
+
|
|
22
|
+
| Prop | Type | Default | Description |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `title` | `string` | | Required. The action as a question. It also names the section. |
|
|
25
|
+
| `state` | `'pending' \| 'approved' \| 'rejected'` | | Required. Pending shows the buttons; approved or rejected shows the status line. |
|
|
26
|
+
| `onApprove` | `() => void` | | Required. Run the action, then set state to approved. |
|
|
27
|
+
| `onReject` | `() => void` | | Required. Set state to rejected. Nothing else happens. |
|
|
28
|
+
| `approveLabel` | `string` | `'Approve'` | The approve button. Say the action, like "Send". |
|
|
29
|
+
| `rejectLabel` | `string` | `'Reject'` | The reject button, like "Don't send". |
|
|
30
|
+
| `approvedLabel` | `string` | `'Approved by you'` | The status line once approved. |
|
|
31
|
+
| `rejectedLabel` | `string` | `'Rejected'` | The status line once rejected. |
|
|
32
|
+
| `className` | `string` | | Goes on the section. |
|
|
33
|
+
|
|
34
|
+
## Types and helpers
|
|
35
|
+
|
|
36
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
export type ApprovalState = 'pending' | 'approved' | 'rejected'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
45
|
+
|
|
46
|
+
### Pending: approve or reject, and the keyboard focus then moves to the result
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
function LiveApproval() {
|
|
50
|
+
const [state, setState] = useState<ApprovalState>('pending')
|
|
51
|
+
return (
|
|
52
|
+
<div className="flex w-full max-w-xl flex-col items-start gap-3">
|
|
53
|
+
<ApprovalRequest
|
|
54
|
+
title="Send this reorder to the supplier?"
|
|
55
|
+
state={state}
|
|
56
|
+
onApprove={() => setState('approved')}
|
|
57
|
+
onReject={() => setState('rejected')}
|
|
58
|
+
approveLabel="Send reorder"
|
|
59
|
+
rejectLabel="Don't send"
|
|
60
|
+
className="w-full"
|
|
61
|
+
>
|
|
62
|
+
<p>To: Arad replica supply</p>
|
|
63
|
+
<p>10 × K-41 field radio, 8 × trail pack. Delivery to Depot 4 by Thursday 22 October.</p>
|
|
64
|
+
</ApprovalRequest>
|
|
65
|
+
{state === 'pending' ? null : (
|
|
66
|
+
<Button size="sm" variant="ghost" onClick={() => setState('pending')}>
|
|
67
|
+
Reset the example
|
|
68
|
+
</Button>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
<LiveApproval />
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Resolved
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<div className="flex w-full max-w-xl flex-col gap-3">
|
|
81
|
+
<ApprovalRequest title="Move the Friday pickup to Thursday?" state="approved" onApprove={() => {}} onReject={() => {}}>
|
|
82
|
+
Pickup at Depot 4, Thursday 08:00 instead of Friday 08:00.
|
|
83
|
+
</ApprovalRequest>
|
|
84
|
+
<ApprovalRequest title="Cancel order ORD-1182?" state="rejected" onApprove={() => {}} onReject={() => {}}>
|
|
85
|
+
Two replicas, already packed.
|
|
86
|
+
</ApprovalRequest>
|
|
87
|
+
</div>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Keyboard
|
|
91
|
+
|
|
92
|
+
| Keys | Result |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `Tab` | Moves to Approve, then Reject. |
|
|
95
|
+
| `Enter` `Space` | Approves or rejects. Focus then moves to the status line that replaces the buttons. |
|
|
96
|
+
|
|
97
|
+
## Do
|
|
98
|
+
|
|
99
|
+
- Ask before every action that reaches outside the conversation: a message sent, a record changed, money spent.
|
|
100
|
+
- Name the buttons for the action ("Send", "Don't send") when the title alone leaves any doubt.
|
|
101
|
+
- Keep the decided request in the thread. It's the record of what was agreed.
|
|
102
|
+
|
|
103
|
+
## Don't
|
|
104
|
+
|
|
105
|
+
- Don't run the action before onApprove. The preview is a promise of what will happen, not a report.
|
|
106
|
+
- Don't focus the Approve button when the request appears: someone in the middle of typing would approve it with their next Enter.
|
|
107
|
+
- Don't summarise the preview. Show the email, the change, the amount.
|
|
108
|
+
|
|
109
|
+
## Common mistakes
|
|
110
|
+
|
|
111
|
+
Wrong:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
<ApprovalRequest title="Send the email?" ...>An email to the depot lead.</ApprovalRequest>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Right:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
<ApprovalRequest title="Send this email to Ilse Varga?" ...><p>Subject: Thursday pickup</p><p>Hi Ilse, ...</p></ApprovalRequest>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A person can only approve what they can see. The preview is the content itself, not a summary of it.
|
|
124
|
+
|
|
125
|
+
Wrong:
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
{state === "pending" && <ApprovalRequest ... />}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Right:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
<ApprovalRequest state={state} ... />
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
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.
|
|
138
|
+
|
|
139
|
+
## Anatomy
|
|
140
|
+
|
|
141
|
+
- **Section**: Named by the title. Outlined in the signal colour while pending, and a thin line once it's decided.
|
|
142
|
+
- **Label**: "Needs your approval" with a shield icon while pending, then "Approval".
|
|
143
|
+
- **Title**: The action as a question: "Send this order to Arad depot?"
|
|
144
|
+
- **Preview**: Exactly what will happen, beside a thin line.
|
|
145
|
+
- **Actions**: Approve (primary) and Reject (secondary). Neither has focus when the request appears.
|
|
146
|
+
- **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.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# AuthForm
|
|
2
|
+
|
|
3
|
+
> One form with four modes: sign in, create an account, reset a password, or enter a two-step code. It's built so password managers, pasting and keyboards all just work.
|
|
4
|
+
|
|
5
|
+
- **Best for:** Sign in, create an account, reset a password or enter a two-step code.
|
|
6
|
+
- **Not for:** A newsletter sign-up: use NewsletterForm. Any other form: build it from FormField controls.
|
|
7
|
+
- Organism. Source: `src/components/organisms/auth-form/AuthForm.tsx`
|
|
8
|
+
- Rendered docs page: `/components/auth-form`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { AuthForm } from 'src/components/organisms/auth-form/AuthForm'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| `mode` | `'sign-in' \| 'create-account' \| 'reset-password' \| 'verify-code'` | | Required. Which form shows. |
|
|
21
|
+
| `onSubmit` | `(values: AuthValues, mode: AuthMode) => void \| AuthErrors \| Promise<void \| AuthErrors>` | | Required. Runs once every field passes its checks. Resolve with errors keyed by field name to show them. If it rejects, a general alert for the whole form appears, so the failure isn't lost. |
|
|
22
|
+
| `onModeChange` | `(mode: AuthMode) => void` | | Offers the other modes and reports when someone switches. |
|
|
23
|
+
| `headingLevel` | `2 \| 3 \| 4` | `2` | Match the page's heading outline. The error summary sits one level below. The exception is 4, the lowest heading this system uses: there the summary stays an h4 (a level-4 heading) too. |
|
|
24
|
+
| `className` | `string` | | Merged with the root classes. Later Tailwind utilities win. |
|
|
25
|
+
| `ref` | `Ref<HTMLFormElement>` | | The form. |
|
|
26
|
+
|
|
27
|
+
## Types and helpers
|
|
28
|
+
|
|
29
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
export type AuthMode = 'sign-in' | 'create-account' | 'reset-password' | 'verify-code'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
export type AuthField = 'name' | 'email' | 'password' | 'code'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
export type AuthValues = Record<AuthField, string>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export type AuthErrors = FieldErrors<AuthField>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
50
|
+
|
|
51
|
+
### Sign in, with a server error, and the other modes
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const wait = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms))
|
|
55
|
+
|
|
56
|
+
function AccountExample() {
|
|
57
|
+
const [mode, setMode] = useState<AuthMode>('sign-in')
|
|
58
|
+
const [signedIn, setSignedIn] = useState('')
|
|
59
|
+
const submit = async (values: AuthValues, current: AuthMode) => {
|
|
60
|
+
await wait(600)
|
|
61
|
+
if (current === 'sign-in' && values.password !== 'hexline') {
|
|
62
|
+
return { password: 'That email and password do not match.' }
|
|
63
|
+
}
|
|
64
|
+
if (current === 'sign-in' || current === 'create-account') setSignedIn(`Signed in as ${values.email}. This is a demo.`)
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
return (
|
|
68
|
+
<div className="flex flex-col gap-3 w-full max-w-sm">
|
|
69
|
+
<p className="font-body text-sm text-[var(--color-text-secondary)]">
|
|
70
|
+
Demo: any email works, and the password <code className="font-mono">hexline</code> signs in.
|
|
71
|
+
</p>
|
|
72
|
+
<AuthForm
|
|
73
|
+
mode={mode}
|
|
74
|
+
onModeChange={next => {
|
|
75
|
+
setMode(next)
|
|
76
|
+
setSignedIn('')
|
|
77
|
+
}}
|
|
78
|
+
onSubmit={submit}
|
|
79
|
+
headingLevel={4}
|
|
80
|
+
/>
|
|
81
|
+
<p role="status" className="font-body text-sm text-[var(--color-text-primary)]">
|
|
82
|
+
{signedIn}
|
|
83
|
+
</p>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
<AccountExample />
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Two-step sign-in with a code
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
const wait = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms))
|
|
95
|
+
|
|
96
|
+
<AuthForm mode="verify-code" onSubmit={() => wait(600).then(() => undefined)} headingLevel={4} />
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Keyboard
|
|
100
|
+
|
|
101
|
+
| Keys | Result |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `Tab` | Moves through the fields (the password field, then its Show password button), the submit button and the mode links. |
|
|
104
|
+
| `Enter` | In a field, submits. On an error summary entry or a mode link, activates it. |
|
|
105
|
+
| `Space` | On Show password, toggles it. On a mode link, activates it. |
|
|
106
|
+
|
|
107
|
+
## Do
|
|
108
|
+
|
|
109
|
+
- Let password managers work. Every field has its autocomplete token, and pasting is never blocked.
|
|
110
|
+
- Resolve onSubmit with errors keyed by field (a wrong password, say), so they show in the summary and at the field.
|
|
111
|
+
- Expect a rejected onSubmit to show up as its own alert instead of failing silently. There's no field to attach it to.
|
|
112
|
+
- Keep the reset confirmation neutral. It never says whether an account exists.
|
|
113
|
+
|
|
114
|
+
## Don't
|
|
115
|
+
|
|
116
|
+
- Don't add a CAPTCHA, a puzzle or a copy-paste block. Signing in mustn't test memory or copying by hand (WCAG success criterion 3.3.8; WCAG is the Web Content Accessibility Guidelines, the public rulebook for usable websites).
|
|
117
|
+
- Don't disable the submit button while fields are empty. The error summary says what's missing.
|
|
118
|
+
- Don't validate on every keystroke.
|
|
119
|
+
- Don't count on an error keyed to a field the current mode doesn't show (name while signing in, for example) landing where you'd expect. It's moved onto email, and a console.warn that only shows in development names it. Key it to a field the mode actually shows instead.
|
|
120
|
+
|
|
121
|
+
## Anatomy
|
|
122
|
+
|
|
123
|
+
- **Heading**: Names the form. Focus (the place your keyboard typing goes) moves here after a mode switch.
|
|
124
|
+
- **Error summary**: Shows up when a submit fails and takes focus. Each entry jumps to its field. It waits for a real submit attempt: leaving a field (a blur) never brings it up, so it can't shove aside a field someone's about to click.
|
|
125
|
+
- **Fields**: A FormField around each Input, with its help and error text connected to it.
|
|
126
|
+
- **Show password**: An icon button inside the password field that shows the typed text. It uses Input's revealable option, reports on or off through aria-pressed (ARIA, Accessible Rich Internet Applications, is a set of attributes that tell screen readers what a control is doing), and keeps the same name either way. The text hides again on submit and on a mode switch.
|
|
127
|
+
- **Submit**: A Button that stays focusable and shows it's busy while the caller works.
|
|
128
|
+
- **Confirmation**: A status line that's always on the page. Reset mode uses it.
|
|
129
|
+
- **Submit error**: An alert that's always present and gets its text when onSubmit rejects (the visible copy only appears then). It clears on the next submit and on a mode switch.
|
|
130
|
+
- **Mode links**: Shown when onModeChange is given: forgot password, create an account, back to sign in. They're buttons, since they change the form in place, but they look exactly like Link: signal text, an underline that thickens on hover, a pointer cursor and a focus ring.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# AvatarGroup
|
|
2
|
+
|
|
3
|
+
> Overlapping avatars (small profile pictures) with a “+N” for the rest. It’s a list named by its count, so a screen reader hears “6 members” and then each person. The people behind the “+N” are named in words, never only counted.
|
|
4
|
+
|
|
5
|
+
- **Best for:** Several people in one place, with a count: a squad, the people on an order, who’s viewing.
|
|
6
|
+
- **Not for:** Picking people: use MultiSelect.
|
|
7
|
+
- Molecule. Source: `src/components/molecules/avatar-group/AvatarGroup.tsx`
|
|
8
|
+
- Rendered docs page: `/components/avatar-group`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { AvatarGroup } from 'src/components/molecules/avatar-group/AvatarGroup'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Requires
|
|
17
|
+
|
|
18
|
+
- With overflowTooltip, the root <TooltipProvider>. AppShell and the demo layout each include one.
|
|
19
|
+
|
|
20
|
+
## Props
|
|
21
|
+
|
|
22
|
+
| Prop | Type | Default | Description |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `people` | `AvatarGroupPerson[]` | | Required. Everyone in the group, in the order shown. |
|
|
25
|
+
| `max` | `number` | `4` | The most avatars drawn. The rest go into the +N chip. |
|
|
26
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Passed to every Avatar and to the chip. |
|
|
27
|
+
| `label` | `string` | | What the group is. The count is added on the end: “Alpha squad, 6 members”. |
|
|
28
|
+
| `overflowTooltip` | `boolean` | `false` | Shows the hidden names in a Tooltip on the chip and lets the chip take keyboard focus. |
|
|
29
|
+
| `className` | `string` | | Merged with the list classes. Later Tailwind utility classes win. |
|
|
30
|
+
| `ref` | `Ref<HTMLUListElement>` | | Passed through to the list. |
|
|
31
|
+
| `...rest` | `Omit<HTMLAttributes<HTMLUListElement>, 'children' \| 'aria-label'>` | | Every standard ul prop except children (the people are the content) and aria-label (label builds it, with the count). |
|
|
32
|
+
| `name (AvatarGroupPerson)` | `string` | | Required. Passed to Avatar. |
|
|
33
|
+
| `src (AvatarGroupPerson)` | `string` | | Optional image, passed to Avatar. |
|
|
34
|
+
| `status (AvatarGroupPerson)` | `'online' \| 'away' \| 'busy' \| 'offline'` | | Optional presence (whether they’re online), passed to Avatar, which includes it in the name screen readers hear. |
|
|
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 AvatarGroupPerson = { name: string; src?: string; status?: AvatarStatus }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Examples
|
|
45
|
+
|
|
46
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
47
|
+
|
|
48
|
+
### Sizes: four people shown, and the rest counted in a small chip
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
const ALPHA: AvatarGroupPerson[] = [
|
|
52
|
+
{ name: 'Ilse Varga', status: 'online' },
|
|
53
|
+
{ name: 'Ravi Okonkwo', status: 'away' },
|
|
54
|
+
{ name: 'Mei Tanaka', status: 'busy' },
|
|
55
|
+
{ name: 'Sana Idrissi', status: 'online' },
|
|
56
|
+
{ name: 'Dana Iverson', status: 'offline' },
|
|
57
|
+
{ name: 'Lin Moreau', status: 'online' },
|
|
58
|
+
{ name: 'Tomas Reyes', status: 'offline' },
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
<div className="flex flex-col gap-4">
|
|
62
|
+
<AvatarGroup people={ALPHA} size="sm" label="Alpha squad" />
|
|
63
|
+
<AvatarGroup people={ALPHA} size="md" label="Alpha squad" />
|
|
64
|
+
<AvatarGroup people={ALPHA} size="lg" label="Alpha squad" />
|
|
65
|
+
</div>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Everyone fits: no chip
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
const BRAVO: AvatarGroupPerson[] = [{ name: 'Noor Haddad' }, { name: 'Kai Lindqvist' }, { name: 'Ada Brennan' }]
|
|
72
|
+
|
|
73
|
+
<AvatarGroup people={BRAVO} label="Bravo squad" />
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### The hidden names in a tooltip (press Tab to reach the chip)
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
const ALPHA: AvatarGroupPerson[] = [
|
|
80
|
+
{ name: 'Ilse Varga', status: 'online' },
|
|
81
|
+
{ name: 'Ravi Okonkwo', status: 'away' },
|
|
82
|
+
{ name: 'Mei Tanaka', status: 'busy' },
|
|
83
|
+
{ name: 'Sana Idrissi', status: 'online' },
|
|
84
|
+
{ name: 'Dana Iverson', status: 'offline' },
|
|
85
|
+
{ name: 'Lin Moreau', status: 'online' },
|
|
86
|
+
{ name: 'Tomas Reyes', status: 'offline' },
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
<AvatarGroup people={ALPHA} max={3} label="Alpha squad" overflowTooltip />
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### In a row, beside the words it summarises
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
const ALPHA: AvatarGroupPerson[] = [
|
|
96
|
+
{ name: 'Ilse Varga', status: 'online' },
|
|
97
|
+
{ name: 'Ravi Okonkwo', status: 'away' },
|
|
98
|
+
{ name: 'Mei Tanaka', status: 'busy' },
|
|
99
|
+
{ name: 'Sana Idrissi', status: 'online' },
|
|
100
|
+
{ name: 'Dana Iverson', status: 'offline' },
|
|
101
|
+
{ name: 'Lin Moreau', status: 'online' },
|
|
102
|
+
{ name: 'Tomas Reyes', status: 'offline' },
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
<div className="flex items-center gap-3">
|
|
106
|
+
<AvatarGroup people={ALPHA} max={3} size="sm" label="Watching this replica" />
|
|
107
|
+
<p className="text-sm text-[var(--color-text-secondary)]">7 people are watching this replica</p>
|
|
108
|
+
</div>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Keyboard
|
|
112
|
+
|
|
113
|
+
| Keys | Result |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `Tab` | With overflowTooltip, focuses the +N chip and opens the tooltip. Otherwise nothing in the group can take focus. |
|
|
116
|
+
|
|
117
|
+
## Do
|
|
118
|
+
|
|
119
|
+
- Give it a label that says who the people are: a squad, the people on an order, who’s viewing.
|
|
120
|
+
- Keep max at four or fewer in dense rows. The chip covers the rest.
|
|
121
|
+
- Put the full list somewhere people can reach (a dialog, a roster page) when they need to act on it.
|
|
122
|
+
|
|
123
|
+
## Don't
|
|
124
|
+
|
|
125
|
+
- Don't use it as a control. To pick people, use MultiSelect. To open the list, put a Button next to it.
|
|
126
|
+
- Don't set overflowTooltip on every row of a long table, because each chip becomes a Tab stop.
|
|
127
|
+
- Don't pass aria-label. The label plus the count is the name.
|
|
128
|
+
|
|
129
|
+
## Common mistakes
|
|
130
|
+
|
|
131
|
+
Wrong:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
<div className="flex -space-x-2">{people.map(p => <Avatar name={p.name} />)}</div>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Right:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<AvatarGroup people={people} label="Alpha squad" />
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A hand-built stack has no count, no thin outline between the faces, and no words for the people a +N leaves out.
|
|
144
|
+
|
|
145
|
+
## Anatomy
|
|
146
|
+
|
|
147
|
+
- **List**: A ul (a bulleted list element) named “<label>, N members”, or just “N members” when there’s no label.
|
|
148
|
+
- **Item**: One Avatar in a 2px (two pixel) frame in surface.base, cut to the same corner. Each one slides a quarter of its width under the one before.
|
|
149
|
+
- **+N chip**: Shaped like an avatar, in monospaced text in text.secondary. The “+2” is aria-hidden (screen readers skip it), and hidden text reading “and 2 more: …” names the people it stands for.
|
|
150
|
+
- **Tooltip**: Optional. Shows the hidden names on hover and on keyboard focus. The chip then becomes a Tab stop, with its focus ring drawn on an outer box that isn’t clipped.
|