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.
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 +189 -7
  74. package/public/manifest/tokens.json +39 -0
  75. package/scripts/division-ds.mjs +76 -37
  76. package/scripts/lib/agent-docs.mjs +59 -2
  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,242 @@
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
+ ## Requires
17
+
18
+ - A height, because the thread scrolls and the page doesn't. Use h-[28rem], or flex-1 min-h-0 inside a column.
19
+ - ChatMessage children, so the thread can announce each finished reply.
20
+
21
+ ## Props
22
+
23
+ | Prop | Type | Default | Description |
24
+ |---|---|---|---|
25
+ | `label` | `string` | | Required. Names the log. |
26
+ | `busy` | `boolean` | `false` | The assistant is working. Shows a working line and makes one announcement when it starts. |
27
+ | `busyLabel` | `string` | `'Assistant is working'` | The working line's words, and what gets announced. |
28
+ | `jumpLabel` | `string` | `'Jump to latest'` | The jump button's words. |
29
+ | `className` | `string` | | Goes on the root. Give the thread a height here. |
30
+
31
+ ## Examples
32
+
33
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
34
+
35
+ ### A live conversation: send anything, watch the reply stream in, press Stop to end it, or scroll up while it's still arriving
36
+
37
+ ```tsx
38
+ type Turn =
39
+ | { id: number; role: 'user'; text: string }
40
+ | { id: number; role: 'assistant'; text: string; tool?: ToolCallStatus; approval?: boolean }
41
+ | { id: number; role: 'system'; text: string }
42
+
43
+ const REPLIES = [
44
+ '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.',
45
+ '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.',
46
+ ]
47
+
48
+ const HISTORY: Turn[] = [
49
+ { id: 1, role: 'user', text: 'Summarise stock at Depot 4.' },
50
+ {
51
+ id: 2,
52
+ role: 'assistant',
53
+ 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.',
54
+ tool: 'done',
55
+ approval: true,
56
+ },
57
+ ]
58
+
59
+ function LiveConversation() {
60
+ const [turns, setTurns] = useState<Turn[]>(HISTORY)
61
+ const [draft, setDraft] = useState('')
62
+ const [phase, setPhase] = useState<'idle' | 'tool' | 'streaming'>('idle')
63
+ const [approval, setApproval] = useState<ApprovalState>('pending')
64
+ const [replyIndex, setReplyIndex] = useState(0)
65
+ const nextId = useRef(HISTORY.length + 1)
66
+ const activeId = useRef<number | null>(null)
67
+ const reply = useStreamedText(REPLIES[replyIndex % REPLIES.length], { active: phase === 'streaming' })
68
+
69
+ const updateActive = (change: (turn: Turn & { role: 'assistant' }) => Turn) =>
70
+ setTurns(list => list.map(turn => (turn.id === activeId.current && turn.role === 'assistant' ? change(turn) : turn)))
71
+
72
+ const addStopped = () => {
73
+ const id = nextId.current++
74
+ setTurns(list => [...list, { id, role: 'system', text: 'Assistant stopped' }])
75
+ }
76
+
77
+ // The tool "runs" for a moment, then the reply starts streaming.
78
+ useEffect(() => {
79
+ if (phase !== 'tool') return
80
+ const timer = window.setTimeout(() => {
81
+ updateActive(turn => ({ ...turn, tool: 'done' }))
82
+ setPhase('streaming')
83
+ }, 1200)
84
+ return () => window.clearTimeout(timer)
85
+ }, [phase])
86
+
87
+ // Finished or stopped: keep what arrived, and hand the thread a message that is no longer streaming.
88
+ useEffect(() => {
89
+ if (phase !== 'streaming' || !reply.done) return
90
+ updateActive(turn => ({ ...turn, text: reply.text }))
91
+ if (reply.stopped) addStopped()
92
+ setPhase('idle')
93
+ setReplyIndex(index => index + 1)
94
+ }, [phase, reply.done, reply.text, reply.stopped])
95
+
96
+ const send = (text: string) => {
97
+ const question = nextId.current++
98
+ const answer = nextId.current++
99
+ activeId.current = answer
100
+ setTurns(list => [...list, { id: question, role: 'user', text }, { id: answer, role: 'assistant', text: '', tool: 'running' }])
101
+ setDraft('')
102
+ setPhase('tool')
103
+ }
104
+
105
+ const stop = () => {
106
+ if (phase === 'streaming') {
107
+ reply.stop()
108
+ return
109
+ }
110
+ // Stopped before any text: the empty reply goes, the system line says what happened.
111
+ setTurns(list => list.filter(turn => turn.id !== activeId.current))
112
+ addStopped()
113
+ setPhase('idle')
114
+ }
115
+
116
+ const busy = phase !== 'idle'
117
+
118
+ return (
119
+ <div className="flex w-full max-w-2xl flex-col border border-[var(--color-border-hairline-strong)]">
120
+ <ChatThread label="Conversation with the assistant" busy={phase === 'tool'} className="h-[28rem]">
121
+ {turns.map(turn => {
122
+ if (turn.role === 'user') {
123
+ return (
124
+ <ChatMessage key={turn.id} role="user" author="You">
125
+ {turn.text}
126
+ </ChatMessage>
127
+ )
128
+ }
129
+ if (turn.role === 'system') {
130
+ return (
131
+ <ChatMessage key={turn.id} role="system" author="Status">
132
+ {turn.text}
133
+ </ChatMessage>
134
+ )
135
+ }
136
+ const streaming = phase === 'streaming' && turn.id === activeId.current
137
+ const text = streaming ? reply.text : turn.text
138
+ return (
139
+ <ChatMessage key={turn.id} role="assistant" author="Assistant" streaming={streaming}>
140
+ {turn.tool === undefined ? null : (
141
+ <ToolCall
142
+ name="query_stock_levels"
143
+ title="Read stock levels at Depot 4"
144
+ status={turn.tool}
145
+ duration={turn.tool === 'done' ? '1.2 s' : undefined}
146
+ input={'{ "depot": 4 }'}
147
+ output={turn.tool === 'done' ? '{ "on_hand": 1240, "lines": 38, "below_reorder_point": 2 }' : undefined}
148
+ />
149
+ )}
150
+ {text === '' ? null : <p>{text}</p>}
151
+ {turn.approval ? (
152
+ <ApprovalRequest
153
+ title="Send this reorder to the supplier?"
154
+ state={approval}
155
+ onApprove={() => setApproval('approved')}
156
+ onReject={() => setApproval('rejected')}
157
+ approveLabel="Send reorder"
158
+ rejectLabel="Don't send"
159
+ >
160
+ <p>To: Arad replica supply</p>
161
+ <p>10 × K-41 field radio, 8 × trail pack, to Depot 4 by Thursday 22 October.</p>
162
+ </ApprovalRequest>
163
+ ) : null}
164
+ </ChatMessage>
165
+ )
166
+ })}
167
+ </ChatThread>
168
+ <PromptInput
169
+ label="Message the assistant"
170
+ placeholder="Ask about stock, orders or depots"
171
+ value={draft}
172
+ onValueChange={setDraft}
173
+ onSubmit={send}
174
+ busy={busy}
175
+ onStop={stop}
176
+ className="p-4 border-t border-[var(--color-border-hairline-strong)]"
177
+ />
178
+ </div>
179
+ )
180
+ }
181
+
182
+ <LiveConversation />
183
+ ```
184
+
185
+ ## Keyboard
186
+
187
+ | Keys | Result |
188
+ |---|---|
189
+ | `Tab` | Moves to the log, then into the controls inside messages, then to Jump to latest when it's shown. |
190
+ | `ArrowUp` `ArrowDown` | With the log focused, scrolls it. |
191
+ | `PageUp` `PageDown` | With the log focused, scrolls it a page at a time. |
192
+
193
+ ## Do
194
+
195
+ - Give the thread a fixed or flexible height so the thread scrolls, not the page.
196
+ - Set busy as soon as the request starts. Clear it when the last fragment has arrived or the reply is stopped.
197
+ - After a Stop, add a system line ("Assistant stopped") so the record and the announcement both say what happened.
198
+ - Keep the PromptInput outside the thread, directly under it.
199
+
200
+ ## Don't
201
+
202
+ - Don't scroll the thread yourself on new messages. It follows the end only while the reader is there.
203
+ - Don't move focus into the thread when a reply arrives. Focus stays where the person put it.
204
+ - 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.
205
+
206
+ ## Common mistakes
207
+
208
+ Wrong:
209
+
210
+ ```tsx
211
+ <div role="log" aria-live="polite">{messages}</div>
212
+ ```
213
+
214
+ Right:
215
+
216
+ ```tsx
217
+ <ChatThread label="Conversation with the assistant">{messages}</ChatThread>
218
+ ```
219
+
220
+ 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.
221
+
222
+ Wrong:
223
+
224
+ ```tsx
225
+ useEffect(() => endRef.current?.scrollIntoView({ behavior: "smooth" }), [messages])
226
+ ```
227
+
228
+ Right:
229
+
230
+ ```tsx
231
+ <ChatThread ...> // follows the end only while the reader is there
232
+ ```
233
+
234
+ 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.
235
+
236
+ ## Anatomy
237
+
238
+ - **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.
239
+ - **Messages**: ChatMessage children, oldest first, 24px (pixels) apart.
240
+ - **Working line**: Shown while busy: the conveyor Spinner and "Assistant is working" at the end of the log.
241
+ - **Jump to latest**: Appears only when the reader has scrolled away from the end. It jumps instantly and puts focus on the log.
242
+ - **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,147 @@
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
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `checked` | `boolean \| 'indeterminate'` | | The checked state, when your code controls it. |
21
+ | `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. |
22
+ | `onCheckedChange` | `(checked: boolean \| 'indeterminate') => void` | | Runs every time it toggles. |
23
+ | `disabled` | `boolean` | | Takes it out of the tab order and stops it toggling. |
24
+ | `required` | `boolean` | | Uses the browser's built-in form validation. |
25
+ | `name` | `string` | | The field name sent with a native form. |
26
+ | `value` | `string \| number \| readonly string[]` | | The field value sent with a native form. |
27
+ | `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. |
28
+ | `id` | `string` | | Pair it with a real label through htmlFor. |
29
+ | `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). |
30
+ | `ref` | `Ref<HTMLButtonElement>` | | Passed through to the Radix root button. |
31
+ | `...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. |
32
+
33
+ ## Examples
34
+
35
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
36
+
37
+ ### States
38
+
39
+ ```tsx
40
+ <div className="flex items-center gap-2">
41
+ <Checkbox id="state-unchecked" />
42
+ <label htmlFor="state-unchecked">Unchecked</label>
43
+ </div>
44
+ <div className="flex items-center gap-2">
45
+ <Checkbox id="state-checked" checked />
46
+ <label htmlFor="state-checked">Checked</label>
47
+ </div>
48
+ <div className="flex items-center gap-2">
49
+ <Checkbox id="state-indeterminate" checked="indeterminate" />
50
+ <label htmlFor="state-indeterminate">Mixed</label>
51
+ </div>
52
+ <div className="flex items-center gap-2">
53
+ <Checkbox id="state-disabled" disabled />
54
+ <label htmlFor="state-disabled">Disabled</label>
55
+ </div>
56
+ ```
57
+
58
+ ### A group
59
+
60
+ ```tsx
61
+ const KIT_ITEMS = ['Speed loader', 'Rail light', 'Spare battery', 'First aid kit']
62
+
63
+ const slug = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-')
64
+
65
+ function GroupExample() {
66
+ const [checkedItems, setCheckedItems] = useState<Record<string, boolean>>({
67
+ 'Speed loader': true,
68
+ 'Rail light': true,
69
+ 'Spare battery': false,
70
+ 'First aid kit': false,
71
+ })
72
+
73
+ const checkedCount = KIT_ITEMS.filter(item => checkedItems[item]).length
74
+ const parentChecked: boolean | 'indeterminate' =
75
+ checkedCount === 0 ? false : checkedCount === KIT_ITEMS.length ? true : 'indeterminate'
76
+
77
+ function setAll(value: boolean) {
78
+ setCheckedItems(Object.fromEntries(KIT_ITEMS.map(item => [item, value])))
79
+ }
80
+
81
+ return (
82
+ <div className="flex flex-col gap-2">
83
+ <div className="flex items-center gap-2">
84
+ <Checkbox
85
+ id="kit-parent"
86
+ checked={parentChecked}
87
+ onCheckedChange={checked => setAll(checked === true)}
88
+ />
89
+ <label htmlFor="kit-parent" className="font-medium">
90
+ Pack full field kit
91
+ </label>
92
+ </div>
93
+ <div className="flex flex-col gap-2 pl-6">
94
+ {KIT_ITEMS.map(item => (
95
+ <div key={item} className="flex items-center gap-2">
96
+ <Checkbox
97
+ id={`kit-item-${slug(item)}`}
98
+ checked={checkedItems[item]}
99
+ onCheckedChange={checked =>
100
+ setCheckedItems(prev => ({ ...prev, [item]: checked === true }))
101
+ }
102
+ />
103
+ <label htmlFor={`kit-item-${slug(item)}`}>{item}</label>
104
+ </div>
105
+ ))}
106
+ </div>
107
+ </div>
108
+ )
109
+ }
110
+
111
+ <GroupExample />
112
+ ```
113
+
114
+ ### Invalid
115
+
116
+ ```tsx
117
+ <div className="flex items-center gap-2">
118
+ <Checkbox id="state-invalid" invalid />
119
+ <label htmlFor="state-invalid">Confirm waiver on file</label>
120
+ </div>
121
+ ```
122
+
123
+ ## Keyboard
124
+
125
+ | Keys | Result |
126
+ |---|---|
127
+ | `Tab` | Moves focus to it. |
128
+ | `Space` | Toggles it. |
129
+ | `Enter` | Nothing. That's how a native checkbox behaves, so it isn't a bug to fix. |
130
+
131
+ ## Do
132
+
133
+ - Give it a real <label htmlFor>, or let FormField do it.
134
+ - Use the mixed state for a parent whose children are partly selected.
135
+ - Put the label on the right and make it part of the clickable target.
136
+
137
+ ## Don't
138
+
139
+ - Don't use a checkbox for something that takes effect right away. That's a Switch.
140
+ - Don't rely on the fill colour alone. The tick and the dash are what show the state.
141
+ - Don't nest checkbox groups more than one level deep.
142
+
143
+ ## Anatomy
144
+
145
+ - **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.
146
+ - **Indicator**: A tick when checked, a dash when mixed. The shape changes, so the state never depends on colour alone.
147
+ - **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,126 @@
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
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `lines` | `CartLine[]` | | Required. What's being bought. The order summary lists them. |
21
+ | `countries` | `SelectOption[]` | | Required. The options for the country Select. |
22
+ | `deliveryOptions` | `DeliveryOption[]` | `DELIVERY_OPTIONS` | Prices in the currency's smallest unit. Zero shows as Free. |
23
+ | `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. |
24
+ | `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). |
25
+ | `currency` | `string` | `'EUR'` | Passed to formatMoney. |
26
+ | `locale` | `string` | `'en-IE'` | Passed to formatMoney. |
27
+ | `className` | `string` | | Merged with the root classes. Later Tailwind utilities win. |
28
+ | `ref` | `Ref<HTMLDivElement>` | | The root. |
29
+
30
+ ## Types and helpers
31
+
32
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
33
+
34
+ ```ts
35
+ export type DeliveryOption = {
36
+ id: string
37
+ label: string
38
+ detail: string
39
+ /** In the currency's minor unit (cents for EUR). Zero shows as "Free". */
40
+ price: number
41
+ }
42
+ ```
43
+
44
+ ```ts
45
+ export const DELIVERY_OPTIONS: DeliveryOption[] = [
46
+ { id: 'standard', label: 'Standard', detail: '3 to 5 working days', price: 0 },
47
+ { id: 'express', label: 'Express', detail: 'Next working day', price: 1200 },
48
+ ]
49
+ ```
50
+
51
+ ```ts
52
+ export type CheckoutAddress = { line1: string; line2: string; city: string; postalCode: string; country: string }
53
+ ```
54
+
55
+ ```ts
56
+ /** What the form hands over. No card field ever leaves it, demo or not. */
57
+ export type CheckoutOrder = {
58
+ email: string
59
+ name: string
60
+ phone: string
61
+ shipping: CheckoutAddress
62
+ deliveryId: string
63
+ billing: CheckoutAddress | 'same-as-shipping'
64
+ /** In the currency's minor unit: the lines plus delivery. */
65
+ total: number
66
+ }
67
+ ```
68
+
69
+ ## Examples
70
+
71
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
72
+
73
+ ### Three steps, a demo order
74
+
75
+ ```tsx
76
+ const LINES: CartLine[] = [
77
+ { id: 'kestrel-c', name: 'Kestrel C carbine replica', unitPrice: 13900, quantity: 1 },
78
+ { id: 'prism-holo-sight', name: 'Prism holo sight', unitPrice: 4900, quantity: 2 },
79
+ { id: 'beam-600', name: 'Beam 600 rail light', unitPrice: 2400, quantity: 1 },
80
+ ]
81
+
82
+ const COUNTRIES = [
83
+ { value: 'ie', label: 'Ireland' },
84
+ { value: 'ro', label: 'Romania' },
85
+ { value: 'de', label: 'Germany' },
86
+ { value: 'fr', label: 'France' },
87
+ { value: 'es', label: 'Spain' },
88
+ ]
89
+
90
+ const wait = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms))
91
+
92
+ <CheckoutForm lines={LINES} countries={COUNTRIES} onPlaceOrder={() => wait(800).then(() => undefined)} headingLevel={3} />
93
+ ```
94
+
95
+ ## Keyboard
96
+
97
+ | Keys | Result |
98
+ |---|---|
99
+ | `Tab` | Moves through the fields, then Back and Continue. |
100
+ | `Enter` | In a field, goes on to the next step, or places the order on the last one. |
101
+ | `↑` `↓` | In the delivery options, moves and picks. |
102
+ | `Space` | On the billing checkbox, toggles it. |
103
+
104
+ ## Do
105
+
106
+ - Pass the cart lines, so the order summary and the total come from the same numbers.
107
+ - Keep the demo notice whenever the card fields are fake.
108
+ - Store prices in the currency's smallest unit (cents for EUR).
109
+
110
+ ## Don't
111
+
112
+ - Don't make the Stepper clickable. Back and Continue move between steps.
113
+ - Don't ask for the billing address unless it's different from shipping.
114
+ - Don't validate a card number in a demo. Nothing's charged.
115
+
116
+ ## Anatomy
117
+
118
+ - **Stepper**: Shows where the person is. It isn't clickable, so nobody can skip to a step whose fields don't exist yet.
119
+ - **Step heading**: Read as 'Step 2 of 3: Delivery' by a screen reader. Focus (where keyboard input goes) lands here on Continue and Back.
120
+ - **Error summary**: Lists the step's missing or wrong fields and takes focus. Each entry jumps to its field.
121
+ - **Delivery options**: A radio group with prices, and nothing picked in advance.
122
+ - **Billing**: Uses the shipping address by default. Unticking the box asks for a billing address.
123
+ - **Demo notice**: An info Alert above the card fields saying no payment is taken.
124
+ - **Order summary**: Lines, subtotal, delivery and total, formatted by formatMoney.
125
+ - **Confirmation**: A status line that's always on the page and confirms the order.
126
+ - **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.
@@ -0,0 +1,145 @@
1
+ # CommandPalette
2
+
3
+ > Every action in one place. Press Ctrl+K anywhere, type a few letters, move with the arrow keys and press Enter. Focus (where keyboard input goes) returns to where it was, and then the command runs.
4
+
5
+ - **Best for:** Every action in one place behind Ctrl+K, with a visible button that opens it too.
6
+ - **Not for:** Searching page content: use Search. A few actions on one control: use DropdownMenu.
7
+ - Organism. Source: `src/components/organisms/command-palette/CommandPalette.tsx`
8
+ - Rendered docs page: `/components/command-palette`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { CommandPalette, filterCommands, groupCommands } from 'src/components/organisms/command-palette/CommandPalette'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `commands` | `Command[]` | | Required. Each has id, label, group, keywords, shortcut, icon and onSelect. |
21
+ | `open` | `boolean` | | Open state when controlled, meaning your code decides whether it is open. |
22
+ | `defaultOpen` | `boolean` | `false` | Starting open state when uncontrolled, meaning the palette keeps track of it itself. |
23
+ | `onOpenChange` | `(open: boolean) => void` | | Called on every open and close, including Escape and a click on the scrim (the dimmed backdrop). If you refuse the close, the chosen command stays ready to run until the user does anything else in the palette. Closing it yourself later counts as accepting it. |
24
+ | `shortcut` | `boolean` | `true` | Ctrl+K or Cmd+K opens and closes the palette from anywhere. |
25
+ | `label` | `string` | `'Command palette'` | The dialog's accessible name (what a screen reader announces), hidden from view. |
26
+ | `searchLabel` | `string` | `'Search commands'` | The search field's accessible name. |
27
+ | `placeholder` | `string` | `'Type a command or search'` | Shown in the empty search field. |
28
+ | `emptyMessage` | `string` | `'No matching commands'` | Shown when nothing matches the search. |
29
+ | `ref` | `Ref<HTMLInputElement>` | | The search field, while the palette is open. |
30
+
31
+ ## Types and helpers
32
+
33
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
34
+
35
+ ```ts
36
+ export type Command = {
37
+ id: string
38
+ label: string
39
+ /** Commands with the same group are drawn together under its heading. */
40
+ group?: string
41
+ /** Extra words a search matches on. */
42
+ keywords?: string[]
43
+ /** A hint shown on the option, e.g. "N". Display only: the palette does not bind it. */
44
+ shortcut?: string
45
+ /** Drawn aria-hidden; the label carries the meaning. */
46
+ icon?: ReactNode
47
+ onSelect: () => void
48
+ }
49
+ ```
50
+
51
+ ```ts
52
+ export type CommandGroup = { group?: string; commands: Command[] }
53
+ ```
54
+
55
+ ```ts
56
+ /** Every whitespace-separated term must appear in the label, the group or a keyword, ignoring case.
57
+ * A label that starts with the first term ranks first; otherwise the given order holds. */
58
+ export function filterCommands(commands: readonly Command[], query: string): Command[]
59
+ ```
60
+
61
+ ```ts
62
+ /** Groups in the order each group first appears; order within a group is kept. */
63
+ export function groupCommands(commands: readonly Command[]): CommandGroup[]
64
+ ```
65
+
66
+ ## Examples
67
+
68
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
69
+
70
+ ### Store commands
71
+
72
+ ```tsx
73
+ function StoreCommands() {
74
+ const [open, setOpen] = useState(false)
75
+ const [last, setLast] = useState('none')
76
+ const make = (id: string, label: string, group: string, icon: ReactNode, extra: Partial<Command> = {}): Command => ({
77
+ id,
78
+ label,
79
+ group,
80
+ icon,
81
+ onSelect: () => setLast(label),
82
+ ...extra,
83
+ })
84
+ const commands = [
85
+ make('orders', 'Go to orders', 'Navigate', <Package />, { keywords: ['sales'] }),
86
+ make('shipments', 'Go to shipments', 'Navigate', <Truck />),
87
+ make('returns', 'Go to returns', 'Navigate', <Inbox />),
88
+ make('new-shipment', 'New shipment', 'Create', <Plus />, { shortcut: 'N' }),
89
+ make('new-product', 'New product', 'Create', <Box />),
90
+ make('invite', 'Invite a teammate', 'Account', <Users />, { keywords: ['team', 'people'] }),
91
+ make('settings', 'Open settings', 'Account', <Settings />),
92
+ ]
93
+ return (
94
+ <div className="flex flex-wrap items-center gap-3">
95
+ <Button variant="secondary" aria-keyshortcuts="Control+K Meta+K" onClick={() => setOpen(true)}>
96
+ Open command palette
97
+ </Button>
98
+ <span aria-hidden="true" className="inline-flex gap-1">
99
+ <Kbd>Ctrl</Kbd>
100
+ <Kbd>K</Kbd>
101
+ </span>
102
+ <p role="status" className="font-mono text-xs text-[var(--color-text-secondary)]">
103
+ Last command: {last}
104
+ </p>
105
+ <CommandPalette commands={commands} open={open} onOpenChange={setOpen} />
106
+ </div>
107
+ )
108
+ }
109
+
110
+ <StoreCommands />
111
+ ```
112
+
113
+ ## Keyboard
114
+
115
+ | Keys | Result |
116
+ |---|---|
117
+ | `Ctrl` `K` | Opens or closes it from anywhere on the page (Cmd+K on macOS). |
118
+ | `↓` | Next command, wrapping round to the first. |
119
+ | `↑` | Previous command, wrapping round to the last. |
120
+ | `Home` | First command. |
121
+ | `End` | Last command. |
122
+ | `Enter` | Runs the active command. |
123
+ | `Esc` | Closes without running anything. |
124
+
125
+ ## Do
126
+
127
+ - Name commands as verbs: 'Go to orders', 'New shipment'.
128
+ - Add keywords for the words people actually type.
129
+ - Give it a visible button that opens it too. People won't find a shortcut on its own.
130
+
131
+ ## Don't
132
+
133
+ - Don't put destructive commands in it without a dialog that asks for confirmation.
134
+ - Don't show a shortcut hint the app doesn't actually set up.
135
+ - Don't put more than one palette on a page. Each one listens for the same Ctrl+K/Cmd+K across the whole page, so a second one fights the first for the shortcut.
136
+
137
+ ## Anatomy
138
+
139
+ - **Dialog**: Named 'Command palette', with the name hidden from view. Drawn on surface.base with the corner crosshair.
140
+ - **Search field**: A combobox, a text field tied to a list of choices. Focus stays here the whole time.
141
+ - **Listbox**: Named 'Commands'. It's not there at all when nothing matches.
142
+ - **Group**: Named by its heading.
143
+ - **Option**: The active one gets the fill, and the search field points to it as its active descendant, so screen readers announce it while focus stays in the field.
144
+ - **Count**: A status line hidden from view but read by screen readers: '2 commands'.
145
+ - **Shortcut hint**: A Kbd that's hidden from assistive technology (screen readers and similar tools). It's for display only.