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.
Files changed (98) hide show
  1. package/AGENTS.md +23 -3
  2. package/README.md +11 -0
  3. package/division-ds-0.2.1.tgz +0 -0
  4. package/package.json +2 -2
  5. package/public/docs/components/accordion.md +207 -0
  6. package/public/docs/components/ai-label.md +90 -0
  7. package/public/docs/components/alert-dialog.md +292 -0
  8. package/public/docs/components/alert.md +175 -0
  9. package/public/docs/components/approval-request.md +148 -0
  10. package/public/docs/components/auth-form.md +132 -0
  11. package/public/docs/components/avatar-group.md +152 -0
  12. package/public/docs/components/avatar.md +179 -0
  13. package/public/docs/components/badge.md +178 -0
  14. package/public/docs/components/breadcrumbs.md +112 -0
  15. package/public/docs/components/button.md +204 -0
  16. package/public/docs/components/card.md +257 -0
  17. package/public/docs/components/carousel.md +130 -0
  18. package/public/docs/components/cart.md +172 -0
  19. package/public/docs/components/chart-panel.md +358 -0
  20. package/public/docs/components/chat-message.md +157 -0
  21. package/public/docs/components/chat-thread.md +244 -0
  22. package/public/docs/components/checkbox.md +149 -0
  23. package/public/docs/components/checkout-form.md +128 -0
  24. package/public/docs/components/command-palette.md +147 -0
  25. package/public/docs/components/data-table.md +401 -0
  26. package/public/docs/components/description-list.md +103 -0
  27. package/public/docs/components/dialog.md +197 -0
  28. package/public/docs/components/divider.md +97 -0
  29. package/public/docs/components/dropdown-menu.md +222 -0
  30. package/public/docs/components/empty-state.md +145 -0
  31. package/public/docs/components/footer.md +397 -0
  32. package/public/docs/components/form-field.md +216 -0
  33. package/public/docs/components/icon-button.md +127 -0
  34. package/public/docs/components/input.md +200 -0
  35. package/public/docs/components/kanban-board.md +300 -0
  36. package/public/docs/components/kbd.md +72 -0
  37. package/public/docs/components/link.md +98 -0
  38. package/public/docs/components/list-item.md +241 -0
  39. package/public/docs/components/loading-animation.md +72 -0
  40. package/public/docs/components/multi-select.md +202 -0
  41. package/public/docs/components/navbar.md +144 -0
  42. package/public/docs/components/newsletter-form.md +112 -0
  43. package/public/docs/components/page-header.md +151 -0
  44. package/public/docs/components/pagination.md +212 -0
  45. package/public/docs/components/product-card.md +172 -0
  46. package/public/docs/components/progress.md +101 -0
  47. package/public/docs/components/prompt-input.md +151 -0
  48. package/public/docs/components/quantity-stepper.md +140 -0
  49. package/public/docs/components/radio.md +177 -0
  50. package/public/docs/components/rating.md +106 -0
  51. package/public/docs/components/score-ring.md +95 -0
  52. package/public/docs/components/search.md +116 -0
  53. package/public/docs/components/section-heading.md +113 -0
  54. package/public/docs/components/segmented-control.md +126 -0
  55. package/public/docs/components/select.md +224 -0
  56. package/public/docs/components/sheet.md +145 -0
  57. package/public/docs/components/sidebar.md +193 -0
  58. package/public/docs/components/skeleton.md +135 -0
  59. package/public/docs/components/slider.md +114 -0
  60. package/public/docs/components/sparkline.md +128 -0
  61. package/public/docs/components/spinner.md +98 -0
  62. package/public/docs/components/stat.md +106 -0
  63. package/public/docs/components/status-tracker.md +145 -0
  64. package/public/docs/components/stepper.md +147 -0
  65. package/public/docs/components/switch.md +153 -0
  66. package/public/docs/components/tabs.md +180 -0
  67. package/public/docs/components/tag.md +105 -0
  68. package/public/docs/components/textarea.md +148 -0
  69. package/public/docs/components/timeline.md +152 -0
  70. package/public/docs/components/toast.md +194 -0
  71. package/public/docs/components/tool-call.md +107 -0
  72. package/public/docs/components/tooltip.md +108 -0
  73. package/public/manifest/components.json +198 -15
  74. package/public/manifest/tokens.json +75 -5
  75. package/scripts/division-ds.mjs +78 -42
  76. package/scripts/lib/agent-docs.mjs +80 -6
  77. package/scripts/lib/ai-copy.mjs +120 -0
  78. package/scripts/lib/button-spacing.mjs +5 -5
  79. package/scripts/lib/check-text.mjs +57 -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 +116 -0
  83. package/scripts/lib/mcp-tools.mjs +266 -0
  84. package/scripts/lib/props-validator.mjs +3 -2
  85. package/scripts/lib/rules-installer.mjs +42 -4
  86. package/scripts/lib/token-discipline.mjs +10 -3
  87. package/skills/division-ds/SKILL.md +90 -0
  88. package/skills/division-ds/commands/add.md +25 -0
  89. package/skills/division-ds/commands/build.md +46 -0
  90. package/skills/division-ds/commands/fix.md +28 -0
  91. package/skills/division-ds/commands/review.md +25 -0
  92. package/skills/division-ds/topics/ai.md +8 -0
  93. package/skills/division-ds/topics/colour.md +11 -0
  94. package/skills/division-ds/topics/feedback.md +9 -0
  95. package/skills/division-ds/topics/forms.md +22 -0
  96. package/skills/division-ds/topics/layout.md +36 -0
  97. package/skills/division-ds/topics/motion.md +10 -0
  98. package/skills/division-ds/topics/overlays.md +9 -0
