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.
Files changed (98) hide show
  1. package/AGENTS.md +23 -3
  2. package/README.md +11 -0
  3. package/division-ds-0.2.1.tgz +0 -0
  4. package/package.json +2 -2
  5. package/public/docs/components/accordion.md +207 -0
  6. package/public/docs/components/ai-label.md +90 -0
  7. package/public/docs/components/alert-dialog.md +292 -0
  8. package/public/docs/components/alert.md +175 -0
  9. package/public/docs/components/approval-request.md +148 -0
  10. package/public/docs/components/auth-form.md +132 -0
  11. package/public/docs/components/avatar-group.md +152 -0
  12. package/public/docs/components/avatar.md +179 -0
  13. package/public/docs/components/badge.md +178 -0
  14. package/public/docs/components/breadcrumbs.md +112 -0
  15. package/public/docs/components/button.md +204 -0
  16. package/public/docs/components/card.md +257 -0
  17. package/public/docs/components/carousel.md +130 -0
  18. package/public/docs/components/cart.md +172 -0
  19. package/public/docs/components/chart-panel.md +358 -0
  20. package/public/docs/components/chat-message.md +157 -0
  21. package/public/docs/components/chat-thread.md +244 -0
  22. package/public/docs/components/checkbox.md +149 -0
  23. package/public/docs/components/checkout-form.md +128 -0
  24. package/public/docs/components/command-palette.md +147 -0
  25. package/public/docs/components/data-table.md +401 -0
  26. package/public/docs/components/description-list.md +103 -0
  27. package/public/docs/components/dialog.md +197 -0
  28. package/public/docs/components/divider.md +97 -0
  29. package/public/docs/components/dropdown-menu.md +222 -0
  30. package/public/docs/components/empty-state.md +145 -0
  31. package/public/docs/components/footer.md +397 -0
  32. package/public/docs/components/form-field.md +216 -0
  33. package/public/docs/components/icon-button.md +127 -0
  34. package/public/docs/components/input.md +200 -0
  35. package/public/docs/components/kanban-board.md +300 -0
  36. package/public/docs/components/kbd.md +72 -0
  37. package/public/docs/components/link.md +98 -0
  38. package/public/docs/components/list-item.md +241 -0
  39. package/public/docs/components/loading-animation.md +72 -0
  40. package/public/docs/components/multi-select.md +202 -0
  41. package/public/docs/components/navbar.md +144 -0
  42. package/public/docs/components/newsletter-form.md +112 -0
  43. package/public/docs/components/page-header.md +151 -0
  44. package/public/docs/components/pagination.md +212 -0
  45. package/public/docs/components/product-card.md +172 -0
  46. package/public/docs/components/progress.md +101 -0
  47. package/public/docs/components/prompt-input.md +151 -0
  48. package/public/docs/components/quantity-stepper.md +140 -0
  49. package/public/docs/components/radio.md +177 -0
  50. package/public/docs/components/rating.md +106 -0
  51. package/public/docs/components/score-ring.md +95 -0
  52. package/public/docs/components/search.md +116 -0
  53. package/public/docs/components/section-heading.md +113 -0
  54. package/public/docs/components/segmented-control.md +126 -0
  55. package/public/docs/components/select.md +224 -0
  56. package/public/docs/components/sheet.md +145 -0
  57. package/public/docs/components/sidebar.md +193 -0
  58. package/public/docs/components/skeleton.md +135 -0
  59. package/public/docs/components/slider.md +114 -0
  60. package/public/docs/components/sparkline.md +128 -0
  61. package/public/docs/components/spinner.md +98 -0
  62. package/public/docs/components/stat.md +106 -0
  63. package/public/docs/components/status-tracker.md +145 -0
  64. package/public/docs/components/stepper.md +147 -0
  65. package/public/docs/components/switch.md +153 -0
  66. package/public/docs/components/tabs.md +180 -0
  67. package/public/docs/components/tag.md +105 -0
  68. package/public/docs/components/textarea.md +148 -0
  69. package/public/docs/components/timeline.md +152 -0
  70. package/public/docs/components/toast.md +194 -0
  71. package/public/docs/components/tool-call.md +107 -0
  72. package/public/docs/components/tooltip.md +108 -0
  73. package/public/manifest/components.json +198 -15
  74. package/public/manifest/tokens.json +75 -5
  75. package/scripts/division-ds.mjs +78 -42
  76. package/scripts/lib/agent-docs.mjs +80 -6
  77. package/scripts/lib/ai-copy.mjs +120 -0
  78. package/scripts/lib/button-spacing.mjs +5 -5
  79. package/scripts/lib/check-text.mjs +57 -0
  80. package/scripts/lib/contrast.mjs +111 -0
  81. package/scripts/lib/host-inline.mjs +59 -0
  82. package/scripts/lib/mcp-server.mjs +116 -0
  83. package/scripts/lib/mcp-tools.mjs +266 -0
  84. package/scripts/lib/props-validator.mjs +3 -2
  85. package/scripts/lib/rules-installer.mjs +42 -4
  86. package/scripts/lib/token-discipline.mjs +10 -3
  87. package/skills/division-ds/SKILL.md +90 -0
  88. package/skills/division-ds/commands/add.md +25 -0
  89. package/skills/division-ds/commands/build.md +46 -0
  90. package/skills/division-ds/commands/fix.md +28 -0
  91. package/skills/division-ds/commands/review.md +25 -0
  92. package/skills/division-ds/topics/ai.md +8 -0
  93. package/skills/division-ds/topics/colour.md +11 -0
  94. package/skills/division-ds/topics/feedback.md +9 -0
  95. package/skills/division-ds/topics/forms.md +22 -0
  96. package/skills/division-ds/topics/layout.md +36 -0
  97. package/skills/division-ds/topics/motion.md +10 -0
  98. 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.