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,70 @@
1
+ # Kbd
2
+
3
+ > A small keycap that shows a key's name. Making shortcuts easy to find was one of the motifs from the G1 mood board (the project's first approval step) worth keeping: if a surface has a shortcut, the surface says so.
4
+
5
+ - **Best for:** Showing a keyboard shortcut next to the action it triggers, with one Kbd per key.
6
+ - **Not for:** Anything that isn't a key, such as a status: use Badge.
7
+ - Atom. Source: `src/components/atoms/kbd/Kbd.tsx`
8
+ - Rendered docs page: `/components/kbd`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Kbd } from 'src/components/atoms/kbd/Kbd'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `children` | `ReactNode` | | The key's name, for example "Esc" or "⌘". |
21
+ | `className` | `string` | | Merged with the component's classes. When Tailwind utility classes clash, the later one wins. |
22
+ | `ref` | `Ref<HTMLElement>` | | Passed through to the kbd element. |
23
+ | `...rest` | `HTMLAttributes<HTMLElement>` | | Every native attribute, passed onto the kbd element, such as a title, or an aria-label for a symbol key (aria-label is the name screen readers read out). |
24
+
25
+ ## Examples
26
+
27
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
28
+
29
+ ### Single keys
30
+
31
+ ```tsx
32
+ <Kbd>Esc</Kbd>
33
+ <Kbd>Tab</Kbd>
34
+ <Kbd>Enter</Kbd>
35
+ <Kbd>Space</Kbd>
36
+ <Kbd>/</Kbd>
37
+ ```
38
+
39
+ ### A chord: one cap per key
40
+
41
+ ```tsx
42
+ <span className="flex items-center gap-1">
43
+ <Kbd>Ctrl</Kbd>
44
+ <span className="text-[var(--color-text-muted)] text-sm">+</span>
45
+ <Kbd>K</Kbd>
46
+ </span>
47
+ <span className="text-sm text-[var(--color-text-secondary)]">Open the command palette</span>
48
+ ```
49
+
50
+ ## Keyboard
51
+
52
+ Not focusable. No keyboard behaviour of its own.
53
+
54
+ ## Do
55
+
56
+ - Use one Kbd per physical key, and put them side by side for a chord (keys pressed together).
57
+ - Put it next to the action it triggers, in the same row or the same tooltip.
58
+ - Write the key name the way the keyboard prints it.
59
+
60
+ ## Don't
61
+
62
+ - Don’t bevel it. Cut corners are for surfaces and controls, not 24px chrome.
63
+ - Don’t put a whole chord inside one Kbd ("Ctrl+K" in a single cap).
64
+ - Don’t use it for anything that isn’t a key. It’s not a badge.
65
+
66
+ ## Anatomy
67
+
68
+ - **kbd element**: The HTML element made for keys. Screen readers announce it as keyboard input.
69
+ - **Hairline border**: A one-pixel border in border.hairline-strong, with no bevel (cut corners).
70
+ - **Mono legend**: JetBrains Mono, uppercase, 11px (pixels): the tertiary type role.
@@ -0,0 +1,96 @@
1
+ # Link
2
+
3
+ > A link inside text. It's underlined, because in a paragraph colour alone isn't enough to show that something is a link.
4
+
5
+ - **Best for:** Text that takes you somewhere: another page, a section or another site.
6
+ - **Not for:** Something that does an action. Use Button for that (its link variant if it's inside running text).
7
+ - Atom. Source: `src/components/atoms/link/Link.tsx`
8
+ - Rendered docs page: `/components/link`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Link, linkVariants } from 'src/components/atoms/link/Link'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `tone` | `'signal' \| 'quiet'` | `'signal'` | Which colour treatment to use. |
21
+ | `external` | `boolean` | `false` | Opens the link in a new tab. It adds target="_blank", the rel hardening (attributes that stop the new page reaching back into yours), a trailing icon and the hidden "(opens in a new tab)" warning. Using it together with target or rel is a type error, so it can't fail silently. external already sets those values, so those props are removed from the type instead of being quietly overwritten while the page runs. |
22
+ | `href` | `string` | | Where the link goes. |
23
+ | `className` | `string` | | Merged with the component classes. If two clash, the later Tailwind utility wins (Tailwind is the CSS framework this system styles with). |
24
+ | `ref` | `Ref<HTMLAnchorElement>` | | Passed through to the anchor element. |
25
+ | `...rest` | `AnchorHTMLAttributes<HTMLAnchorElement>` | | Every native anchor prop. |
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 const linkVariants = cva( …
33
+ ```
34
+
35
+ ## Examples
36
+
37
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
38
+
39
+ ### In running text
40
+
41
+ ```tsx
42
+ <p className="text-sm text-[var(--color-text-secondary)] max-w-md">
43
+ Check the <Link href="/kits">field kit index</Link> before you head out, and read the{' '}
44
+ <Link href="https://example.com" external tone="quiet">
45
+ supplier notes
46
+ </Link>{' '}
47
+ if the hop-up looks off.
48
+ </p>
49
+ ```
50
+
51
+ ### Tones
52
+
53
+ ```tsx
54
+ <Link href="/kits" tone="signal">
55
+ Field kits
56
+ </Link>
57
+ <Link href="/kits" tone="quiet">
58
+ Field kits
59
+ </Link>
60
+ ```
61
+
62
+ ### External
63
+
64
+ ```tsx
65
+ <Link href="https://example.com" external>
66
+ Supplier
67
+ </Link>
68
+ ```
69
+
70
+ ## Keyboard
71
+
72
+ | Keys | Result |
73
+ |---|---|
74
+ | `Tab` | Moves focus to it. The focus ring (the outline that shows which element the keyboard is on) is drawn on the link itself, and nothing clips it here. |
75
+ | `Enter` | Follows the link. |
76
+
77
+ ## Do
78
+
79
+ - Keep the underline on links inside running text.
80
+ - Put the destination in the link text: 'the field kit index', not 'click here'.
81
+ - Set external on anything that leaves the app, so screen readers announce the new tab.
82
+ - Use quiet in dense lists where every row is a link anyway.
83
+
84
+ ## Don't
85
+
86
+ - Don't use a link for something that does an action. That's a Button.
87
+ - Don't open a new tab without external. The rel hardening and the warning both come with it.
88
+ - Don't remove the underline to make a paragraph look tidier.
89
+ - Don't pass target or rel together with external. Those props are removed from the type, so you can't override them. If you need different values, render a plain anchor yourself.
90
+
91
+ ## Anatomy
92
+
93
+ - **Anchor**: The browser's own <a> element. You can focus it and use it from the keyboard without any extra work.
94
+ - **Underline**: 1px (pixel) thick, sitting 2px below the text. It gets thicker on hover in the signal tone.
95
+ - **External icon**: Hidden from screen readers with aria-hidden (an ARIA attribute, from Accessible Rich Internet Applications). It only shows when external is set.
96
+ - **Hidden warning**: ‘(opens in a new tab)’, added to the accessible name, which is what screen readers announce for the link.
@@ -0,0 +1,238 @@
1
+ # ListItem
2
+
3
+ > One row of a list: something at the start (a picture or icon), a title and description, extra details on the right, and actions at the end. A row that goes somewhere is one real link or button stretched over the whole row. Its actions sit beside it, never inside it.
4
+
5
+ - **Best for:** A row people scan in a List: media, a title, meta and one or two actions.
6
+ - **Not for:** Data in a table that people compare across columns: use DataTable.
7
+ - Molecule. Source: `src/components/molecules/list-item/ListItem.tsx`
8
+ - Rendered docs page: `/components/list-item`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { List, ListItem } from 'src/components/molecules/list-item/ListItem'
14
+ ```
15
+
16
+ ## Requires
17
+
18
+ - A <List> around the rows (a ul with role="list"), or as="div" on a row that stands alone.
19
+
20
+ ## Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |---|---|---|---|
24
+ | `title` | `ReactNode` | | Required. The main line, and the name screen readers give the row’s link or button. |
25
+ | `description` | `ReactNode` | | A second line under the title, in secondary text. |
26
+ | `media` | `ReactNode` | | Something at the start of the row: an Avatar, an icon (hidden from screen readers with aria-hidden), a thumbnail. |
27
+ | `meta` | `ReactNode` | | Right-aligned monospaced text: a time, a price, a count. |
28
+ | `actions` | `ReactNode` | | Controls at the end. Each one can still be focused and clicked on its own. |
29
+ | `href` | `string` | | Makes the row one link to this address, named by the title. |
30
+ | `renderLink` | `(props: LinkRenderProps) => ReactNode` | `a plain <a>` | Draws the row link when href is set. Pass your router's link here. It has to pass className through. |
31
+ | `onActivate` | `() => void` | | Makes the row one button, named by the title. Ignored when href is set. |
32
+ | `as` | `'li' \| 'div'` | `'li'` | li inside a List. div for a row on its own. |
33
+ | `dividers (List)` | `boolean` | `true` | A thin line between rows. |
34
+ | `className` | `string` | | Extra CSS classes, merged with the outer element's own classes on List or ListItem. |
35
+ | `ref` | `Ref<HTMLElement>` | | Passed through to the ul (List) or the row element (ListItem). |
36
+
37
+ ## Examples
38
+
39
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
40
+
41
+ ### Sessions: plain rows with one action each
42
+
43
+ ```tsx
44
+ function DeviceIcon({ icon: Icon }: { icon: typeof Monitor }) {
45
+ return (
46
+ <span className="flex size-9 items-center justify-center border border-[var(--color-border-hairline-strong)] text-[var(--color-text-secondary)]">
47
+ <Icon aria-hidden="true" className="size-4" />
48
+ </span>
49
+ )
50
+ }
51
+
52
+ function SessionsExample() {
53
+ const [sessions, setSessions] = useState([
54
+ { id: 'win', device: 'Chrome on Windows', where: 'Brasov, Romania', when: 'Now', icon: Monitor },
55
+ { id: 'ios', device: 'Safari on iPhone', where: 'Brasov, Romania', when: '2 h ago', icon: Smartphone },
56
+ { id: 'tab', device: 'Firefox on Android tablet', where: 'Cluj-Napoca, Romania', when: '3 d ago', icon: Tablet },
57
+ ])
58
+ return (
59
+ <div className="w-full max-w-xl">
60
+ <List aria-label="Signed-in sessions">
61
+ {sessions.map(session => (
62
+ <ListItem
63
+ key={session.id}
64
+ media={<DeviceIcon icon={session.icon} />}
65
+ title={session.device}
66
+ description={session.where}
67
+ meta={session.when}
68
+ actions={
69
+ session.id === 'win' ? (
70
+ <Badge tone="ok">This device</Badge>
71
+ ) : (
72
+ <Button
73
+ size="sm"
74
+ variant="secondary"
75
+ aria-label={`Sign out of ${session.device}`}
76
+ onClick={() => setSessions(all => all.filter(s => s.id !== session.id))}
77
+ >
78
+ Sign out
79
+ </Button>
80
+ )
81
+ }
82
+ />
83
+ ))}
84
+ </List>
85
+ </div>
86
+ )
87
+ }
88
+
89
+ <SessionsExample />
90
+ ```
91
+
92
+ ### Notifications: each row one link, with an archive action beside it
93
+
94
+ ```tsx
95
+ <div className="w-full max-w-xl">
96
+ <List aria-label="Notifications">
97
+ <ListItem
98
+ className="px-2"
99
+ title="Order 1042 shipped"
100
+ description="Your carbine replica left the depot."
101
+ meta="09:14"
102
+ href="#order-1042"
103
+ actions={
104
+ <IconButton label="Archive: Order 1042 shipped" variant="ghost" size="sm">
105
+ <Archive />
106
+ </IconButton>
107
+ }
108
+ />
109
+ <ListItem
110
+ className="px-2"
111
+ title="Back in stock"
112
+ description="The holo sight on your wishlist is available again."
113
+ meta="Yesterday"
114
+ href="#wishlist"
115
+ actions={
116
+ <IconButton label="Archive: Back in stock" variant="ghost" size="sm">
117
+ <Archive />
118
+ </IconButton>
119
+ }
120
+ />
121
+ <ListItem
122
+ className="px-2"
123
+ title="Training moved"
124
+ description="Saturday’s training starts at 10:00, not 09:00."
125
+ meta="Mon"
126
+ href="#events"
127
+ actions={
128
+ <IconButton label="Archive: Training moved" variant="ghost" size="sm">
129
+ <Archive />
130
+ </IconButton>
131
+ }
132
+ />
133
+ </List>
134
+ </div>
135
+ ```
136
+
137
+ ### Team members: avatars, the whole row a button
138
+
139
+ ```tsx
140
+ function TeamExample() {
141
+ const [chosen, setChosen] = useState<string | null>(null)
142
+ const team = [
143
+ { name: 'Ilse Varga', email: 'ilse@hexline.example', role: 'Captain' },
144
+ { name: 'Tomas Reyes', email: 'tomas@hexline.example', role: 'Medic' },
145
+ { name: 'Ana Popescu', email: 'ana@hexline.example', role: 'Scout' },
146
+ ]
147
+ return (
148
+ <div className="w-full max-w-xl">
149
+ <List aria-label="Squad members">
150
+ {team.map(member => (
151
+ <ListItem
152
+ key={member.email}
153
+ className="px-2"
154
+ media={<Avatar name={member.name} />}
155
+ title={member.name}
156
+ description={member.email}
157
+ meta={member.role}
158
+ onActivate={() => setChosen(member.name)}
159
+ />
160
+ ))}
161
+ </List>
162
+ <p className="mt-3 text-sm text-[var(--color-text-secondary)]" aria-live="polite">
163
+ {chosen ? `Opened ${chosen}.` : 'Choose a member to open their profile.'}
164
+ </p>
165
+ </div>
166
+ )
167
+ }
168
+
169
+ <TeamExample />
170
+ ```
171
+
172
+ ### A standalone row, outside a List
173
+
174
+ ```tsx
175
+ <div className="w-full max-w-xl">
176
+ <ListItem as="div" title="Spare magazine, 120 rounds" description="Mid-cap, steel" meta="€14.90" />
177
+ </div>
178
+ ```
179
+
180
+ ## Keyboard
181
+
182
+ | Keys | Result |
183
+ |---|---|
184
+ | `Tab` | Moves to the row’s link or button, then to each of its actions, then to the next row. |
185
+ | `Enter` | Follows the row’s link, or presses its button. |
186
+ | `Space` | Presses the row’s button (a link row ignores Space, like any link). |
187
+
188
+ ## Do
189
+
190
+ - Put rows in a List, and give the List an aria-label (a name only screen readers hear) when no heading names it.
191
+ - Use href when the row opens a page, and onActivate when it does something right there.
192
+ - Keep actions to one or two. Put the rest in a DropdownMenu.
193
+ - When an action repeats on every row, name it after its row ("Sign out of Chrome on Windows"), with aria-label or the IconButton label.
194
+
195
+ ## Don't
196
+
197
+ - Don't make a row clickable with onClick on the row. Use href or onActivate.
198
+ - Don't wrap a ListItem in a link or a button.
199
+ - Don't use it for data people compare across columns. That's what DataTable is for.
200
+ - Don't put long paragraphs in description. People scan a row, they don't read it.
201
+
202
+ ## Common mistakes
203
+
204
+ Wrong:
205
+
206
+ ```tsx
207
+ <li onClick={open}>…<Button>Remove</Button></li>
208
+ ```
209
+
210
+ Right:
211
+
212
+ ```tsx
213
+ <ListItem title="Ilse Varga" onActivate={open} actions={<Button>Remove</Button>} />
214
+ ```
215
+
216
+ A clickable li isn't a control. It can't take keyboard focus, and screen readers read it as plain text. onActivate (or href) turns the title into a real button (or link) that covers the row, and Remove keeps its own tab stop.
217
+
218
+ Wrong:
219
+
220
+ ```tsx
221
+ <a href="/orders/1"><ListItem title="Order shipped" actions={<Button>Archive</Button>} /></a>
222
+ ```
223
+
224
+ Right:
225
+
226
+ ```tsx
227
+ <ListItem title="Order shipped" href="/orders/1" actions={<Button>Archive</Button>} />
228
+ ```
229
+
230
+ A button inside a link is invalid HTML, and screen readers announce the two as one merged control. href stretches the link over the row without wrapping the actions.
231
+
232
+ ## Anatomy
233
+
234
+ - **List**: A ul with role="list" written out, so screen readers announce how many items there are even without bullets. Thin dividers between rows by default.
235
+ - **Media**: Optional, at the start: an Avatar, an icon, a thumbnail. Centred against the text.
236
+ - **Title and description**: The title is primary text, semibold. With href or onActivate it becomes the row’s one link or button, stretched over the row and underlined on hover. The description is secondary text, 14px (pixels).
237
+ - **Meta**: Optional, right-aligned monospaced text with tabular figures (digits all the same width, so numbers line up): a time, a price, a count.
238
+ - **Actions**: Optional controls at the end, layered above the stretched link, each with its own tab stop. On a narrow screen, meta and actions wrap under the text.
@@ -0,0 +1,70 @@
1
+ # LoadingAnimation
2
+
3
+ > A loading state that fills a whole panel, drawn as an animated instrument dial in WebGL (the browser's graphics engine for drawing with the graphics card). The three dials follow the same rules. They draw themselves from the outside in, and their structure drifts. One mechanism clicks to its next detent using the system's snap easing (the motion where things arrive a little large and settle), and the signal accent colour only appears on the part that shows progress.
4
+
5
+ - **Best for:** A wait that takes over a whole panel or page, like a route loading, a report building or a first sync.
6
+ - **Not for:** A wait inside a button or a line of text: use Spinner. A wait with a known end: use Progress.
7
+ - Atom. Source: `src/components/atoms/loading-animation/LoadingAnimation.tsx`
8
+ - Rendered docs page: `/components/loading-animation`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { LoadingAnimation } from 'src/components/atoms/loading-animation/LoadingAnimation'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `variant` | `'aperture' \| 'gyro' \| 'reticle'` | `'aperture'` | Aperture: shaded blades stepping round a spinning core. Gyro: nested rings turning against each other. Reticle: a progress ring filling up from a marker at 12 o’clock. |
21
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | A 96px, 160px or 240px square (px means pixels). It shrinks with its container below that size, but never grows. |
22
+ | `label` | `string` | | Status text that is hidden on screen. Passing it turns the loader into a live region, an area screen readers announce when it changes. |
23
+ | `className` | `string` | | Merged with the component classes. If two clash, the later Tailwind utility wins (Tailwind is the CSS framework this system styles with). |
24
+ | `ref` | `Ref<HTMLDivElement>` | | Passed through to the root div. |
25
+ | `...rest` | `Omit<HTMLAttributes<HTMLDivElement>, 'role'>` | | Every native div prop except role, for the same reason as Spinner: label is the only thing that sets role="status". |
26
+
27
+ ## Examples
28
+
29
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
30
+
31
+ ### Three dials, md
32
+
33
+ ```tsx
34
+ <LoadingAnimation variant="aperture" label="Loading field kits" />
35
+ <LoadingAnimation variant="gyro" label="Syncing manifest" />
36
+ <LoadingAnimation variant="reticle" label="Acquiring position" />
37
+ ```
38
+
39
+ ### Aperture, three sizes
40
+
41
+ ```tsx
42
+ <LoadingAnimation variant="aperture" size="sm" label="Loading field kits" />
43
+ <LoadingAnimation variant="aperture" size="md" label="Loading field kits" />
44
+ <LoadingAnimation variant="aperture" size="lg" label="Loading field kits" />
45
+ ```
46
+
47
+ ## Keyboard
48
+
49
+ Not focusable. No keyboard behaviour of its own.
50
+
51
+ ## Do
52
+
53
+ - Use it for a wait that takes over a panel or a page, like a route loading, a report building or a first sync.
54
+ - Pass label, and say what is loading: ‘Loading field kits’, not ‘Loading’.
55
+ - Pick one dial for each product surface and stick with it. The three are a family. They aren't meant to rotate.
56
+
57
+ ## Don't
58
+
59
+ - Don't put it in a button or a line of text. That's Spinner, which doesn't use up a WebGL context.
60
+ - Don't show more than a handful at once. Each one uses a WebGL context (one slot of the browser's graphics engine), and browsers cap how many you get.
61
+ - Don't rely on the motion. When someone has reduced motion turned on, it shows one still frame, so the label has to carry the meaning.
62
+ - Don't use it for a wait with a known end. That's Progress.
63
+
64
+ ## Anatomy
65
+
66
+ - **div**: The root: a square box at the size you pick. It gets role="status", which makes screen readers announce it, only when it has a label.
67
+ - **Stage**: A layer that holds the WebGL canvas, hidden from screen readers with aria-hidden (an ARIA attribute, from Accessible Rich Internet Applications). It fades in once the first frame is drawn.
68
+ - **Dial**: Rings, tick crowns and arcs in text.primary, text.secondary, border.control and border.hairline.strong. One moving part is in accent.signal.fill.
69
+ - **Fallback**: If WebGL isn't available, the Conveyor spinner shows in the same box. It's aria-hidden, so the label is still read only once.
70
+ - **sr-only label**: The text screen readers announce (sr-only means it's for screen readers only). It's only there when you pass label.
@@ -0,0 +1,200 @@
1
+ # MultiSelect
2
+
3
+ > Pick several choices from a list. The chosen ones sit inside the field as tags you can remove, followed by a text field that filters the list as you type. It's the many-values version of Select.
4
+
5
+ - **Best for:** Any number of choices, including none, from a list too long to show all at once.
6
+ - **Not for:** A single choice: use Select, or Radio when there are fewer than about five options. A handful that all fit: use Checkbox.
7
+ - Molecule. Source: `src/components/molecules/multi-select/MultiSelect.tsx`
8
+ - Rendered docs page: `/components/multi-select`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { MultiSelect } from 'src/components/molecules/multi-select/MultiSelect'
14
+ ```
15
+
16
+ ## Requires
17
+
18
+ - A <FormField> around it. FormField supplies the visible label, the help and the error, and replaces the label prop with its own.
19
+
20
+ ## Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |---|---|---|---|
24
+ | `label` | `string` | | Required. The name screen readers give the text field and the list. FormField replaces it with its own label. |
25
+ | `options` | `SelectOption[]` | | { value, label, disabled? }, the same rows Select takes. |
26
+ | `value` | `string[]` | | The current selection when the page keeps track of it (controlled), as option values in the order they were chosen. |
27
+ | `defaultValue` | `string[]` | | The starting selection when the component keeps track itself (uncontrolled). |
28
+ | `onValueChange` | `(value: string[]) => void` | | Runs with the whole new array every time an option is added or removed. |
29
+ | `placeholder` | `string` | `'Select…'` | Shown while nothing is chosen. |
30
+ | `emptyMessage` | `string` | `'No matches'` | Shown in the panel when the typed text matches no option. |
31
+ | `inputSize` | `'sm' \| 'md' \| 'lg'` | `'md'` | Matches Input and Select: a minimum height of 32, 40 or 48px (pixels), with text at size xs, sm or base. It also sets the size of the typed text and of the option rows in the list (28, 32 or 40px). The tags come in one size, so sm fits a single row of them snugly, and md and lg add room around it. The field grows past the minimum as tags wrap. |
32
+ | `invalid` | `boolean` | `false` | Sets aria-invalid (which tells screen readers the value has a problem) and the invalid border. FormField sets it from error. |
33
+ | `disabled` | `boolean` | `false` | Turns the field off. The chosen tags stay visible, without their remove buttons. |
34
+ | `required` | `boolean` | `false` | Sets aria-required on the text field, so screen readers say it's required. |
35
+ | `name` | `string` | | Sends each chosen value with the form as its own hidden input under this name. |
36
+ | `className` | `string` | | Extra CSS classes, merged onto the field. |
37
+ | `ref` | `Ref<HTMLInputElement>` | | Passed through to the text field. |
38
+ | `id` | `string` | | The text field’s id. FormField sets it. |
39
+ | `aria-describedby` | `string` | | The ids of the help and error text, put on the text field so screen readers read them. FormField sets it. |
40
+
41
+ ## Examples
42
+
43
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
44
+
45
+ ### Default
46
+
47
+ ```tsx
48
+ const LOADOUT = [
49
+ { value: 'mags', label: 'Spare magazines' },
50
+ { value: 'goggles', label: 'Full-seal goggles' },
51
+ { value: 'mesh', label: 'Mesh face guard' },
52
+ { value: 'radio', label: 'Team radio', disabled: true },
53
+ { value: 'gloves', label: 'Gloves' },
54
+ { value: 'loader', label: 'Speed loader' },
55
+ { value: 'tracer', label: 'Tracer unit' },
56
+ { value: 'pouch', label: 'Dump pouch' },
57
+ ]
58
+
59
+ <div className="w-full max-w-md">
60
+ <FormField label="Loadout">
61
+ <MultiSelect label="Loadout" options={LOADOUT} placeholder="Choose your kit…" />
62
+ </FormField>
63
+ </div>
64
+ ```
65
+
66
+ ### Controlled: the page holds the chosen options
67
+
68
+ ```tsx
69
+ const LOADOUT = [
70
+ { value: 'mags', label: 'Spare magazines' },
71
+ { value: 'goggles', label: 'Full-seal goggles' },
72
+ { value: 'mesh', label: 'Mesh face guard' },
73
+ { value: 'radio', label: 'Team radio', disabled: true },
74
+ { value: 'gloves', label: 'Gloves' },
75
+ { value: 'loader', label: 'Speed loader' },
76
+ { value: 'tracer', label: 'Tracer unit' },
77
+ { value: 'pouch', label: 'Dump pouch' },
78
+ ]
79
+
80
+ function ControlledExample() {
81
+ const [kit, setKit] = useState<string[]>(['mags', 'goggles', 'gloves'])
82
+ return (
83
+ <div className="flex flex-col gap-2 w-full max-w-md">
84
+ <FormField label="Loadout" help="Remove a tag and watch the others close the gap.">
85
+ <MultiSelect label="Loadout" options={LOADOUT} value={kit} onValueChange={setKit} />
86
+ </FormField>
87
+ <span className="font-mono text-xs text-[var(--color-text-secondary)]">value: [{kit.join(', ')}]</span>
88
+ </div>
89
+ )
90
+ }
91
+
92
+ <ControlledExample />
93
+ ```
94
+
95
+ ### Invalid
96
+
97
+ ```tsx
98
+ const LOADOUT = [
99
+ { value: 'mags', label: 'Spare magazines' },
100
+ { value: 'goggles', label: 'Full-seal goggles' },
101
+ { value: 'mesh', label: 'Mesh face guard' },
102
+ { value: 'radio', label: 'Team radio', disabled: true },
103
+ { value: 'gloves', label: 'Gloves' },
104
+ { value: 'loader', label: 'Speed loader' },
105
+ { value: 'tracer', label: 'Tracer unit' },
106
+ { value: 'pouch', label: 'Dump pouch' },
107
+ ]
108
+
109
+ <div className="w-full max-w-md">
110
+ <FormField label="Loadout" error="Pick eye protection.">
111
+ <MultiSelect label="Loadout" options={LOADOUT} defaultValue={['mags']} />
112
+ </FormField>
113
+ </div>
114
+ ```
115
+
116
+ ### Sizes
117
+
118
+ ```tsx
119
+ const LOADOUT = [
120
+ { value: 'mags', label: 'Spare magazines' },
121
+ { value: 'goggles', label: 'Full-seal goggles' },
122
+ { value: 'mesh', label: 'Mesh face guard' },
123
+ { value: 'radio', label: 'Team radio', disabled: true },
124
+ { value: 'gloves', label: 'Gloves' },
125
+ { value: 'loader', label: 'Speed loader' },
126
+ { value: 'tracer', label: 'Tracer unit' },
127
+ { value: 'pouch', label: 'Dump pouch' },
128
+ ]
129
+
130
+ <div className="flex flex-col gap-3 w-full max-w-md">
131
+ <MultiSelect label="Small loadout" inputSize="sm" options={LOADOUT} defaultValue={['mags']} />
132
+ <MultiSelect label="Medium loadout" inputSize="md" options={LOADOUT} defaultValue={['mags']} />
133
+ <MultiSelect label="Large loadout" inputSize="lg" options={LOADOUT} defaultValue={['mags']} />
134
+ </div>
135
+ ```
136
+
137
+ ### Disabled
138
+
139
+ ```tsx
140
+ const LOADOUT = [
141
+ { value: 'mags', label: 'Spare magazines' },
142
+ { value: 'goggles', label: 'Full-seal goggles' },
143
+ { value: 'mesh', label: 'Mesh face guard' },
144
+ { value: 'radio', label: 'Team radio', disabled: true },
145
+ { value: 'gloves', label: 'Gloves' },
146
+ { value: 'loader', label: 'Speed loader' },
147
+ { value: 'tracer', label: 'Tracer unit' },
148
+ { value: 'pouch', label: 'Dump pouch' },
149
+ ]
150
+
151
+ <div className="w-full max-w-md">
152
+ <MultiSelect label="Locked loadout" options={LOADOUT} defaultValue={['mags', 'gloves']} disabled />
153
+ </div>
154
+ ```
155
+
156
+ ## Keyboard
157
+
158
+ | Keys | Result |
159
+ |---|---|
160
+ | `↓` `↑` | Opens the list, then moves the highlight, skipping rows that are turned off. |
161
+ | `Enter` | Adds or removes the highlighted option. The list stays open. |
162
+ | `Backspace` | When the text field is empty, removes the last tag. |
163
+ | `Esc` | Closes the list. Press it again to clear the typed text. |
164
+ | `Tab` | Moves through the tags’ remove buttons and the text field, and closes the list on the way out. |
165
+
166
+ ## Do
167
+
168
+ - Use it when someone may pick any number of options, including none.
169
+ - Wrap it in a FormField, like every other form control.
170
+ - Keep option labels short. They become tags, and long tags wrap the field onto many lines.
171
+ - Turn off (disable) an option that isn't available, instead of leaving it out.
172
+
173
+ ## Don't
174
+
175
+ - Don't use it for a single choice. That's Select, or radios when there are fewer than about five options.
176
+ - Don't use it for a handful of options that all fit on screen. Checkboxes show every choice at once.
177
+ - Don't put actions in the list. It chooses values, and a command belongs in a DropdownMenu.
178
+
179
+ ## Common mistakes
180
+
181
+ Wrong:
182
+
183
+ ```tsx
184
+ <Select label="Kit" options={kit} /> // then a second Select for the next item
185
+ ```
186
+
187
+ Right:
188
+
189
+ ```tsx
190
+ <MultiSelect label="Kit" options={kit} value={chosen} onValueChange={setChosen} />
191
+ ```
192
+
193
+ Select holds one value. A list of picks is one MultiSelect, whose value is a string array (a list of text values).
194
+
195
+ ## Anatomy
196
+
197
+ - **Field**: Looks like Input, but grows to fit: it wraps onto more lines as tags are added. Clicking anywhere on it puts focus in the text field and opens the list.
198
+ - **Tag**: One for each chosen option, in the order they were chosen. Removing one fades it out and slides the tags after it into its place (M7). Focus goes back to the text field.
199
+ - **Text field**: Has role="combobox", which tells screen readers it's a text field with a list attached. Typing filters the list. The placeholder only shows while nothing is chosen.
200
+ - **Listbox**: The list, where several options can be chosen. It sits in a panel rendered in a portal (at the end of the page, so nothing clips it), with the same background as Select and the crosshair. It slides down out of the field and back up into it (M7). A tick marks every chosen row.