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.
- package/AGENTS.md +16 -2
- package/README.md +11 -0
- package/division-ds-0.2.0.tgz +0 -0
- package/package.json +2 -2
- package/public/docs/components/accordion.md +205 -0
- package/public/docs/components/ai-label.md +88 -0
- package/public/docs/components/alert-dialog.md +290 -0
- package/public/docs/components/alert.md +173 -0
- package/public/docs/components/approval-request.md +146 -0
- package/public/docs/components/auth-form.md +130 -0
- package/public/docs/components/avatar-group.md +150 -0
- package/public/docs/components/avatar.md +177 -0
- package/public/docs/components/badge.md +176 -0
- package/public/docs/components/breadcrumbs.md +110 -0
- package/public/docs/components/button.md +202 -0
- package/public/docs/components/card.md +255 -0
- package/public/docs/components/carousel.md +128 -0
- package/public/docs/components/cart.md +170 -0
- package/public/docs/components/chart-panel.md +356 -0
- package/public/docs/components/chat-message.md +154 -0
- package/public/docs/components/chat-thread.md +242 -0
- package/public/docs/components/checkbox.md +147 -0
- package/public/docs/components/checkout-form.md +126 -0
- package/public/docs/components/command-palette.md +145 -0
- package/public/docs/components/data-table.md +399 -0
- package/public/docs/components/description-list.md +101 -0
- package/public/docs/components/dialog.md +195 -0
- package/public/docs/components/divider.md +95 -0
- package/public/docs/components/dropdown-menu.md +220 -0
- package/public/docs/components/empty-state.md +143 -0
- package/public/docs/components/footer.md +395 -0
- package/public/docs/components/form-field.md +213 -0
- package/public/docs/components/icon-button.md +125 -0
- package/public/docs/components/input.md +198 -0
- package/public/docs/components/kanban-board.md +298 -0
- package/public/docs/components/kbd.md +70 -0
- package/public/docs/components/link.md +96 -0
- package/public/docs/components/list-item.md +238 -0
- package/public/docs/components/loading-animation.md +70 -0
- package/public/docs/components/multi-select.md +200 -0
- package/public/docs/components/navbar.md +142 -0
- package/public/docs/components/newsletter-form.md +110 -0
- package/public/docs/components/page-header.md +149 -0
- package/public/docs/components/pagination.md +210 -0
- package/public/docs/components/product-card.md +169 -0
- package/public/docs/components/progress.md +99 -0
- package/public/docs/components/prompt-input.md +149 -0
- package/public/docs/components/quantity-stepper.md +138 -0
- package/public/docs/components/radio.md +140 -0
- package/public/docs/components/rating.md +104 -0
- package/public/docs/components/score-ring.md +93 -0
- package/public/docs/components/search.md +114 -0
- package/public/docs/components/section-heading.md +111 -0
- package/public/docs/components/segmented-control.md +124 -0
- package/public/docs/components/select.md +222 -0
- package/public/docs/components/sheet.md +143 -0
- package/public/docs/components/sidebar.md +191 -0
- package/public/docs/components/skeleton.md +133 -0
- package/public/docs/components/slider.md +112 -0
- package/public/docs/components/sparkline.md +126 -0
- package/public/docs/components/spinner.md +96 -0
- package/public/docs/components/stat.md +104 -0
- package/public/docs/components/status-tracker.md +143 -0
- package/public/docs/components/stepper.md +145 -0
- package/public/docs/components/switch.md +151 -0
- package/public/docs/components/tabs.md +178 -0
- package/public/docs/components/tag.md +103 -0
- package/public/docs/components/textarea.md +146 -0
- package/public/docs/components/timeline.md +150 -0
- package/public/docs/components/toast.md +192 -0
- package/public/docs/components/tool-call.md +105 -0
- package/public/docs/components/tooltip.md +106 -0
- package/public/manifest/components.json +182 -6
- package/public/manifest/tokens.json +39 -0
- package/scripts/division-ds.mjs +74 -38
- package/scripts/lib/agent-docs.mjs +53 -1
- package/scripts/lib/ai-copy.mjs +106 -0
- package/scripts/lib/button-spacing.mjs +5 -5
- package/scripts/lib/check-text.mjs +44 -0
- package/scripts/lib/contrast.mjs +111 -0
- package/scripts/lib/host-inline.mjs +59 -0
- package/scripts/lib/mcp-server.mjs +115 -0
- package/scripts/lib/mcp-tools.mjs +241 -0
- package/scripts/lib/props-validator.mjs +1 -1
- package/scripts/lib/rules-installer.mjs +42 -4
- 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 +7 -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 +19 -0
- package/skills/division-ds/topics/layout.md +34 -0
- package/skills/division-ds/topics/motion.md +10 -0
- 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.
|