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,132 @@
|
|
|
1
|
+
# AuthForm
|
|
2
|
+
|
|
3
|
+
> One form with four modes: sign in, create an account, reset a password, or enter a two-step code. It's built so password managers, pasting and keyboards all just work.
|
|
4
|
+
|
|
5
|
+
- **Best for:** Sign in, create an account, reset a password or enter a two-step code.
|
|
6
|
+
- **Not for:** A newsletter sign-up: use NewsletterForm. Any other form: build it from FormField controls.
|
|
7
|
+
- Organism. Source: `src/components/organisms/auth-form/AuthForm.tsx`
|
|
8
|
+
- Rendered docs page: `/components/auth-form`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { AuthForm } from 'src/components/organisms/auth-form/AuthForm'
|
|
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
|
+
| `mode` | `'sign-in' \| 'create-account' \| 'reset-password' \| 'verify-code'` | | Required. Which form shows. |
|
|
23
|
+
| `onSubmit` | `(values: AuthValues, mode: AuthMode) => void \| AuthErrors \| Promise<void \| AuthErrors>` | | Required. Runs once every field passes its checks. Resolve with errors keyed by field name to show them. If it rejects, a general alert for the whole form appears, so the failure isn't lost. |
|
|
24
|
+
| `onModeChange` | `(mode: AuthMode) => void` | | Offers the other modes and reports when someone switches. |
|
|
25
|
+
| `headingLevel` | `2 \| 3 \| 4` | `2` | Match the page's heading outline. The error summary sits one level below. The exception is 4, the lowest heading this system uses: there the summary stays an h4 (a level-4 heading) too. |
|
|
26
|
+
| `className` | `string` | | Merged with the root classes. Later Tailwind utilities win. |
|
|
27
|
+
| `ref` | `Ref<HTMLFormElement>` | | The form. |
|
|
28
|
+
|
|
29
|
+
## Types and helpers
|
|
30
|
+
|
|
31
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
export type AuthMode = 'sign-in' | 'create-account' | 'reset-password' | 'verify-code'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
export type AuthField = 'name' | 'email' | 'password' | 'code'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
export type AuthValues = Record<AuthField, string>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
export type AuthErrors = FieldErrors<AuthField>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Examples
|
|
50
|
+
|
|
51
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
52
|
+
|
|
53
|
+
### Sign in, with a server error, and the other modes
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
const wait = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms))
|
|
57
|
+
|
|
58
|
+
function AccountExample() {
|
|
59
|
+
const [mode, setMode] = useState<AuthMode>('sign-in')
|
|
60
|
+
const [signedIn, setSignedIn] = useState('')
|
|
61
|
+
const submit = async (values: AuthValues, current: AuthMode) => {
|
|
62
|
+
await wait(600)
|
|
63
|
+
if (current === 'sign-in' && values.password !== 'hexline') {
|
|
64
|
+
return { password: 'That email and password do not match.' }
|
|
65
|
+
}
|
|
66
|
+
if (current === 'sign-in' || current === 'create-account') setSignedIn(`Signed in as ${values.email}. This is a demo.`)
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
return (
|
|
70
|
+
<div className="flex flex-col gap-3 w-full max-w-sm">
|
|
71
|
+
<p className="font-body text-sm text-[var(--color-text-secondary)]">
|
|
72
|
+
Demo: any email works, and the password <code className="font-mono">hexline</code> signs in.
|
|
73
|
+
</p>
|
|
74
|
+
<AuthForm
|
|
75
|
+
mode={mode}
|
|
76
|
+
onModeChange={next => {
|
|
77
|
+
setMode(next)
|
|
78
|
+
setSignedIn('')
|
|
79
|
+
}}
|
|
80
|
+
onSubmit={submit}
|
|
81
|
+
headingLevel={4}
|
|
82
|
+
/>
|
|
83
|
+
<p role="status" className="font-body text-sm text-[var(--color-text-primary)]">
|
|
84
|
+
{signedIn}
|
|
85
|
+
</p>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
<AccountExample />
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Two-step sign-in with a code
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
const wait = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms))
|
|
97
|
+
|
|
98
|
+
<AuthForm mode="verify-code" onSubmit={() => wait(600).then(() => undefined)} headingLevel={4} />
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Keyboard
|
|
102
|
+
|
|
103
|
+
| Keys | Result |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `Tab` | Moves through the fields (the password field, then its Show password button), the submit button and the mode links. |
|
|
106
|
+
| `Enter` | In a field, submits. On an error summary entry or a mode link, activates it. |
|
|
107
|
+
| `Space` | On Show password, toggles it. On a mode link, activates it. |
|
|
108
|
+
|
|
109
|
+
## Do
|
|
110
|
+
|
|
111
|
+
- Let password managers work. Every field has its autocomplete token, and pasting is never blocked.
|
|
112
|
+
- Resolve onSubmit with errors keyed by field (a wrong password, say), so they show in the summary and at the field.
|
|
113
|
+
- Expect a rejected onSubmit to show up as its own alert instead of failing silently. There's no field to attach it to.
|
|
114
|
+
- Keep the reset confirmation neutral. It never says whether an account exists.
|
|
115
|
+
|
|
116
|
+
## Don't
|
|
117
|
+
|
|
118
|
+
- Don't add a CAPTCHA, a puzzle or a copy-paste block. Signing in mustn't test memory or copying by hand (WCAG success criterion 3.3.8; WCAG is the Web Content Accessibility Guidelines, the public rulebook for usable websites).
|
|
119
|
+
- Don't disable the submit button while fields are empty. The error summary says what's missing.
|
|
120
|
+
- Don't validate on every keystroke.
|
|
121
|
+
- Don't count on an error keyed to a field the current mode doesn't show (name while signing in, for example) landing where you'd expect. It's moved onto email, and a console.warn that only shows in development names it. Key it to a field the mode actually shows instead.
|
|
122
|
+
|
|
123
|
+
## Anatomy
|
|
124
|
+
|
|
125
|
+
- **Heading**: Names the form. Focus (the place your keyboard typing goes) moves here after a mode switch.
|
|
126
|
+
- **Error summary**: Shows up when a submit fails and takes focus. Each entry jumps to its field. It waits for a real submit attempt: leaving a field (a blur) never brings it up, so it can't shove aside a field someone's about to click.
|
|
127
|
+
- **Fields**: A FormField around each Input, with its help and error text connected to it.
|
|
128
|
+
- **Show password**: An icon button inside the password field that shows the typed text. It uses Input's revealable option, reports on or off through aria-pressed (ARIA, Accessible Rich Internet Applications, is a set of attributes that tell screen readers what a control is doing), and keeps the same name either way. The text hides again on submit and on a mode switch.
|
|
129
|
+
- **Submit**: A Button that stays focusable and shows it's busy while the caller works.
|
|
130
|
+
- **Confirmation**: A status line that's always on the page. Reset mode uses it.
|
|
131
|
+
- **Submit error**: An alert that's always present and gets its text when onSubmit rejects (the visible copy only appears then). It clears on the next submit and on a mode switch.
|
|
132
|
+
- **Mode links**: Shown when onModeChange is given: forgot password, create an account, back to sign in. They're buttons, since they change the form in place, but they look exactly like Link: signal text, an underline that thickens on hover, a pointer cursor and a focus ring.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# AvatarGroup
|
|
2
|
+
|
|
3
|
+
> Overlapping avatars (small profile pictures) with a “+N” for the rest. It’s a list named by its count, so a screen reader hears “6 members” and then each person. The people behind the “+N” are named in words, never only counted.
|
|
4
|
+
|
|
5
|
+
- **Best for:** Several people in one place, with a count: a squad, the people on an order, who’s viewing.
|
|
6
|
+
- **Not for:** Picking people: use MultiSelect.
|
|
7
|
+
- Molecule. Source: `src/components/molecules/avatar-group/AvatarGroup.tsx`
|
|
8
|
+
- Rendered docs page: `/components/avatar-group`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { AvatarGroup } from 'src/components/molecules/avatar-group/AvatarGroup'
|
|
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
|
+
- With overflowTooltip, the root <TooltipProvider>. AppShell and the demo layout each include one.
|
|
21
|
+
|
|
22
|
+
## Props
|
|
23
|
+
|
|
24
|
+
| Prop | Type | Default | Description |
|
|
25
|
+
|---|---|---|---|
|
|
26
|
+
| `people` | `AvatarGroupPerson[]` | | Required. Everyone in the group, in the order shown. |
|
|
27
|
+
| `max` | `number` | `4` | The most avatars drawn. The rest go into the +N chip. |
|
|
28
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Passed to every Avatar and to the chip. |
|
|
29
|
+
| `label` | `string` | | What the group is. The count is added on the end: “Alpha squad, 6 members”. |
|
|
30
|
+
| `overflowTooltip` | `boolean` | `false` | Shows the hidden names in a Tooltip on the chip and lets the chip take keyboard focus. |
|
|
31
|
+
| `className` | `string` | | Merged with the list classes. Later Tailwind utility classes win. |
|
|
32
|
+
| `ref` | `Ref<HTMLUListElement>` | | Passed through to the list. |
|
|
33
|
+
| `...rest` | `Omit<HTMLAttributes<HTMLUListElement>, 'children' \| 'aria-label'>` | | Every standard ul prop except children (the people are the content) and aria-label (label builds it, with the count). |
|
|
34
|
+
| `name (AvatarGroupPerson)` | `string` | | Required. Passed to Avatar. |
|
|
35
|
+
| `src (AvatarGroupPerson)` | `string` | | Optional image, passed to Avatar. |
|
|
36
|
+
| `status (AvatarGroupPerson)` | `'online' \| 'away' \| 'busy' \| 'offline'` | | Optional presence (whether they’re online), passed to Avatar, which includes it in the name screen readers hear. |
|
|
37
|
+
|
|
38
|
+
## Types and helpers
|
|
39
|
+
|
|
40
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
export type AvatarGroupPerson = { name: string; src?: string; status?: AvatarStatus }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Examples
|
|
47
|
+
|
|
48
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
49
|
+
|
|
50
|
+
### Sizes: four people shown, and the rest counted in a small chip
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
const ALPHA: AvatarGroupPerson[] = [
|
|
54
|
+
{ name: 'Ilse Varga', status: 'online' },
|
|
55
|
+
{ name: 'Ravi Okonkwo', status: 'away' },
|
|
56
|
+
{ name: 'Mei Tanaka', status: 'busy' },
|
|
57
|
+
{ name: 'Sana Idrissi', status: 'online' },
|
|
58
|
+
{ name: 'Dana Iverson', status: 'offline' },
|
|
59
|
+
{ name: 'Lin Moreau', status: 'online' },
|
|
60
|
+
{ name: 'Tomas Reyes', status: 'offline' },
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
<div className="flex flex-col gap-4">
|
|
64
|
+
<AvatarGroup people={ALPHA} size="sm" label="Alpha squad" />
|
|
65
|
+
<AvatarGroup people={ALPHA} size="md" label="Alpha squad" />
|
|
66
|
+
<AvatarGroup people={ALPHA} size="lg" label="Alpha squad" />
|
|
67
|
+
</div>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Everyone fits: no chip
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
const BRAVO: AvatarGroupPerson[] = [{ name: 'Noor Haddad' }, { name: 'Kai Lindqvist' }, { name: 'Ada Brennan' }]
|
|
74
|
+
|
|
75
|
+
<AvatarGroup people={BRAVO} label="Bravo squad" />
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### The hidden names in a tooltip (press Tab to reach the chip)
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
const ALPHA: AvatarGroupPerson[] = [
|
|
82
|
+
{ name: 'Ilse Varga', status: 'online' },
|
|
83
|
+
{ name: 'Ravi Okonkwo', status: 'away' },
|
|
84
|
+
{ name: 'Mei Tanaka', status: 'busy' },
|
|
85
|
+
{ name: 'Sana Idrissi', status: 'online' },
|
|
86
|
+
{ name: 'Dana Iverson', status: 'offline' },
|
|
87
|
+
{ name: 'Lin Moreau', status: 'online' },
|
|
88
|
+
{ name: 'Tomas Reyes', status: 'offline' },
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
<AvatarGroup people={ALPHA} max={3} label="Alpha squad" overflowTooltip />
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### In a row, beside the words it summarises
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
const ALPHA: AvatarGroupPerson[] = [
|
|
98
|
+
{ name: 'Ilse Varga', status: 'online' },
|
|
99
|
+
{ name: 'Ravi Okonkwo', status: 'away' },
|
|
100
|
+
{ name: 'Mei Tanaka', status: 'busy' },
|
|
101
|
+
{ name: 'Sana Idrissi', status: 'online' },
|
|
102
|
+
{ name: 'Dana Iverson', status: 'offline' },
|
|
103
|
+
{ name: 'Lin Moreau', status: 'online' },
|
|
104
|
+
{ name: 'Tomas Reyes', status: 'offline' },
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
<div className="flex items-center gap-3">
|
|
108
|
+
<AvatarGroup people={ALPHA} max={3} size="sm" label="Watching this replica" />
|
|
109
|
+
<p className="text-sm text-[var(--color-text-secondary)]">7 people are watching this replica</p>
|
|
110
|
+
</div>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Keyboard
|
|
114
|
+
|
|
115
|
+
| Keys | Result |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `Tab` | With overflowTooltip, focuses the +N chip and opens the tooltip. Otherwise nothing in the group can take focus. |
|
|
118
|
+
|
|
119
|
+
## Do
|
|
120
|
+
|
|
121
|
+
- Give it a label that says who the people are: a squad, the people on an order, who’s viewing.
|
|
122
|
+
- Keep max at four or fewer in dense rows. The chip covers the rest.
|
|
123
|
+
- Put the full list somewhere people can reach (a dialog, a roster page) when they need to act on it.
|
|
124
|
+
|
|
125
|
+
## Don't
|
|
126
|
+
|
|
127
|
+
- Don't use it as a control. To pick people, use MultiSelect. To open the list, put a Button next to it.
|
|
128
|
+
- Don't set overflowTooltip on every row of a long table, because each chip becomes a Tab stop.
|
|
129
|
+
- Don't pass aria-label. The label plus the count is the name.
|
|
130
|
+
|
|
131
|
+
## Common mistakes
|
|
132
|
+
|
|
133
|
+
Wrong:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
<div className="flex -space-x-2">{people.map(p => <Avatar name={p.name} />)}</div>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Right:
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<AvatarGroup people={people} label="Alpha squad" />
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A hand-built stack has no count, no thin outline between the faces, and no words for the people a +N leaves out.
|
|
146
|
+
|
|
147
|
+
## Anatomy
|
|
148
|
+
|
|
149
|
+
- **List**: A ul (a bulleted list element) named “<label>, N members”, or just “N members” when there’s no label.
|
|
150
|
+
- **Item**: One Avatar in a 2px (two pixel) frame in surface.base, cut to the same corner. Each one slides a quarter of its width under the one before.
|
|
151
|
+
- **+N chip**: Shaped like an avatar, in monospaced text in text.secondary. The “+2” is aria-hidden (screen readers skip it), and hidden text reading “and 2 more: …” names the people it stands for.
|
|
152
|
+
- **Tooltip**: Optional. Shows the hidden names on hover and on keyboard focus. The chip then becomes a Tab stop, with its focus ring drawn on an outer box that isn’t clipped.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Avatar
|
|
2
|
+
|
|
3
|
+
> A small square that shows who someone is. The name is required, because it's both the alt text (the description screen readers read for an image) and the source of the initials. Without one, an avatar is just an image nobody can identify.
|
|
4
|
+
|
|
5
|
+
- **Best for:** Showing who a person is, next to their name, as in a roster row, a comment or an account menu.
|
|
6
|
+
- **Not for:** Several people in one spot: use AvatarGroup.
|
|
7
|
+
- Atom. Source: `src/components/atoms/avatar/Avatar.tsx`
|
|
8
|
+
- Rendered docs page: `/components/avatar`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Avatar, avatarVariants, initialsFrom } from 'src/components/atoms/avatar/Avatar'
|
|
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
|
+
| `name` | `string` | | Required. The source of the fallback initials and the announced name. |
|
|
23
|
+
| `src` | `string` | | Optional image. If it can't load, the fallback takes its place without the layout jumping. |
|
|
24
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 24 / 36 / 48px. |
|
|
25
|
+
| `status` | `'online' \| 'away' \| 'busy' \| 'offline'` | | Whether the person is around. It adds the dot and adds the word to the announced name. Leave it out when you don't know. |
|
|
26
|
+
| `className` | `string` | | Merged with the component's classes. When Tailwind utility classes clash, the later one wins. |
|
|
27
|
+
| `ref` | `Ref<HTMLSpanElement>` | | Passed through to the root element. |
|
|
28
|
+
| `...rest` | `Omit<HTMLAttributes<HTMLSpanElement>, 'aria-label' \| 'children'>` | | Every native span prop except aria-label and children. aria-label is removed from the types rather than quietly overwritten when the page runs. The announced name comes from the sr-only span, and an aria-label on the root would override it. children is removed because the component renders its own image/fallback/sr-only children, so anything passed as children would silently disappear. |
|
|
29
|
+
|
|
30
|
+
## Types and helpers
|
|
31
|
+
|
|
32
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
/** First letter of the first two words, uppercased.
|
|
36
|
+
*
|
|
37
|
+
* `[...word][0]`, not `word[0]`: a string index yields a UTF-16 code unit, so a name whose first
|
|
38
|
+
* character is outside the Basic Multilingual Plane (an emoji, an extended CJK ideograph, a
|
|
39
|
+
* mathematical or historic-script letter) returns a lone high surrogate — an unpaired half of a
|
|
40
|
+
* character, which renders as a replacement glyph. Spreading iterates by code point instead. */
|
|
41
|
+
export function initialsFrom(name: string)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
export const avatarVariants = cva( …
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
/** Every native span prop except `aria-label` is available — a molecule composing an Avatar can
|
|
50
|
+
* attach a `data-testid` or extra `aria-*`/`data-*` attributes. `aria-label` is typed `never`
|
|
51
|
+
* rather than merely omitted: Avatar's whole guarantee is that the announced name comes from the
|
|
52
|
+
* `sr-only` span's content and is identical whether or not the image loads. An `aria-label` on
|
|
53
|
+
* the root would silently outrank that content-derived name in the accessibility tree (an
|
|
54
|
+
* element's `aria-label` always wins over its text content when the accessible name is
|
|
55
|
+
* computed), defeating the one thing this component promises. `Omit<>` alone is a no-op here —
|
|
56
|
+
* TypeScript's JSX checker never treats an `aria-*`/`data-*`-prefixed attribute as an
|
|
57
|
+
* excess/unknown property — so the key must be redeclared with type `never` to actually be
|
|
58
|
+
* enforced. Same remedy as `IconButton`'s `aria-label` exclusion (see `IconButton.tsx`).
|
|
59
|
+
*
|
|
60
|
+
* `children` is excluded for a plainer reason: `HTMLAttributes` offers it, but this component
|
|
61
|
+
* renders its own image/fallback/`sr-only` children, so JSX children on the element always win
|
|
62
|
+
* and a `children` prop is silently discarded — reachable, typed, and dead. It is an ordinary
|
|
63
|
+
* key, so the `Omit` does the rejecting and the declared `never` only makes the error legible;
|
|
64
|
+
* both are shipped, matching `IconButton`. */
|
|
65
|
+
export type AvatarStatus = 'online' | 'away' | 'busy' | 'offline'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Examples
|
|
69
|
+
|
|
70
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
71
|
+
|
|
72
|
+
### Sizes
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<Avatar name="Dana Iverson" size="sm" />
|
|
76
|
+
<Avatar name="Dana Iverson" size="md" />
|
|
77
|
+
<Avatar name="Dana Iverson" size="lg" />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### With an image
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
const DEMO_IMAGE =
|
|
84
|
+
'data:image/svg+xml;utf8,' +
|
|
85
|
+
encodeURIComponent(
|
|
86
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">' +
|
|
87
|
+
'<rect width="64" height="32" fill="#7c9c6b"/>' + // token-discipline: allow, pixels of a sample photo, not interface colour
|
|
88
|
+
'<rect y="32" width="64" height="32" fill="#3f5a49"/>' +
|
|
89
|
+
'</svg>'
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
<Avatar name="Dana Iverson" src={DEMO_IMAGE} size="lg" />
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Fallback initials
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
<Avatar name="Dana Iverson" size="lg" />
|
|
99
|
+
<Avatar name="Ravi Okonkwo" size="lg" />
|
|
100
|
+
<Avatar name="Lin" size="lg" />
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Status: the dot is decoration, and the status word is part of the avatar's name
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
const STATUS_TEAM = [
|
|
107
|
+
{ name: 'Ilse Varga', status: 'online' },
|
|
108
|
+
{ name: 'Ravi Okonkwo', status: 'away' },
|
|
109
|
+
{ name: 'Mei Tanaka', status: 'busy' },
|
|
110
|
+
{ name: 'Sana Idrissi', status: 'offline' },
|
|
111
|
+
] as const
|
|
112
|
+
|
|
113
|
+
<div className="flex flex-col gap-3">
|
|
114
|
+
{STATUS_TEAM.map(member => (
|
|
115
|
+
<div key={member.name} className="flex items-center gap-3">
|
|
116
|
+
<Avatar name={member.name} status={member.status} size="md" />
|
|
117
|
+
<div>
|
|
118
|
+
<p className="text-sm">{member.name}</p>
|
|
119
|
+
{/* Shown as text too, so a sighted user never has to decode the colour. Hidden
|
|
120
|
+
from assistive tech, which already heard it in the avatar's name. */}
|
|
121
|
+
<p aria-hidden="true" className="text-sm text-[var(--color-text-secondary)] capitalize">
|
|
122
|
+
{member.status}
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
))}
|
|
127
|
+
</div>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### A row
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
const FIELD_TEAM = [
|
|
134
|
+
{ name: 'Dana Iverson', role: 'Route Lead' },
|
|
135
|
+
{ name: 'Ravi Okonkwo', role: 'Comms' },
|
|
136
|
+
{ name: 'Mei Tanaka', role: 'Medic' },
|
|
137
|
+
{ name: 'Sana Idrissi', role: 'Logistics' },
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
<div className="flex flex-col gap-3">
|
|
141
|
+
{FIELD_TEAM.map(member => (
|
|
142
|
+
<div key={member.name} className="flex items-center gap-3">
|
|
143
|
+
<Avatar name={member.name} size="md" />
|
|
144
|
+
<div>
|
|
145
|
+
<p className="text-sm">{member.name}</p>
|
|
146
|
+
<p className="text-xs text-[var(--color-text-muted)]">{member.role}</p>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Keyboard
|
|
154
|
+
|
|
155
|
+
Not focusable. No keyboard behaviour of its own.
|
|
156
|
+
|
|
157
|
+
## Do
|
|
158
|
+
|
|
159
|
+
- Pass the full name, not the initials. The component works those out itself.
|
|
160
|
+
- Let it fall back. The fallback is part of the design, not an error state.
|
|
161
|
+
- Use sm inside dense rows and tables.
|
|
162
|
+
- Set status only where presence is live and matters (a roster, a chat). A dot that's out of date is worse than no dot.
|
|
163
|
+
|
|
164
|
+
## Don't
|
|
165
|
+
|
|
166
|
+
- Don't round it. The system has no round corners, and an avatar isn't the place to start.
|
|
167
|
+
- Don't add the cut corner to the sm avatar. It's 24px, the same size Kbd leaves out by name. An 8px cut takes a third of that edge, and it looks like a broken square instead of a shaped one.
|
|
168
|
+
- Don't build an overlapping stack by hand. AvatarGroup does it, with a '+N' that's real text.
|
|
169
|
+
- Don't explain the dot colours only in a legend. The status word is already in the name, so show it as text where sighted users need it too.
|
|
170
|
+
- Don't use an avatar as the only label for a person. Put the name next to it in dense UI (user interface).
|
|
171
|
+
- Don't pass aria-label. It's removed from the types so nothing can override it, and name already decides the one label that gets announced.
|
|
172
|
+
|
|
173
|
+
## Anatomy
|
|
174
|
+
|
|
175
|
+
- **Root**: A border.hairline-strong edge on the sunken surface, with overflow clipped and a cut bottom-right corner at md and lg.
|
|
176
|
+
- **Image**: alt is empty on purpose, because the hidden name already does the announcing.
|
|
177
|
+
- **Fallback**: Initials worked out from the name, aria-hidden, shown when there's no image or it fails to load. aria-hidden is an ARIA attribute (Accessible Rich Internet Applications, the labels that tell screen readers what things are) that hides something from screen readers.
|
|
178
|
+
- **Hidden name**: sr-only (visible to screen readers only) and always there, so the announcement is the same either way. With a status it reads "Ilse Varga, online".
|
|
179
|
+
- **Status dot**: Optional. A square in the top-right corner (the bottom-right is the cut), in accent.ok.text, accent.warning.text, accent.hostile.text or text.muted, with a 2px (pixel) surface.base keyline around it. It's aria-hidden because the status is announced in the name.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Badge
|
|
2
|
+
|
|
3
|
+
> A small chip that shows a status. It comes in six tones and two emphases, and it always carries a word. A colour on its own isn't a status anyone can read.
|
|
4
|
+
|
|
5
|
+
- **Best for:** You need to label a status with a word and a tone, like an order state or a sync result.
|
|
6
|
+
- **Not for:** A filter or selection someone can remove. Use Tag for that.
|
|
7
|
+
- Atom. Source: `src/components/atoms/badge/Badge.tsx`
|
|
8
|
+
- Rendered docs page: `/components/badge`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Badge, badgeVariants } from 'src/components/atoms/badge/Badge'
|
|
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
|
+
| `tone` | `'neutral' \| 'signal' \| 'ok' \| 'warning' \| 'hostile' \| 'info'` | `'neutral'` | The status the badge shows. |
|
|
23
|
+
| `emphasis` | `'solid' \| 'subtle'` | `'subtle'` | Solid uses the invariant fill (the same in light and dark themes) with its matching ink. Subtle is the outline badge: a 1px border and text in the tone’s theme-adaptive text token, with no fill. There's no separate outline emphasis, because it would be this same look under a second name. |
|
|
24
|
+
| `size` | `'sm' \| 'md'` | `'sm'` | sm (small) is the chip for everyday interface: rows, cards, tables. md (medium) takes the small control height (32px) and the 14px text step. Use it for a badge next to display-size type, like a price. |
|
|
25
|
+
| `dot` | `boolean` | `false` | Adds a marker next to the text. |
|
|
26
|
+
| `icon` | `ReactNode` | | An icon before the text, hidden from screen readers with aria-hidden and sized to the badge. It takes the badge’s colour, so the same contrast pair that covers the text covers it too. It adds to the word. It never replaces it. |
|
|
27
|
+
| `className` | `string` | | Merged with the component classes. If two clash, the later Tailwind utility wins (Tailwind is the CSS framework this system styles with). |
|
|
28
|
+
| `ref` | `Ref<HTMLSpanElement>` | | Passed through to the span element. |
|
|
29
|
+
| `...rest` | `HTMLAttributes<HTMLSpanElement>` | | Any other span attribute. For example, aria-hidden on a count that a label next to it already reads out. |
|
|
30
|
+
|
|
31
|
+
## Types and helpers
|
|
32
|
+
|
|
33
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
export const badgeVariants = cva( …
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Examples
|
|
40
|
+
|
|
41
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
42
|
+
|
|
43
|
+
### Tones, subtle (the outline badge)
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
const TONES = ['neutral', 'signal', 'ok', 'warning', 'hostile', 'info'] as const
|
|
47
|
+
|
|
48
|
+
{TONES.map(tone => (
|
|
49
|
+
<Badge key={tone} tone={tone} emphasis="subtle">
|
|
50
|
+
{tone}
|
|
51
|
+
</Badge>
|
|
52
|
+
))}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Tones, solid
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
const TONES = ['neutral', 'signal', 'ok', 'warning', 'hostile', 'info'] as const
|
|
59
|
+
|
|
60
|
+
{TONES.map(tone => (
|
|
61
|
+
<Badge key={tone} tone={tone} emphasis="solid">
|
|
62
|
+
{tone}
|
|
63
|
+
</Badge>
|
|
64
|
+
))}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### With a dot
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
<Badge tone="ok" dot>
|
|
71
|
+
Nominal
|
|
72
|
+
</Badge>
|
|
73
|
+
<Badge tone="warning" dot>
|
|
74
|
+
Degraded
|
|
75
|
+
</Badge>
|
|
76
|
+
<Badge tone="hostile" dot>
|
|
77
|
+
Offline
|
|
78
|
+
</Badge>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### With an icon
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<Badge tone="ok" icon={<Check />}>
|
|
85
|
+
In stock
|
|
86
|
+
</Badge>
|
|
87
|
+
<Badge tone="warning" icon={<Clock />}>
|
|
88
|
+
Backorder
|
|
89
|
+
</Badge>
|
|
90
|
+
<Badge tone="signal" emphasis="solid" icon={<Zap />}>
|
|
91
|
+
New
|
|
92
|
+
</Badge>
|
|
93
|
+
<Badge tone="info" size="md" icon={<Truck />}>
|
|
94
|
+
Ships today
|
|
95
|
+
</Badge>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Sizes
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
<Badge tone="signal" emphasis="solid">
|
|
102
|
+
Sale
|
|
103
|
+
</Badge>
|
|
104
|
+
<Badge tone="signal" emphasis="solid" size="md">
|
|
105
|
+
Sale
|
|
106
|
+
</Badge>
|
|
107
|
+
<Badge tone="ok" dot>
|
|
108
|
+
In stock
|
|
109
|
+
</Badge>
|
|
110
|
+
<Badge tone="ok" dot size="md">
|
|
111
|
+
In stock
|
|
112
|
+
</Badge>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Size md, beside a price
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
<p className="flex flex-wrap items-center gap-x-4 gap-y-2 font-mono text-4xl tabular-nums text-[var(--color-text-primary)]">
|
|
119
|
+
<span>€189.00</span>
|
|
120
|
+
<Badge tone="signal" emphasis="solid" size="md">
|
|
121
|
+
−39%
|
|
122
|
+
</Badge>
|
|
123
|
+
</p>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### In a row
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
<div className="flex flex-col gap-2 w-full max-w-xs">
|
|
130
|
+
<div className="flex items-center justify-between">
|
|
131
|
+
<span className="text-sm">Depot Relay</span>
|
|
132
|
+
<Badge tone="ok" emphasis="subtle">
|
|
133
|
+
Nominal
|
|
134
|
+
</Badge>
|
|
135
|
+
</div>
|
|
136
|
+
<div className="flex items-center justify-between">
|
|
137
|
+
<span className="text-sm">North Sensor Array</span>
|
|
138
|
+
<Badge tone="warning" emphasis="subtle">
|
|
139
|
+
Degraded
|
|
140
|
+
</Badge>
|
|
141
|
+
</div>
|
|
142
|
+
<div className="flex items-center justify-between">
|
|
143
|
+
<span className="text-sm">Northgate Beacon</span>
|
|
144
|
+
<Badge tone="hostile" emphasis="solid">
|
|
145
|
+
Offline
|
|
146
|
+
</Badge>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Keyboard
|
|
152
|
+
|
|
153
|
+
Not focusable. No keyboard behaviour of its own.
|
|
154
|
+
|
|
155
|
+
## Do
|
|
156
|
+
|
|
157
|
+
- Give every badge a word.
|
|
158
|
+
- Go easy on solid. One solid badge in a row reads as the exception. Six read as noise.
|
|
159
|
+
- Keep the tone mapping the same across the product. ok is never ‘in progress’ on one screen and ‘done’ on another.
|
|
160
|
+
- Use size md when the badge sits next to a headline figure, so the two read as one statement.
|
|
161
|
+
|
|
162
|
+
## Don't
|
|
163
|
+
|
|
164
|
+
- Don't build a badge that's colour only, even with a tooltip.
|
|
165
|
+
- Don't put a badge inside a button. It muddies what the clickable target is.
|
|
166
|
+
- Don't use hostile for something that's only pending.
|
|
167
|
+
- Don't bevel it (give it a cut corner). At 22px a badge is chrome, the frame around content, not an object with its own footprint. Kbd follows the same rule for the same reason.
|
|
168
|
+
- Don't resize a badge with height or text classes. The two sizes are the prop. A third size is a change to the system.
|
|
169
|
+
- Don't use size md in a table row or on a card. It's as tall as a small button, so it starts to look like one.
|
|
170
|
+
- Don't give one badge both an icon and a dot. Pick one marker. Two say the same thing twice.
|
|
171
|
+
- Don't colour the icon yourself. It inherits the badge’s tone, and that's the pair the contrast gate (the automated colour-contrast check) measures.
|
|
172
|
+
|
|
173
|
+
## Anatomy
|
|
174
|
+
|
|
175
|
+
- **Chip**: Set in mono (a fixed-width typeface), uppercase. That's the tertiary type role. By default it's 22px tall with 11px text (px means pixels); at size md it's 32px tall with 14px text.
|
|
176
|
+
- **Icon**: Optional, and placed before the text. It's aria-hidden (an ARIA attribute, from Accessible Rich Internet Applications, that hides it from screen readers) and uses the current text colour. It's 12px, or 14px at size md.
|
|
177
|
+
- **Dot**: An optional square, also aria-hidden, in the current text colour. It's 6px, or 8px at size md.
|
|
178
|
+
- **Label**: Required. The word is what makes the badge accessible.
|