division-ds 0.1.2 → 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.
Files changed (97) hide show
  1. package/AGENTS.md +16 -2
  2. package/README.md +11 -0
  3. package/division-ds-0.2.0.tgz +0 -0
  4. package/package.json +2 -2
  5. package/public/docs/components/accordion.md +205 -0
  6. package/public/docs/components/ai-label.md +88 -0
  7. package/public/docs/components/alert-dialog.md +290 -0
  8. package/public/docs/components/alert.md +173 -0
  9. package/public/docs/components/approval-request.md +146 -0
  10. package/public/docs/components/auth-form.md +130 -0
  11. package/public/docs/components/avatar-group.md +150 -0
  12. package/public/docs/components/avatar.md +177 -0
  13. package/public/docs/components/badge.md +176 -0
  14. package/public/docs/components/breadcrumbs.md +110 -0
  15. package/public/docs/components/button.md +202 -0
  16. package/public/docs/components/card.md +255 -0
  17. package/public/docs/components/carousel.md +128 -0
  18. package/public/docs/components/cart.md +170 -0
  19. package/public/docs/components/chart-panel.md +356 -0
  20. package/public/docs/components/chat-message.md +154 -0
  21. package/public/docs/components/chat-thread.md +242 -0
  22. package/public/docs/components/checkbox.md +147 -0
  23. package/public/docs/components/checkout-form.md +126 -0
  24. package/public/docs/components/command-palette.md +145 -0
  25. package/public/docs/components/data-table.md +399 -0
  26. package/public/docs/components/description-list.md +101 -0
  27. package/public/docs/components/dialog.md +195 -0
  28. package/public/docs/components/divider.md +95 -0
  29. package/public/docs/components/dropdown-menu.md +220 -0
  30. package/public/docs/components/empty-state.md +143 -0
  31. package/public/docs/components/footer.md +395 -0
  32. package/public/docs/components/form-field.md +213 -0
  33. package/public/docs/components/icon-button.md +125 -0
  34. package/public/docs/components/input.md +198 -0
  35. package/public/docs/components/kanban-board.md +298 -0
  36. package/public/docs/components/kbd.md +70 -0
  37. package/public/docs/components/link.md +96 -0
  38. package/public/docs/components/list-item.md +238 -0
  39. package/public/docs/components/loading-animation.md +70 -0
  40. package/public/docs/components/multi-select.md +200 -0
  41. package/public/docs/components/navbar.md +142 -0
  42. package/public/docs/components/newsletter-form.md +110 -0
  43. package/public/docs/components/page-header.md +149 -0
  44. package/public/docs/components/pagination.md +210 -0
  45. package/public/docs/components/product-card.md +169 -0
  46. package/public/docs/components/progress.md +99 -0
  47. package/public/docs/components/prompt-input.md +149 -0
  48. package/public/docs/components/quantity-stepper.md +138 -0
  49. package/public/docs/components/radio.md +140 -0
  50. package/public/docs/components/rating.md +104 -0
  51. package/public/docs/components/score-ring.md +93 -0
  52. package/public/docs/components/search.md +114 -0
  53. package/public/docs/components/section-heading.md +111 -0
  54. package/public/docs/components/segmented-control.md +124 -0
  55. package/public/docs/components/select.md +222 -0
  56. package/public/docs/components/sheet.md +143 -0
  57. package/public/docs/components/sidebar.md +191 -0
  58. package/public/docs/components/skeleton.md +133 -0
  59. package/public/docs/components/slider.md +112 -0
  60. package/public/docs/components/sparkline.md +126 -0
  61. package/public/docs/components/spinner.md +96 -0
  62. package/public/docs/components/stat.md +104 -0
  63. package/public/docs/components/status-tracker.md +143 -0
  64. package/public/docs/components/stepper.md +145 -0
  65. package/public/docs/components/switch.md +151 -0
  66. package/public/docs/components/tabs.md +178 -0
  67. package/public/docs/components/tag.md +103 -0
  68. package/public/docs/components/textarea.md +146 -0
  69. package/public/docs/components/timeline.md +150 -0
  70. package/public/docs/components/toast.md +192 -0
  71. package/public/docs/components/tool-call.md +105 -0
  72. package/public/docs/components/tooltip.md +106 -0
  73. package/public/manifest/components.json +182 -6
  74. package/public/manifest/tokens.json +39 -0
  75. package/scripts/division-ds.mjs +74 -38
  76. package/scripts/lib/agent-docs.mjs +53 -1
  77. package/scripts/lib/ai-copy.mjs +106 -0
  78. package/scripts/lib/button-spacing.mjs +5 -5
  79. package/scripts/lib/check-text.mjs +44 -0
  80. package/scripts/lib/contrast.mjs +111 -0
  81. package/scripts/lib/host-inline.mjs +59 -0
  82. package/scripts/lib/mcp-server.mjs +115 -0
  83. package/scripts/lib/mcp-tools.mjs +241 -0
  84. package/scripts/lib/props-validator.mjs +1 -1
  85. package/scripts/lib/rules-installer.mjs +42 -4
  86. package/skills/division-ds/SKILL.md +90 -0
  87. package/skills/division-ds/commands/add.md +25 -0
  88. package/skills/division-ds/commands/build.md +46 -0
  89. package/skills/division-ds/commands/fix.md +28 -0
  90. package/skills/division-ds/commands/review.md +25 -0
  91. package/skills/division-ds/topics/ai.md +7 -0
  92. package/skills/division-ds/topics/colour.md +11 -0
  93. package/skills/division-ds/topics/feedback.md +9 -0
  94. package/skills/division-ds/topics/forms.md +19 -0
  95. package/skills/division-ds/topics/layout.md +34 -0
  96. package/skills/division-ds/topics/motion.md +10 -0
  97. 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.