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.
- package/AGENTS.md +23 -3
- package/README.md +11 -0
- package/division-ds-0.2.1.tgz +0 -0
- package/package.json +2 -2
- package/public/docs/components/accordion.md +207 -0
- package/public/docs/components/ai-label.md +90 -0
- package/public/docs/components/alert-dialog.md +292 -0
- package/public/docs/components/alert.md +175 -0
- package/public/docs/components/approval-request.md +148 -0
- package/public/docs/components/auth-form.md +132 -0
- package/public/docs/components/avatar-group.md +152 -0
- package/public/docs/components/avatar.md +179 -0
- package/public/docs/components/badge.md +178 -0
- package/public/docs/components/breadcrumbs.md +112 -0
- package/public/docs/components/button.md +204 -0
- package/public/docs/components/card.md +257 -0
- package/public/docs/components/carousel.md +130 -0
- package/public/docs/components/cart.md +172 -0
- package/public/docs/components/chart-panel.md +358 -0
- package/public/docs/components/chat-message.md +157 -0
- package/public/docs/components/chat-thread.md +244 -0
- package/public/docs/components/checkbox.md +149 -0
- package/public/docs/components/checkout-form.md +128 -0
- package/public/docs/components/command-palette.md +147 -0
- package/public/docs/components/data-table.md +401 -0
- package/public/docs/components/description-list.md +103 -0
- package/public/docs/components/dialog.md +197 -0
- package/public/docs/components/divider.md +97 -0
- package/public/docs/components/dropdown-menu.md +222 -0
- package/public/docs/components/empty-state.md +145 -0
- package/public/docs/components/footer.md +397 -0
- package/public/docs/components/form-field.md +216 -0
- package/public/docs/components/icon-button.md +127 -0
- package/public/docs/components/input.md +200 -0
- package/public/docs/components/kanban-board.md +300 -0
- package/public/docs/components/kbd.md +72 -0
- package/public/docs/components/link.md +98 -0
- package/public/docs/components/list-item.md +241 -0
- package/public/docs/components/loading-animation.md +72 -0
- package/public/docs/components/multi-select.md +202 -0
- package/public/docs/components/navbar.md +144 -0
- package/public/docs/components/newsletter-form.md +112 -0
- package/public/docs/components/page-header.md +151 -0
- package/public/docs/components/pagination.md +212 -0
- package/public/docs/components/product-card.md +172 -0
- package/public/docs/components/progress.md +101 -0
- package/public/docs/components/prompt-input.md +151 -0
- package/public/docs/components/quantity-stepper.md +140 -0
- package/public/docs/components/radio.md +177 -0
- package/public/docs/components/rating.md +106 -0
- package/public/docs/components/score-ring.md +95 -0
- package/public/docs/components/search.md +116 -0
- package/public/docs/components/section-heading.md +113 -0
- package/public/docs/components/segmented-control.md +126 -0
- package/public/docs/components/select.md +224 -0
- package/public/docs/components/sheet.md +145 -0
- package/public/docs/components/sidebar.md +193 -0
- package/public/docs/components/skeleton.md +135 -0
- package/public/docs/components/slider.md +114 -0
- package/public/docs/components/sparkline.md +128 -0
- package/public/docs/components/spinner.md +98 -0
- package/public/docs/components/stat.md +106 -0
- package/public/docs/components/status-tracker.md +145 -0
- package/public/docs/components/stepper.md +147 -0
- package/public/docs/components/switch.md +153 -0
- package/public/docs/components/tabs.md +180 -0
- package/public/docs/components/tag.md +105 -0
- package/public/docs/components/textarea.md +148 -0
- package/public/docs/components/timeline.md +152 -0
- package/public/docs/components/toast.md +194 -0
- package/public/docs/components/tool-call.md +107 -0
- package/public/docs/components/tooltip.md +108 -0
- package/public/manifest/components.json +198 -15
- package/public/manifest/tokens.json +75 -5
- package/scripts/division-ds.mjs +78 -42
- package/scripts/lib/agent-docs.mjs +80 -6
- package/scripts/lib/ai-copy.mjs +120 -0
- package/scripts/lib/button-spacing.mjs +5 -5
- package/scripts/lib/check-text.mjs +57 -0
- package/scripts/lib/contrast.mjs +111 -0
- package/scripts/lib/host-inline.mjs +59 -0
- package/scripts/lib/mcp-server.mjs +116 -0
- package/scripts/lib/mcp-tools.mjs +266 -0
- package/scripts/lib/props-validator.mjs +3 -2
- package/scripts/lib/rules-installer.mjs +42 -4
- package/scripts/lib/token-discipline.mjs +10 -3
- package/skills/division-ds/SKILL.md +90 -0
- package/skills/division-ds/commands/add.md +25 -0
- package/skills/division-ds/commands/build.md +46 -0
- package/skills/division-ds/commands/fix.md +28 -0
- package/skills/division-ds/commands/review.md +25 -0
- package/skills/division-ds/topics/ai.md +8 -0
- package/skills/division-ds/topics/colour.md +11 -0
- package/skills/division-ds/topics/feedback.md +9 -0
- package/skills/division-ds/topics/forms.md +22 -0
- package/skills/division-ds/topics/layout.md +36 -0
- package/skills/division-ds/topics/motion.md +10 -0
- package/skills/division-ds/topics/overlays.md +9 -0
|
@@ -0,0 +1,202 @@
|
|
|
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
|
+
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 <FormField> around it. FormField supplies the visible label, the help and the error, and replaces the label prop with its own.
|
|
21
|
+
|
|
22
|
+
## Props
|
|
23
|
+
|
|
24
|
+
| Prop | Type | Default | Description |
|
|
25
|
+
|---|---|---|---|
|
|
26
|
+
| `label` | `string` | | Required. The name screen readers give the text field and the list. FormField replaces it with its own label. |
|
|
27
|
+
| `options` | `SelectOption[]` | | { value, label, disabled? }, the same rows Select takes. |
|
|
28
|
+
| `value` | `string[]` | | The current selection when the page keeps track of it (controlled), as option values in the order they were chosen. |
|
|
29
|
+
| `defaultValue` | `string[]` | | The starting selection when the component keeps track itself (uncontrolled). |
|
|
30
|
+
| `onValueChange` | `(value: string[]) => void` | | Runs with the whole new array every time an option is added or removed. |
|
|
31
|
+
| `placeholder` | `string` | `'Select…'` | Shown while nothing is chosen. |
|
|
32
|
+
| `emptyMessage` | `string` | `'No matches'` | Shown in the panel when the typed text matches no option. |
|
|
33
|
+
| `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. |
|
|
34
|
+
| `invalid` | `boolean` | `false` | Sets aria-invalid (which tells screen readers the value has a problem) and the invalid border. FormField sets it from error. |
|
|
35
|
+
| `disabled` | `boolean` | `false` | Turns the field off. The chosen tags stay visible, without their remove buttons. |
|
|
36
|
+
| `required` | `boolean` | `false` | Sets aria-required on the text field, so screen readers say it's required. |
|
|
37
|
+
| `name` | `string` | | Sends each chosen value with the form as its own hidden input under this name. |
|
|
38
|
+
| `className` | `string` | | Extra CSS classes, merged onto the field. |
|
|
39
|
+
| `ref` | `Ref<HTMLInputElement>` | | Passed through to the text field. |
|
|
40
|
+
| `id` | `string` | | The text field’s id. FormField sets it. |
|
|
41
|
+
| `aria-describedby` | `string` | | The ids of the help and error text, put on the text field so screen readers read them. FormField sets it. |
|
|
42
|
+
|
|
43
|
+
## Examples
|
|
44
|
+
|
|
45
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
46
|
+
|
|
47
|
+
### Default
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
const LOADOUT = [
|
|
51
|
+
{ value: 'mags', label: 'Spare magazines' },
|
|
52
|
+
{ value: 'goggles', label: 'Full-seal goggles' },
|
|
53
|
+
{ value: 'mesh', label: 'Mesh face guard' },
|
|
54
|
+
{ value: 'radio', label: 'Team radio', disabled: true },
|
|
55
|
+
{ value: 'gloves', label: 'Gloves' },
|
|
56
|
+
{ value: 'loader', label: 'Speed loader' },
|
|
57
|
+
{ value: 'tracer', label: 'Tracer unit' },
|
|
58
|
+
{ value: 'pouch', label: 'Dump pouch' },
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
<div className="w-full max-w-md">
|
|
62
|
+
<FormField label="Loadout">
|
|
63
|
+
<MultiSelect label="Loadout" options={LOADOUT} placeholder="Choose your kit…" />
|
|
64
|
+
</FormField>
|
|
65
|
+
</div>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Controlled: the page holds the chosen options
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
const LOADOUT = [
|
|
72
|
+
{ value: 'mags', label: 'Spare magazines' },
|
|
73
|
+
{ value: 'goggles', label: 'Full-seal goggles' },
|
|
74
|
+
{ value: 'mesh', label: 'Mesh face guard' },
|
|
75
|
+
{ value: 'radio', label: 'Team radio', disabled: true },
|
|
76
|
+
{ value: 'gloves', label: 'Gloves' },
|
|
77
|
+
{ value: 'loader', label: 'Speed loader' },
|
|
78
|
+
{ value: 'tracer', label: 'Tracer unit' },
|
|
79
|
+
{ value: 'pouch', label: 'Dump pouch' },
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
function ControlledExample() {
|
|
83
|
+
const [kit, setKit] = useState<string[]>(['mags', 'goggles', 'gloves'])
|
|
84
|
+
return (
|
|
85
|
+
<div className="flex flex-col gap-2 w-full max-w-md">
|
|
86
|
+
<FormField label="Loadout" help="Remove a tag and watch the others close the gap.">
|
|
87
|
+
<MultiSelect label="Loadout" options={LOADOUT} value={kit} onValueChange={setKit} />
|
|
88
|
+
</FormField>
|
|
89
|
+
<span className="font-mono text-xs text-[var(--color-text-secondary)]">value: [{kit.join(', ')}]</span>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
<ControlledExample />
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Invalid
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
const LOADOUT = [
|
|
101
|
+
{ value: 'mags', label: 'Spare magazines' },
|
|
102
|
+
{ value: 'goggles', label: 'Full-seal goggles' },
|
|
103
|
+
{ value: 'mesh', label: 'Mesh face guard' },
|
|
104
|
+
{ value: 'radio', label: 'Team radio', disabled: true },
|
|
105
|
+
{ value: 'gloves', label: 'Gloves' },
|
|
106
|
+
{ value: 'loader', label: 'Speed loader' },
|
|
107
|
+
{ value: 'tracer', label: 'Tracer unit' },
|
|
108
|
+
{ value: 'pouch', label: 'Dump pouch' },
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
<div className="w-full max-w-md">
|
|
112
|
+
<FormField label="Loadout" error="Pick eye protection.">
|
|
113
|
+
<MultiSelect label="Loadout" options={LOADOUT} defaultValue={['mags']} />
|
|
114
|
+
</FormField>
|
|
115
|
+
</div>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Sizes
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
const LOADOUT = [
|
|
122
|
+
{ value: 'mags', label: 'Spare magazines' },
|
|
123
|
+
{ value: 'goggles', label: 'Full-seal goggles' },
|
|
124
|
+
{ value: 'mesh', label: 'Mesh face guard' },
|
|
125
|
+
{ value: 'radio', label: 'Team radio', disabled: true },
|
|
126
|
+
{ value: 'gloves', label: 'Gloves' },
|
|
127
|
+
{ value: 'loader', label: 'Speed loader' },
|
|
128
|
+
{ value: 'tracer', label: 'Tracer unit' },
|
|
129
|
+
{ value: 'pouch', label: 'Dump pouch' },
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
<div className="flex flex-col gap-3 w-full max-w-md">
|
|
133
|
+
<MultiSelect label="Small loadout" inputSize="sm" options={LOADOUT} defaultValue={['mags']} />
|
|
134
|
+
<MultiSelect label="Medium loadout" inputSize="md" options={LOADOUT} defaultValue={['mags']} />
|
|
135
|
+
<MultiSelect label="Large loadout" inputSize="lg" options={LOADOUT} defaultValue={['mags']} />
|
|
136
|
+
</div>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Disabled
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
const LOADOUT = [
|
|
143
|
+
{ value: 'mags', label: 'Spare magazines' },
|
|
144
|
+
{ value: 'goggles', label: 'Full-seal goggles' },
|
|
145
|
+
{ value: 'mesh', label: 'Mesh face guard' },
|
|
146
|
+
{ value: 'radio', label: 'Team radio', disabled: true },
|
|
147
|
+
{ value: 'gloves', label: 'Gloves' },
|
|
148
|
+
{ value: 'loader', label: 'Speed loader' },
|
|
149
|
+
{ value: 'tracer', label: 'Tracer unit' },
|
|
150
|
+
{ value: 'pouch', label: 'Dump pouch' },
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
<div className="w-full max-w-md">
|
|
154
|
+
<MultiSelect label="Locked loadout" options={LOADOUT} defaultValue={['mags', 'gloves']} disabled />
|
|
155
|
+
</div>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Keyboard
|
|
159
|
+
|
|
160
|
+
| Keys | Result |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `↓` `↑` | Opens the list, then moves the highlight, skipping rows that are turned off. |
|
|
163
|
+
| `Enter` | Adds or removes the highlighted option. The list stays open. |
|
|
164
|
+
| `Backspace` | When the text field is empty, removes the last tag. |
|
|
165
|
+
| `Esc` | Closes the list. Press it again to clear the typed text. |
|
|
166
|
+
| `Tab` | Moves through the tags’ remove buttons and the text field, and closes the list on the way out. |
|
|
167
|
+
|
|
168
|
+
## Do
|
|
169
|
+
|
|
170
|
+
- Use it when someone may pick any number of options, including none.
|
|
171
|
+
- Wrap it in a FormField, like every other form control.
|
|
172
|
+
- Keep option labels short. They become tags, and long tags wrap the field onto many lines.
|
|
173
|
+
- Turn off (disable) an option that isn't available, instead of leaving it out.
|
|
174
|
+
|
|
175
|
+
## Don't
|
|
176
|
+
|
|
177
|
+
- Don't use it for a single choice. That's Select, or radios when there are fewer than about five options.
|
|
178
|
+
- Don't use it for a handful of options that all fit on screen. Checkboxes show every choice at once.
|
|
179
|
+
- Don't put actions in the list. It chooses values, and a command belongs in a DropdownMenu.
|
|
180
|
+
|
|
181
|
+
## Common mistakes
|
|
182
|
+
|
|
183
|
+
Wrong:
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
<Select label="Kit" options={kit} /> // then a second Select for the next item
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Right:
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
<MultiSelect label="Kit" options={kit} value={chosen} onValueChange={setChosen} />
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Select holds one value. A list of picks is one MultiSelect, whose value is a string array (a list of text values).
|
|
196
|
+
|
|
197
|
+
## Anatomy
|
|
198
|
+
|
|
199
|
+
- **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.
|
|
200
|
+
- **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.
|
|
201
|
+
- **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.
|
|
202
|
+
- **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.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Navbar
|
|
2
|
+
|
|
3
|
+
> The top of a page: the brand, the main links and a few actions. When its container gets narrow, the links move into a menu sheet. Nothing gets cut off, and nothing is hidden for good.
|
|
4
|
+
|
|
5
|
+
- **Best for:** The top of a site: the brand, up to about six main links and a few actions.
|
|
6
|
+
- **Not for:** App navigation across many grouped pages: use Sidebar.
|
|
7
|
+
- Organism. Source: `src/components/organisms/navbar/Navbar.tsx`
|
|
8
|
+
- Rendered docs page: `/components/navbar`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Navbar } from 'src/components/organisms/navbar/Navbar'
|
|
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
|
+
| `label` | `string` | | Required. The name screen readers give the navigation region. |
|
|
23
|
+
| `brand` | `ReactNode` | | Required. The wordmark or logo, usually a link to the home page. |
|
|
24
|
+
| `items` | `NavbarItem[]` | | Required. Each item has label, href and current. |
|
|
25
|
+
| `actions` | `ReactNode` | | Controls on the right: icon buttons, a cart link, an account menu. Keep them small, because they stay visible at every width. |
|
|
26
|
+
| `renderLink` | `(props: LinkRenderProps) => ReactNode` | `a plain anchor` | Pass your router's link component here. Map href to its own prop, and pass className, aria-current and onClick through. |
|
|
27
|
+
| `menuLabel` | `string` | `'Menu'` | The menu button's name for screen readers, and the sheet's title. |
|
|
28
|
+
| `menuFooter` | `ReactNode` | | Shown in the menu sheet under the links. Use it for an action that doesn't fit in actions on a narrow bar. Hide the matching copy in actions below the breakpoint by wrapping it in an element with the classes hidden @2xl:flex. |
|
|
29
|
+
| `className` | `string` | | Added to the root classes. Where two Tailwind utility classes clash, the later one wins. |
|
|
30
|
+
| `ref` | `Ref<HTMLElement>` | | The header element. |
|
|
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 NavbarItem = { label: string; href: string; current?: boolean }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Examples
|
|
41
|
+
|
|
42
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
43
|
+
|
|
44
|
+
### Store navigation
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
const ITEMS = [
|
|
48
|
+
{ label: 'Replicas', href: '#replicas' },
|
|
49
|
+
{ label: 'Optics', href: '#optics', current: true },
|
|
50
|
+
{ label: 'Parts', href: '#parts' },
|
|
51
|
+
{ label: 'Care', href: '#care' },
|
|
52
|
+
{ label: 'Field notes', href: '#field-notes' },
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
const BRAND = (
|
|
56
|
+
<a
|
|
57
|
+
href="#home"
|
|
58
|
+
className="inline-flex items-center min-h-10 font-display font-bold uppercase tracking-[0.14em] text-[var(--color-text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]"
|
|
59
|
+
>
|
|
60
|
+
Hexline
|
|
61
|
+
</a>
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const ACTIONS = (
|
|
65
|
+
<>
|
|
66
|
+
<IconButton label="Search" variant="ghost">
|
|
67
|
+
<Search aria-hidden="true" className="w-5 h-5" />
|
|
68
|
+
</IconButton>
|
|
69
|
+
<IconButton label="Cart, 2 items" variant="ghost" asChild>
|
|
70
|
+
<a href="#cart">
|
|
71
|
+
<ShoppingBag aria-hidden="true" className="w-5 h-5" />
|
|
72
|
+
</a>
|
|
73
|
+
</IconButton>
|
|
74
|
+
</>
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
<Navbar label="Store navigation" brand={BRAND} items={ITEMS} actions={ACTIONS} />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### A narrow container
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
const ITEMS = [
|
|
84
|
+
{ label: 'Replicas', href: '#replicas' },
|
|
85
|
+
{ label: 'Optics', href: '#optics', current: true },
|
|
86
|
+
{ label: 'Parts', href: '#parts' },
|
|
87
|
+
{ label: 'Care', href: '#care' },
|
|
88
|
+
{ label: 'Field notes', href: '#field-notes' },
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
const BRAND = (
|
|
92
|
+
<a
|
|
93
|
+
href="#home"
|
|
94
|
+
className="inline-flex items-center min-h-10 font-display font-bold uppercase tracking-[0.14em] text-[var(--color-text-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]"
|
|
95
|
+
>
|
|
96
|
+
Hexline
|
|
97
|
+
</a>
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const ACTIONS = (
|
|
101
|
+
<>
|
|
102
|
+
<IconButton label="Search" variant="ghost">
|
|
103
|
+
<Search aria-hidden="true" className="w-5 h-5" />
|
|
104
|
+
</IconButton>
|
|
105
|
+
<IconButton label="Cart, 2 items" variant="ghost" asChild>
|
|
106
|
+
<a href="#cart">
|
|
107
|
+
<ShoppingBag aria-hidden="true" className="w-5 h-5" />
|
|
108
|
+
</a>
|
|
109
|
+
</IconButton>
|
|
110
|
+
</>
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
<div className="max-w-sm">
|
|
114
|
+
<Navbar label="Store navigation, narrow" brand={BRAND} items={ITEMS} actions={ACTIONS} />
|
|
115
|
+
</div>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Keyboard
|
|
119
|
+
|
|
120
|
+
| Keys | Result |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `Tab` | Moves through the brand, each link and the actions, then the menu button when it shows. |
|
|
123
|
+
| `Enter` | Follows a link, or opens the menu. |
|
|
124
|
+
| `Esc` | Closes the menu and sends focus back to its button. |
|
|
125
|
+
|
|
126
|
+
## Do
|
|
127
|
+
|
|
128
|
+
- Mark exactly one item as current.
|
|
129
|
+
- Keep actions to icon buttons with names, or one short button.
|
|
130
|
+
- If a page has more than one navbar, give each its own label.
|
|
131
|
+
|
|
132
|
+
## Don't
|
|
133
|
+
|
|
134
|
+
- Don't put more than about six links in the bar. Group the rest under a page.
|
|
135
|
+
- Don't hide an action inside the menu that the bar shows at wide widths only.
|
|
136
|
+
- Don't mark a link as current with colour alone. The fill and aria-current both move with it.
|
|
137
|
+
|
|
138
|
+
## Anatomy
|
|
139
|
+
|
|
140
|
+
- **Header**: The page's banner landmark, a region screen readers can jump straight to, at the top level of the page.
|
|
141
|
+
- **Brand**: The wordmark or logo, usually a link to the home page.
|
|
142
|
+
- **Links**: A named navigation region. The current page gets the signal fill, plus aria-current, an ARIA attribute (Accessible Rich Internet Applications, the labels screen readers rely on) that tells screen readers this is the page you are on.
|
|
143
|
+
- **Actions**: They stay visible at every width.
|
|
144
|
+
- **Menu**: When the container is narrower than 42rem (rem is a unit based on the root text size, usually 16 pixels), an icon button opens the same links in a Sheet. If the container widens past that breakpoint while the sheet is open, the sheet stays open. It doesn't close itself when the links reappear in the bar.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# NewsletterForm
|
|
2
|
+
|
|
3
|
+
> An email field, a line saying what you're agreeing to, and a button. It checks input the same way every form here does, is honest about waiting while it sends, and tells you when it worked.
|
|
4
|
+
|
|
5
|
+
- **Best for:** Collecting one email address for a newsletter, with a consent line.
|
|
6
|
+
- **Not for:** Creating an account: use AuthForm.
|
|
7
|
+
- Organism. Source: `src/components/organisms/newsletter-form/NewsletterForm.tsx`
|
|
8
|
+
- Rendered docs page: `/components/newsletter-form`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { NewsletterForm } from 'src/components/organisms/newsletter-form/NewsletterForm'
|
|
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
|
+
| `label` | `string` | | Required. The form's accessible name, the name screen readers say, e.g. "Newsletter sign-up". Leaving it out is a compile error. |
|
|
23
|
+
| `onSubmit` | `(email: string) => void \| Promise<void>` | | Required. Called with the trimmed address once it passes validation. Resolve (finish successfully) to show the success state. Reject (fail) to show the alert, and what was typed stays in the field. |
|
|
24
|
+
| `consent` | `ReactNode` | | Required. The consent line. Leaving it out is a compile error. |
|
|
25
|
+
| `fieldLabel` | `string` | `'Email'` | The field's label and the name screen readers say for it, as one string. Say what the address is for when the page might have another email field, such as a footer under a sign-in form. |
|
|
26
|
+
| `submitLabel` | `string` | `'Subscribe'` | The button text. |
|
|
27
|
+
| `busyLabel` | `string` | `'Subscribing'` | What a screen reader hears while onSubmit is pending. |
|
|
28
|
+
| `successTitle` | `string` | `'You are subscribed'` | The success heading. |
|
|
29
|
+
| `successBody` | `(email: string) => ReactNode` | `email => `Updates go to ${email}.`` | The line under the success heading. It receives the subscribed address. |
|
|
30
|
+
| `headingLevel` | `2 \| 3 \| 4` | `3` | The level of the error summary heading and the success heading. Make it one below the heading of the section the form sits in. |
|
|
31
|
+
| `className` | `string` | | Added to the root element. |
|
|
32
|
+
| `ref` | `Ref<HTMLDivElement>` | | Passed to the root element, which stays on the page when the success message replaces the form. |
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
37
|
+
|
|
38
|
+
### A sign-up that succeeds
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
const wait = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms))
|
|
42
|
+
|
|
43
|
+
const CONSENT = 'One email a month. Every one has an unsubscribe link.'
|
|
44
|
+
|
|
45
|
+
async function accept() {
|
|
46
|
+
await wait(700)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
<div className="flex w-full flex-col gap-3">
|
|
50
|
+
<p className="font-body text-sm text-[var(--color-text-secondary)]">
|
|
51
|
+
Demo: any address is accepted after a short wait. Nothing is sent.
|
|
52
|
+
</p>
|
|
53
|
+
<NewsletterForm label="Field notes by email" consent={CONSENT} onSubmit={accept} headingLevel={4} />
|
|
54
|
+
</div>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### A sign-up that fails
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
const wait = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms))
|
|
61
|
+
|
|
62
|
+
async function refuse() {
|
|
63
|
+
await wait(700)
|
|
64
|
+
throw new Error('The demo server is switched off.')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
<div className="flex w-full flex-col gap-3">
|
|
68
|
+
<p className="font-body text-sm text-[var(--color-text-secondary)]">
|
|
69
|
+
Demo: this one rejects every address, so you can see the alert, and the address you typed stays in the field.
|
|
70
|
+
</p>
|
|
71
|
+
<NewsletterForm
|
|
72
|
+
label="Restock alerts by email"
|
|
73
|
+
consent="An email when a sold-out replica is back. Nothing else."
|
|
74
|
+
submitLabel="Notify me"
|
|
75
|
+
busyLabel="Signing you up"
|
|
76
|
+
onSubmit={refuse}
|
|
77
|
+
headingLevel={4}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Keyboard
|
|
83
|
+
|
|
84
|
+
| Keys | Result |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `Tab` | Moves to the field, then the button. Screen readers read the consent line with the button. |
|
|
87
|
+
| `Enter` | In the field or on the button, it submits, and it does nothing while a submit is pending. On the error summary entry, it moves focus to the field. |
|
|
88
|
+
|
|
89
|
+
## Do
|
|
90
|
+
|
|
91
|
+
- Say in the consent line how often you write and how to stop.
|
|
92
|
+
- Start the section it sits in with a heading that says what the emails are about.
|
|
93
|
+
- Reject onSubmit when the request fails. The form shows the alert and keeps the address.
|
|
94
|
+
- Set headingLevel to one below the section heading above it.
|
|
95
|
+
|
|
96
|
+
## Don't
|
|
97
|
+
|
|
98
|
+
- Don't pre-tick anything, or sign people up as a side effect of another form.
|
|
99
|
+
- Don't put network code in it. onSubmit belongs to you; the form handles busy, failure and success.
|
|
100
|
+
- Don't place it on a raised panel and remove its fill. Field errors sit on surface.base.
|
|
101
|
+
- Don't ask for a name. Having just one field is the point.
|
|
102
|
+
|
|
103
|
+
## Anatomy
|
|
104
|
+
|
|
105
|
+
- **Form**: Screen readers name it by the label prop. It sits on opaque surface.base, in a hairline frame with the corner crosshair.
|
|
106
|
+
- **Error summary**: Appears when a submit fails and takes focus. Its entry moves focus to the field. It waits for someone to try submitting: leaving the field (a blur) never brings it up on its own.
|
|
107
|
+
- **Email**: A FormField wrapped around an Input, set to type email, autocomplete email and required.
|
|
108
|
+
- **Consent**: Required. What subscribing means and how to stop. Screen readers read it as the submit button's description.
|
|
109
|
+
- **Submit**: A Button that keeps focus and shows it's busy while onSubmit is pending. aria-busy and aria-disabled, two ARIA attributes (Accessible Rich Internet Applications, the labels screen readers rely on), tell screen readers the same.
|
|
110
|
+
- **Submit error**: An alert region that is always there and gets its text when onSubmit fails. The visible message only appears then. It clears before the next try.
|
|
111
|
+
- **Success**: Takes the place of the form: a check icon, a heading that takes focus, and the address that was subscribed.
|
|
112
|
+
- **Status**: Hidden from view and present from the start. It announces the success only if focus had already left the form.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# PageHeader
|
|
2
|
+
|
|
3
|
+
> The top of a screen. It holds the page’s one h1 (the main, level-1 heading), and can add an eyebrow, a description, a breadcrumb trail and the page’s own actions at the far edge.
|
|
4
|
+
|
|
5
|
+
- **Best for:** The top of every screen: its one h1, with eyebrow, description, breadcrumbs and page actions.
|
|
6
|
+
- **Not for:** A section inside a page: use SectionHeading.
|
|
7
|
+
- Molecule. Source: `src/components/molecules/page-header/PageHeader.tsx`
|
|
8
|
+
- Rendered docs page: `/components/page-header`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { PageHeader } from 'src/components/molecules/page-header/PageHeader'
|
|
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
|
+
| `title` | `string` | | Required. The page’s name, shown as its h1. |
|
|
23
|
+
| `eyebrow` | `ReactNode` | | A short label above the title: the area the page belongs to. |
|
|
24
|
+
| `description` | `ReactNode` | | One or two sentences under the title. |
|
|
25
|
+
| `actions` | `ReactNode` | | The page’s own actions, usually one or two Buttons. The component lays out the row and its gap. |
|
|
26
|
+
| `breadcrumbs` | `ReactNode` | | A Breadcrumbs element, drawn above the eyebrow. |
|
|
27
|
+
| `id` | `string` | | Goes on the h1, not the wrapper, so main can point aria-labelledby at it. That ARIA (Accessible Rich Internet Applications) attribute lets the main area take its name from the heading. |
|
|
28
|
+
| `level` | `1 \| 2` | `1` | Use 2 only for a page-style header inside a page that already has its h1 (a pane, a tab panel). It looks the same either way. |
|
|
29
|
+
| `className` | `string` | | Extra CSS classes, merged onto the outer element. When two Tailwind classes clash, the later one wins. |
|
|
30
|
+
| `ref` | `Ref<HTMLDivElement>` | | Passed through to the outer element. |
|
|
31
|
+
|
|
32
|
+
## Examples
|
|
33
|
+
|
|
34
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
35
|
+
|
|
36
|
+
### Title and description
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
<PageHeader level={2} title="Orders" description="Every order from the last 90 days, newest first." />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### With an eyebrow and actions
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
<PageHeader
|
|
46
|
+
level={2}
|
|
47
|
+
eyebrow="Operations"
|
|
48
|
+
title="Inventory"
|
|
49
|
+
description="Stock across both depots, counted at close of day."
|
|
50
|
+
actions={
|
|
51
|
+
<>
|
|
52
|
+
<Button variant="secondary">Export</Button>
|
|
53
|
+
<Button>Add replica</Button>
|
|
54
|
+
</>
|
|
55
|
+
}
|
|
56
|
+
/>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Under a breadcrumb trail
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<PageHeader
|
|
63
|
+
level={2}
|
|
64
|
+
breadcrumbs={
|
|
65
|
+
<Breadcrumbs
|
|
66
|
+
items={[
|
|
67
|
+
{ label: 'Account', href: '#account' },
|
|
68
|
+
{ label: 'Settings', href: '#settings' },
|
|
69
|
+
{ label: 'Notifications' },
|
|
70
|
+
]}
|
|
71
|
+
/>
|
|
72
|
+
}
|
|
73
|
+
title="Notifications"
|
|
74
|
+
description="What we send, and where."
|
|
75
|
+
actions={<Button variant="secondary">Reset to defaults</Button>}
|
|
76
|
+
/>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Keyboard
|
|
80
|
+
|
|
81
|
+
| Keys | Result |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `Tab` | Moves through the breadcrumb links, then the actions. The header itself never takes keyboard focus. |
|
|
84
|
+
|
|
85
|
+
## Do
|
|
86
|
+
|
|
87
|
+
- Use one per screen, at the top, inside main.
|
|
88
|
+
- Keep the eyebrow to the area’s name (“Operations”), and the title to the page’s.
|
|
89
|
+
- Put the page’s primary action last in the actions row, so it lands at the far edge.
|
|
90
|
+
- When the page has more than one landmark (a named region screen readers can jump to), give the h1 an id and use it to name main.
|
|
91
|
+
|
|
92
|
+
## Don't
|
|
93
|
+
|
|
94
|
+
- Don’t use it for a section inside a page. That’s a SectionHeading.
|
|
95
|
+
- Don’t put the page’s filters or tabs in the actions slot. They belong to the content under it.
|
|
96
|
+
- Don’t repeat the title in the eyebrow. The eyebrow names where the page sits, and the title names the page.
|
|
97
|
+
- Don’t add a second h1 inside the description or the actions.
|
|
98
|
+
|
|
99
|
+
## Common mistakes
|
|
100
|
+
|
|
101
|
+
Wrong:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
<PageHeader level={2} title="Orders" />
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Right:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
<PageHeader title="Orders" />
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The examples on this page pass level={2} only because they sit inside this docs page, which has its own h1. A screen’s own header is its h1, so leave level out.
|
|
114
|
+
|
|
115
|
+
Wrong:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
<PageHeader title="Inventory" />
|
|
119
|
+
<PageHeader title="Low stock" />
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Right:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<PageHeader title="Inventory" />
|
|
126
|
+
<SectionHeading level={2} title="Low stock" />
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
A page has one h1. Every heading under it is a SectionHeading, at the level the page’s outline of headings needs.
|
|
130
|
+
|
|
131
|
+
Wrong:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
<PageHeader title="Inventory" breadcrumbs={[{ label: "Operations" }]} />
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Right:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<PageHeader title="Inventory" breadcrumbs={<Breadcrumbs items={[{ label: "Operations", href: "/ops" }, { label: "Inventory" }]} />} />
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
breadcrumbs is a slot, not an array. Pass a Breadcrumbs element, which keeps its own renderLink and maxItems.
|
|
144
|
+
|
|
145
|
+
## Anatomy
|
|
146
|
+
|
|
147
|
+
- **Breadcrumbs**: Optional. A Breadcrumbs trail above everything else, first in reading order.
|
|
148
|
+
- **Eyebrow**: Optional. A small label: JetBrains Mono, 12px (pixels), semibold, uppercase, in text.muted. It's a paragraph above the h1 and isn't part of the heading's name.
|
|
149
|
+
- **Title**: The page’s h1. Oxanium, semibold, 24px, growing to 30px when the header gets 48rem or more of width (a rem is the base text size, usually 16px).
|
|
150
|
+
- **Description**: Optional. 16px body text in text.secondary, with lines no longer than 64 characters.
|
|
151
|
+
- **Actions**: Optional. The page’s own controls at the far edge, in a row with a 12px gap. The row drops under the title when it runs out of room.
|