@@ -0,0 +1,157 @@
1
+ # ChatMessage
2
+
3
+ > One turn in a conversation with an assistant: the person's words in a bubble, the reply at full width, or a centred system line. While a reply is still streaming in, it sets aria-busy (an ARIA, Accessible Rich Internet Applications, attribute that tells screen readers the content isn't finished) and shows a caret that doesn't blink.
4
+
5
+ - **Best for:** Each turn inside a ChatThread.
6
+ - **Not for:** Comments or an activity history between people: use Timeline.
7
+ - **Also on this page:** `ResponseFeedback`
8
+ - Molecule. Source: `src/components/molecules/chat-message/ChatMessage.tsx`
9
+ - Rendered docs page: `/components/chat-message`
10
+
11
+ ## Import
12
+
13
+ ```tsx
14
+ import { ChatMessage } from 'src/components/molecules/chat-message/ChatMessage'
15
+ import { ResponseFeedback } from 'src/components/molecules/chat-message/ResponseFeedback'
16
+ ```
17
+
18
+ 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.
19
+
20
+ ## Requires
21
+
22
+ - A ChatThread around it. Outside one, a message still shows, but nothing announces it when it finishes.
23
+
24
+ ## Props
25
+
26
+ | Prop | Type | Default | Description |
27
+ |---|---|---|---|
28
+ | `role` | `'user' \| 'assistant' \| 'system'` | | Required. Who's speaking. system is a quiet, centred status line. |
29
+ | `author` | `string` | | Required. "You", "Assistant". It names the message. |
30
+ | `time` | `string` | | The date and time in ISO format (the international standard way of writing them), for <time dateTime>. |
31
+ | `timeLabel` | `string` | | The time as shown on screen: "09:40". |
32
+ | `streaming` | `boolean` | `false` | The text is still arriving: sets aria-busy and shows a caret. Inside a ChatThread, the finished text is announced once, when this turns false. |
33
+ | `actions` | `ReactNode` | | Small buttons under an assistant message. They aren't shown on a user message. |
34
+ | `className` | `string` | | Goes on the article. |
35
+ | `onSubmit (ResponseFeedback)` | `(vote: 'helpful' \| 'not-helpful') => void \| Promise<void>` | | Required. Runs on a thumbs up or down. Return a promise to show "Submitting feedback" until it settles; a rejection puts the buttons back. |
36
+ | `onDetail (ResponseFeedback)` | `(text: string) => void` | | Offers an optional, dismissable "what was wrong" box after a Not helpful vote. Leave it out for no such step. |
37
+ | `className (ResponseFeedback)` | `string` | | Goes on the group. |
38
+
39
+ ## Types and helpers
40
+
41
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
42
+
43
+ ```ts
44
+ export type ChatMessageRole = 'user' | 'assistant' | 'system'
45
+ ```
46
+
47
+ ```ts
48
+ export type FeedbackVote = 'helpful' | 'not-helpful'
49
+ ```
50
+
51
+ ## Examples
52
+
53
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
54
+
55
+ ### The three roles
56
+
57
+ ```tsx
58
+ <div className="flex w-full max-w-xl flex-col gap-6">
59
+ <ChatMessage role="user" author="You" time="2026-10-15T09:40:00+03:00" timeLabel="09:40">
60
+ Which replicas are running low at Depot 4?
61
+ </ChatMessage>
62
+ <ChatMessage
63
+ role="assistant"
64
+ author="Assistant"
65
+ time="2026-10-15T09:40:00+03:00"
66
+ timeLabel="09:40"
67
+ actions={
68
+ <>
69
+ <Button size="xs" variant="ghost" iconStart={<Copy />}>
70
+ Copy
71
+ </Button>
72
+ <Button size="xs" variant="ghost" iconStart={<RotateCcw />}>
73
+ Retry
74
+ </Button>
75
+ </>
76
+ }
77
+ >
78
+ Two are under their reorder point: the K-41 field radio (6 left, reorder at 10) and the trail
79
+ pack (3 left, reorder at 8). Everything else has at least two weeks of cover.
80
+ </ChatMessage>
81
+ <ChatMessage role="system" author="Status">
82
+ Assistant stopped
83
+ </ChatMessage>
84
+ </div>
85
+ ```
86
+
87
+ ### While the reply is still streaming in: aria-busy, an ARIA (Accessible Rich Internet Applications) attribute that tells screen readers the message isn't finished, and a cursor that doesn't blink
88
+
89
+ ```tsx
90
+ <div className="flex w-full max-w-xl flex-col gap-6">
91
+ <ChatMessage role="assistant" author="Assistant" streaming>
92
+ Depot 4 holds 1,240 replicas across 38 lines. The busiest
93
+ </ChatMessage>
94
+ </div>
95
+ ```
96
+
97
+ ### Feedback on a reply: thumbs up or down, one vote, and an optional detail step after Not helpful
98
+
99
+ ```tsx
100
+ const pretendSend = () => new Promise<void>(resolve => setTimeout(resolve, 800))
101
+
102
+ <div className="flex w-full max-w-xl flex-col gap-6">
103
+ <ChatMessage
104
+ role="assistant"
105
+ author="Assistant"
106
+ actions={<ResponseFeedback onSubmit={pretendSend} onDetail={() => {}} />}
107
+ >
108
+ Depot 4 holds 1,240 replicas. Eleven are below their reorder point.
109
+ </ChatMessage>
110
+ </div>
111
+ ```
112
+
113
+ ## Keyboard
114
+
115
+ | Keys | Result |
116
+ |---|---|
117
+ | `Tab` | Moves to the controls inside the message: a ToolCall's details, an approval's buttons, the actions. |
118
+
119
+ ## Do
120
+
121
+ - Keep streaming true until the last piece of text has arrived, then set it to false once.
122
+ - Put tool calls and approvals in the assistant message where they happened, in order with its text.
123
+ - Use a system line for events in the conversation itself: stopped, retried, context cleared.
124
+ - Give every message an author, including a system line ("Status"). It names the article.
125
+ - Put ResponseFeedback in the actions of a generated reply once it has finished streaming, so a vote rates the whole reply.
126
+
127
+ ## Don't
128
+
129
+ - Don't animate the text in yourself. useStreamedText or your stream supplies it, and the thread handles announcing it.
130
+ - Don't put actions on the person's own message. Editing a sent message is a different flow.
131
+ - Don't colour the assistant's text. The bubble, the alignment and the author line already tell the roles apart.
132
+
133
+ ## Common mistakes
134
+
135
+ Wrong:
136
+
137
+ ```tsx
138
+ <ChatMessage role="assistant" author="Assistant">{partial}<span className="animate-pulse">▍</span></ChatMessage>
139
+ ```
140
+
141
+ Right:
142
+
143
+ ```tsx
144
+ <ChatMessage role="assistant" author="Assistant" streaming={!done}>{partial}</ChatMessage>
145
+ ```
146
+
147
+ streaming sets aria-busy, draws a caret that doesn't blink, and tells the thread to announce the text once, when it's finished. A hand-made blinking caret flashes for the whole stream.
148
+
149
+ ## Anatomy
150
+
151
+ - **Article**: Named by the author line, so a screen reader hears who's speaking first. Has aria-busy while streaming.
152
+ - **Author line**: The author as a label in monospaced type, then the time in a <time dateTime> element.
153
+ - **Bubble (user)**: The person's words on surface.sunken, aligned right, at most 85% of the width.
154
+ - **Body (assistant)**: Full width, with no bubble: text, paragraphs, and ToolCall and ApprovalRequest blocks stacked in a column.
155
+ - **Caret**: A solid block in the signal colour after the last paragraph while streaming, drawn with the CSS ::after rule. It never blinks.
156
+ - **Actions**: Small buttons under an assistant message: Copy, Retry, and ResponseFeedback (thumbs up and down).
157
+ - **System line**: A centred label in monospaced type between two thin lines: "Assistant stopped". Its author is there for screen readers only.
@@ -0,0 +1,244 @@
1
+ # ChatThread
2
+
3
+ > A scrolling log of ChatMessages. It follows new text only while the reader is at the end, and it announces each finished reply to screen readers once.
4
+
5
+ - **Best for:** A conversation with an assistant, above a PromptInput, where replies stream in.
6
+ - **Not for:** A record of events on one thing: use Timeline.
7
+ - Organism. Source: `src/components/organisms/chat-thread/ChatThread.tsx`
8
+ - Rendered docs page: `/components/chat-thread`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { ChatThread } from 'src/components/organisms/chat-thread/ChatThread'
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 height, because the thread scrolls and the page doesn't. Use h-[28rem], or flex-1 min-h-0 inside a column.
21
+ - ChatMessage children, so the thread can announce each finished reply.
22
+
23
+ ## Props
24
+
25
+ | Prop | Type | Default | Description |
26
+ |---|---|---|---|
27
+ | `label` | `string` | | Required. Names the log. |
28
+ | `busy` | `boolean` | `false` | The assistant is working. Shows a working line and makes one announcement when it starts. |
29
+ | `busyLabel` | `string` | `'Assistant is working'` | The working line's words, and what gets announced. |
30
+ | `jumpLabel` | `string` | `'Jump to latest'` | The jump button's words. |
31
+ | `className` | `string` | | Goes on the root. Give the thread a height here. |
32
+
33
+ ## Examples
34
+
35
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
36
+
37
+ ### A live conversation: send anything, watch the reply stream in, press Stop to end it, or scroll up while it's still arriving
38
+
39
+ ```tsx
40
+ type Turn =
41
+ | { id: number; role: 'user'; text: string }
42
+ | { id: number; role: 'assistant'; text: string; tool?: ToolCallStatus; approval?: boolean }
43
+ | { id: number; role: 'system'; text: string }
44
+
45
+ const REPLIES = [
46
+ 'Depot 4 holds 1,240 replicas across 38 lines. Two lines are under their reorder point: the K-41 field radio has 6 left against a reorder point of 10, and the trail pack has 3 left against 8. Everything else has at least two weeks of cover at the current rate. The reorder above covers both lines; once it is approved, the supplier confirms within a working day.',
47
+ 'Inbound this week: one delivery from Arad on Thursday morning, 22 pallets, booked into bay 2. Outbound: nine shipments, the largest to Sibiu on Friday. Nothing is waiting on a person except the reorder above.',
48
+ ]
49
+
50
+ const HISTORY: Turn[] = [
51
+ { id: 1, role: 'user', text: 'Summarise stock at Depot 4.' },
52
+ {
53
+ id: 2,
54
+ role: 'assistant',
55
+ text: 'Depot 4 is in good shape: 1,240 replicas on hand and nothing overdue. Two lines are low, so I have drafted a reorder for you to check.',
56
+ tool: 'done',
57
+ approval: true,
58
+ },
59
+ ]
60
+
61
+ function LiveConversation() {
62
+ const [turns, setTurns] = useState<Turn[]>(HISTORY)
63
+ const [draft, setDraft] = useState('')
64
+ const [phase, setPhase] = useState<'idle' | 'tool' | 'streaming'>('idle')
65
+ const [approval, setApproval] = useState<ApprovalState>('pending')
66
+ const [replyIndex, setReplyIndex] = useState(0)
67
+ const nextId = useRef(HISTORY.length + 1)
68
+ const activeId = useRef<number | null>(null)
69
+ const reply = useStreamedText(REPLIES[replyIndex % REPLIES.length], { active: phase === 'streaming' })
70
+
71
+ const updateActive = (change: (turn: Turn & { role: 'assistant' }) => Turn) =>
72
+ setTurns(list => list.map(turn => (turn.id === activeId.current && turn.role === 'assistant' ? change(turn) : turn)))
73
+
74
+ const addStopped = () => {
75
+ const id = nextId.current++
76
+ setTurns(list => [...list, { id, role: 'system', text: 'Assistant stopped' }])
77
+ }
78
+
79
+ // The tool "runs" for a moment, then the reply starts streaming.
80
+ useEffect(() => {
81
+ if (phase !== 'tool') return
82
+ const timer = window.setTimeout(() => {
83
+ updateActive(turn => ({ ...turn, tool: 'done' }))
84
+ setPhase('streaming')
85
+ }, 1200)
86
+ return () => window.clearTimeout(timer)
87
+ }, [phase])
88
+
89
+ // Finished or stopped: keep what arrived, and hand the thread a message that is no longer streaming.
90
+ useEffect(() => {
91
+ if (phase !== 'streaming' || !reply.done) return
92
+ updateActive(turn => ({ ...turn, text: reply.text }))
93
+ if (reply.stopped) addStopped()
94
+ setPhase('idle')
95
+ setReplyIndex(index => index + 1)
96
+ }, [phase, reply.done, reply.text, reply.stopped])
97
+
98
+ const send = (text: string) => {
99
+ const question = nextId.current++
100
+ const answer = nextId.current++
101
+ activeId.current = answer
102
+ setTurns(list => [...list, { id: question, role: 'user', text }, { id: answer, role: 'assistant', text: '', tool: 'running' }])
103
+ setDraft('')
104
+ setPhase('tool')
105
+ }
106
+
107
+ const stop = () => {
108
+ if (phase === 'streaming') {
109
+ reply.stop()
110
+ return
111
+ }
112
+ // Stopped before any text: the empty reply goes, the system line says what happened.
113
+ setTurns(list => list.filter(turn => turn.id !== activeId.current))
114
+ addStopped()
115
+ setPhase('idle')
116
+ }
117
+
118
+ const busy = phase !== 'idle'
119
+
120
+ return (
121
+ <div className="flex w-full max-w-2xl flex-col border border-[var(--color-border-hairline-strong)]">
122
+ <ChatThread label="Conversation with the assistant" busy={phase === 'tool'} className="h-[28rem]">
123
+ {turns.map(turn => {
124
+ if (turn.role === 'user') {
125
+ return (
126
+ <ChatMessage key={turn.id} role="user" author="You">
127
+ {turn.text}
128
+ </ChatMessage>
129
+ )
130
+ }
131
+ if (turn.role === 'system') {
132
+ return (
133
+ <ChatMessage key={turn.id} role="system" author="Status">
134
+ {turn.text}
135
+ </ChatMessage>
136
+ )
137
+ }
138
+ const streaming = phase === 'streaming' && turn.id === activeId.current
139
+ const text = streaming ? reply.text : turn.text
140
+ return (
141
+ <ChatMessage key={turn.id} role="assistant" author="Assistant" streaming={streaming}>
142
+ {turn.tool === undefined ? null : (
143
+ <ToolCall
144
+ name="query_stock_levels"
145
+ title="Read stock levels at Depot 4"
146
+ status={turn.tool}
147
+ duration={turn.tool === 'done' ? '1.2 s' : undefined}
148
+ input={'{ "depot": 4 }'}
149
+ output={turn.tool === 'done' ? '{ "on_hand": 1240, "lines": 38, "below_reorder_point": 2 }' : undefined}
150
+ />
151
+ )}
152
+ {text === '' ? null : <p>{text}</p>}
153
+ {turn.approval ? (
154
+ <ApprovalRequest
155
+ title="Send this reorder to the supplier?"
156
+ state={approval}
157
+ onApprove={() => setApproval('approved')}
158
+ onReject={() => setApproval('rejected')}
159
+ approveLabel="Send reorder"
160
+ rejectLabel="Don't send"
161
+ >
162
+ <p>To: Arad replica supply</p>
163
+ <p>10 × K-41 field radio, 8 × trail pack, to Depot 4 by Thursday 22 October.</p>
164
+ </ApprovalRequest>
165
+ ) : null}
166
+ </ChatMessage>
167
+ )
168
+ })}
169
+ </ChatThread>
170
+ <PromptInput
171
+ label="Message the assistant"
172
+ placeholder="Ask about stock, orders or depots"
173
+ value={draft}
174
+ onValueChange={setDraft}
175
+ onSubmit={send}
176
+ busy={busy}
177
+ onStop={stop}
178
+ className="p-4 border-t border-[var(--color-border-hairline-strong)]"
179
+ />
180
+ </div>
181
+ )
182
+ }
183
+
184
+ <LiveConversation />
185
+ ```
186
+
187
+ ## Keyboard
188
+
189
+ | Keys | Result |
190
+ |---|---|
191
+ | `Tab` | Moves to the log, then into the controls inside messages, then to Jump to latest when it's shown. |
192
+ | `ArrowUp` `ArrowDown` | With the log focused, scrolls it. |
193
+ | `PageUp` `PageDown` | With the log focused, scrolls it a page at a time. |
194
+
195
+ ## Do
196
+
197
+ - Give the thread a fixed or flexible height so the thread scrolls, not the page.
198
+ - Set busy as soon as the request starts. Clear it when the last fragment has arrived or the reply is stopped.
199
+ - After a Stop, add a system line ("Assistant stopped") so the record and the announcement both say what happened.
200
+ - Keep the PromptInput outside the thread, directly under it.
201
+
202
+ ## Don't
203
+
204
+ - Don't scroll the thread yourself on new messages. It follows the end only while the reader is there.
205
+ - Don't move focus into the thread when a reply arrives. Focus stays where the person put it.
206
+ - Don't add a second live region (an area screen readers announce changes from) for replies or tool calls. The thread's status is the only voice.
207
+
208
+ ## Common mistakes
209
+
210
+ Wrong:
211
+
212
+ ```tsx
213
+ <div role="log" aria-live="polite">{messages}</div>
214
+ ```
215
+
216
+ Right:
217
+
218
+ ```tsx
219
+ <ChatThread label="Conversation with the assistant">{messages}</ChatThread>
220
+ ```
221
+
222
+ A polite live log makes a screen reader read a streaming reply aloud one fragment at a time. The thread keeps the log quiet and announces each finished reply once.
223
+
224
+ Wrong:
225
+
226
+ ```tsx
227
+ useEffect(() => endRef.current?.scrollIntoView({ behavior: "smooth" }), [messages])
228
+ ```
229
+
230
+ Right:
231
+
232
+ ```tsx
233
+ <ChatThread ...> // follows the end only while the reader is there
234
+ ```
235
+
236
+ Scrolling on every change yanks away a reader who scrolled up to read. And smooth scrolling on every fragment means the screen never stops moving.
237
+
238
+ ## Anatomy
239
+
240
+ - **Log**: Has role="log" and is named by label. aria-live is off, so screen readers don't read every change. It's a tab stop (a place the Tab key lands) so a keyboard can scroll it, and it scrolls inside its own height.
241
+ - **Messages**: ChatMessage children, oldest first, 24px (pixels) apart.
242
+ - **Working line**: Shown while busy: the conveyor Spinner and "Assistant is working" at the end of the log.
243
+ - **Jump to latest**: Appears only when the reader has scrolled away from the end. It jumps instantly and puts focus on the log.
244
+ - **Status**: A visually hidden role="status" that screen readers hear. It says the busy label once when work starts, and each finished reply once.
@@ -0,0 +1,149 @@
1
+ # Checkbox
2
+
3
+ > A checkbox with three states: off, on and mixed. Radix (the open-source library of accessible building blocks underneath it) handles the semantics and the keyboard behaviour. The look comes from the system's shape language: a square with no rounding and a tick in ink.
4
+
5
+ - **Best for:** An on/off choice that applies when the form is saved, or a parent whose children are partly selected.
6
+ - **Not for:** A setting that takes effect the moment you flip it. Use Switch for that.
7
+ - Atom. Source: `src/components/atoms/checkbox/Checkbox.tsx`
8
+ - Rendered docs page: `/components/checkbox`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Checkbox } from 'src/components/atoms/checkbox/Checkbox'
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
+ | `checked` | `boolean \| 'indeterminate'` | | The checked state, when your code controls it. |
23
+ | `defaultChecked` | `boolean` | | The starting state when the checkbox manages itself (uncontrolled). It's narrower than Radix's own boolean \| 'indeterminate'. The tick or dash can only follow a checked value the caller passed in. Radix would track an uncontrolled mixed state internally but never hand it back, so that state is excluded instead of being drawn with the wrong mark. |
24
+ | `onCheckedChange` | `(checked: boolean \| 'indeterminate') => void` | | Runs every time it toggles. |
25
+ | `disabled` | `boolean` | | Takes it out of the tab order and stops it toggling. |
26
+ | `required` | `boolean` | | Uses the browser's built-in form validation. |
27
+ | `name` | `string` | | The field name sent with a native form. |
28
+ | `value` | `string \| number \| readonly string[]` | | The field value sent with a native form. |
29
+ | `invalid` | `boolean` | `false` | Sets aria-invalid (an ARIA attribute, from Accessible Rich Internet Applications, that tells screen readers the field has an error) and shows the invalid border. |
30
+ | `id` | `string` | | Pair it with a real label through htmlFor. |
31
+ | `className` | `string` | | Merged with the control's classes. If two clash, the later Tailwind utility wins (Tailwind is the CSS framework this system styles with). |
32
+ | `ref` | `Ref<HTMLButtonElement>` | | Passed through to the Radix root button. |
33
+ | `...rest` | `Omit<CheckboxProps, 'aria-invalid' \| 'defaultChecked'>` | | Every Radix Checkbox.Root prop except aria-invalid, spread onto the root. aria-invalid is removed from the type, so you can't pass it, instead of being quietly overwritten while the page runs. invalid is the only thing that sets both the attribute and the border. defaultChecked is declared again above, narrowed to boolean. |
34
+
35
+ ## Examples
36
+
37
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
38
+
39
+ ### States
40
+
41
+ ```tsx
42
+ <div className="flex items-center gap-2">
43
+ <Checkbox id="state-unchecked" />
44
+ <label htmlFor="state-unchecked">Unchecked</label>
45
+ </div>
46
+ <div className="flex items-center gap-2">
47
+ <Checkbox id="state-checked" checked />
48
+ <label htmlFor="state-checked">Checked</label>
49
+ </div>
50
+ <div className="flex items-center gap-2">
51
+ <Checkbox id="state-indeterminate" checked="indeterminate" />
52
+ <label htmlFor="state-indeterminate">Mixed</label>
53
+ </div>
54
+ <div className="flex items-center gap-2">
55
+ <Checkbox id="state-disabled" disabled />
56
+ <label htmlFor="state-disabled">Disabled</label>
57
+ </div>
58
+ ```
59
+
60
+ ### A group
61
+
62
+ ```tsx
63
+ const KIT_ITEMS = ['Speed loader', 'Rail light', 'Spare battery', 'First aid kit']
64
+
65
+ const slug = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-')
66
+
67
+ function GroupExample() {
68
+ const [checkedItems, setCheckedItems] = useState<Record<string, boolean>>({
69
+ 'Speed loader': true,
70
+ 'Rail light': true,
71
+ 'Spare battery': false,
72
+ 'First aid kit': false,
73
+ })
74
+
75
+ const checkedCount = KIT_ITEMS.filter(item => checkedItems[item]).length
76
+ const parentChecked: boolean | 'indeterminate' =
77
+ checkedCount === 0 ? false : checkedCount === KIT_ITEMS.length ? true : 'indeterminate'
78
+
79
+ function setAll(value: boolean) {
80
+ setCheckedItems(Object.fromEntries(KIT_ITEMS.map(item => [item, value])))
81
+ }
82
+
83
+ return (
84
+ <div className="flex flex-col gap-2">
85
+ <div className="flex items-center gap-2">
86
+ <Checkbox
87
+ id="kit-parent"
88
+ checked={parentChecked}
89
+ onCheckedChange={checked => setAll(checked === true)}
90
+ />
91
+ <label htmlFor="kit-parent" className="font-medium">
92
+ Pack full field kit
93
+ </label>
94
+ </div>
95
+ <div className="flex flex-col gap-2 pl-6">
96
+ {KIT_ITEMS.map(item => (
97
+ <div key={item} className="flex items-center gap-2">
98
+ <Checkbox
99
+ id={`kit-item-${slug(item)}`}
100
+ checked={checkedItems[item]}
101
+ onCheckedChange={checked =>
102
+ setCheckedItems(prev => ({ ...prev, [item]: checked === true }))
103
+ }
104
+ />
105
+ <label htmlFor={`kit-item-${slug(item)}`}>{item}</label>
106
+ </div>
107
+ ))}
108
+ </div>
109
+ </div>
110
+ )
111
+ }
112
+
113
+ <GroupExample />
114
+ ```
115
+
116
+ ### Invalid
117
+
118
+ ```tsx
119
+ <div className="flex items-center gap-2">
120
+ <Checkbox id="state-invalid" invalid />
121
+ <label htmlFor="state-invalid">Confirm waiver on file</label>
122
+ </div>
123
+ ```
124
+
125
+ ## Keyboard
126
+
127
+ | Keys | Result |
128
+ |---|---|
129
+ | `Tab` | Moves focus to it. |
130
+ | `Space` | Toggles it. |
131
+ | `Enter` | Nothing. That's how a native checkbox behaves, so it isn't a bug to fix. |
132
+
133
+ ## Do
134
+
135
+ - Give it a real <label htmlFor>, or let FormField do it.
136
+ - Use the mixed state for a parent whose children are partly selected.
137
+ - Put the label on the right and make it part of the clickable target.
138
+
139
+ ## Don't
140
+
141
+ - Don't use a checkbox for something that takes effect right away. That's a Switch.
142
+ - Don't rely on the fill colour alone. The tick and the dash are what show the state.
143
+ - Don't nest checkbox groups more than one level deep.
144
+
145
+ ## Anatomy
146
+
147
+ - **Root**: A button with role=checkbox, which tells screen readers it's a checkbox. A native input can't be styled this way. The box is 20px (pixels) and the hit area, the part you can click or tap, is 24px.
148
+ - **Indicator**: A tick when checked, a dash when mixed. The shape changes, so the state never depends on colour alone.
149
+ - **Hit area**: A ::before inset of -2px, which clears the 24×24 minimum in WCAG 2.2 (the Web Content Accessibility Guidelines, the public rulebook for making websites usable by people with disabilities).
@@ -0,0 +1,128 @@
1
+ # CheckoutForm
2
+
3
+ > Shipping, delivery and payment in three steps, with the order summary beside them. It's a demo: no payment is ever taken, and the card fields say so.
4
+
5
+ - **Best for:** A demo checkout: shipping, delivery and payment in three steps beside the order summary.
6
+ - **Not for:** Taking a real payment. It's a demo and never charges.
7
+ - Organism. Source: `src/components/organisms/checkout-form/CheckoutForm.tsx`
8
+ - Rendered docs page: `/components/checkout-form`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { CheckoutForm, DELIVERY_OPTIONS } from 'src/components/organisms/checkout-form/CheckoutForm'
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
+ | `lines` | `CartLine[]` | | Required. What's being bought. The order summary lists them. |
23
+ | `countries` | `SelectOption[]` | | Required. The options for the country Select. |
24
+ | `deliveryOptions` | `DeliveryOption[]` | `DELIVERY_OPTIONS` | Prices in the currency's smallest unit. Zero shows as Free. |
25
+ | `onPlaceOrder` | `(order: CheckoutOrder) => void \| { error: string } \| Promise<void \| { error: string }>` | | Required. Gets the contact details, addresses, delivery id and total, and never a card field. If it rejects, an alert appears that lets the person try again, so the failure isn't lost. Resolving { error } shows that specific message in the same alert. |
26
+ | `headingLevel` | `2 \| 3` | `2` | The level of the 'Checkout' heading. Steps and the order summary sit one level below it, and never below ErrorSummary's own fixed h4 (level-4 heading). |
27
+ | `currency` | `string` | `'EUR'` | Passed to formatMoney. |
28
+ | `locale` | `string` | `'en-IE'` | Passed to formatMoney. |
29
+ | `className` | `string` | | Merged with the root classes. Later Tailwind utilities win. |
30
+ | `ref` | `Ref<HTMLDivElement>` | | The root. |
31
+
32
+ ## Types and helpers
33
+
34
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
35
+
36
+ ```ts
37
+ export type DeliveryOption = {
38
+ id: string
39
+ label: string
40
+ detail: string
41
+ /** In the currency's minor unit (cents for EUR). Zero shows as "Free". */
42
+ price: number
43
+ }
44
+ ```
45
+
46
+ ```ts
47
+ export const DELIVERY_OPTIONS: DeliveryOption[] = [
48
+ { id: 'standard', label: 'Standard', detail: '3 to 5 working days', price: 0 },
49
+ { id: 'express', label: 'Express', detail: 'Next working day', price: 1200 },
50
+ ]
51
+ ```
52
+
53
+ ```ts
54
+ export type CheckoutAddress = { line1: string; line2: string; city: string; postalCode: string; country: string }
55
+ ```
56
+
57
+ ```ts
58
+ /** What the form hands over. No card field ever leaves it, demo or not. */
59
+ export type CheckoutOrder = {
60
+ email: string
61
+ name: string
62
+ phone: string
63
+ shipping: CheckoutAddress
64
+ deliveryId: string
65
+ billing: CheckoutAddress | 'same-as-shipping'
66
+ /** In the currency's minor unit: the lines plus delivery. */
67
+ total: number
68
+ }
69
+ ```
70
+
71
+ ## Examples
72
+
73
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
74
+
75
+ ### Three steps, a demo order
76
+
77
+ ```tsx
78
+ const LINES: CartLine[] = [
79
+ { id: 'kestrel-c', name: 'Kestrel C carbine replica', unitPrice: 13900, quantity: 1 },
80
+ { id: 'prism-holo-sight', name: 'Prism holo sight', unitPrice: 4900, quantity: 2 },
81
+ { id: 'beam-600', name: 'Beam 600 rail light', unitPrice: 2400, quantity: 1 },
82
+ ]
83
+
84
+ const COUNTRIES = [
85
+ { value: 'ie', label: 'Ireland' },
86
+ { value: 'ro', label: 'Romania' },
87
+ { value: 'de', label: 'Germany' },
88
+ { value: 'fr', label: 'France' },
89
+ { value: 'es', label: 'Spain' },
90
+ ]
91
+
92
+ const wait = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms))
93
+
94
+ <CheckoutForm lines={LINES} countries={COUNTRIES} onPlaceOrder={() => wait(800).then(() => undefined)} headingLevel={3} />
95
+ ```
96
+
97
+ ## Keyboard
98
+
99
+ | Keys | Result |
100
+ |---|---|
101
+ | `Tab` | Moves through the fields, then Back and Continue. |
102
+ | `Enter` | In a field, goes on to the next step, or places the order on the last one. |
103
+ | `↑` `↓` | In the delivery options, moves and picks. |
104
+ | `Space` | On the billing checkbox, toggles it. |
105
+
106
+ ## Do
107
+
108
+ - Pass the cart lines, so the order summary and the total come from the same numbers.
109
+ - Keep the demo notice whenever the card fields are fake.
110
+ - Store prices in the currency's smallest unit (cents for EUR).
111
+
112
+ ## Don't
113
+
114
+ - Don't make the Stepper clickable. Back and Continue move between steps.
115
+ - Don't ask for the billing address unless it's different from shipping.
116
+ - Don't validate a card number in a demo. Nothing's charged.
117
+
118
+ ## Anatomy
119
+
120
+ - **Stepper**: Shows where the person is. It isn't clickable, so nobody can skip to a step whose fields don't exist yet.
121
+ - **Step heading**: Read as 'Step 2 of 3: Delivery' by a screen reader. Focus (where keyboard input goes) lands here on Continue and Back.
122
+ - **Error summary**: Lists the step's missing or wrong fields and takes focus. Each entry jumps to its field.
123
+ - **Delivery options**: A radio group with prices, and nothing picked in advance.
124
+ - **Billing**: Uses the shipping address by default. Unticking the box asks for a billing address.
125
+ - **Demo notice**: An info Alert above the card fields saying no payment is taken.
126
+ - **Order summary**: Lines, subtotal, delivery and total, formatted by formatMoney.
127
+ - **Confirmation**: A status line that's always on the page and confirms the order.
128
+ - **Placement alert**: Also always on the page. If onPlaceOrder rejects, or resolves { error } with a specific message, this alert says so, instead of the button just quietly stopping being busy. Everything already typed stays put for another try.