division-ds 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/AGENTS.md +16 -2
  2. package/README.md +11 -0
  3. package/division-ds-0.2.0.tgz +0 -0
  4. package/package.json +2 -2
  5. package/public/docs/components/accordion.md +205 -0
  6. package/public/docs/components/ai-label.md +88 -0
  7. package/public/docs/components/alert-dialog.md +290 -0
  8. package/public/docs/components/alert.md +173 -0
  9. package/public/docs/components/approval-request.md +146 -0
  10. package/public/docs/components/auth-form.md +130 -0
  11. package/public/docs/components/avatar-group.md +150 -0
  12. package/public/docs/components/avatar.md +177 -0
  13. package/public/docs/components/badge.md +176 -0
  14. package/public/docs/components/breadcrumbs.md +110 -0
  15. package/public/docs/components/button.md +202 -0
  16. package/public/docs/components/card.md +255 -0
  17. package/public/docs/components/carousel.md +128 -0
  18. package/public/docs/components/cart.md +170 -0
  19. package/public/docs/components/chart-panel.md +356 -0
  20. package/public/docs/components/chat-message.md +154 -0
  21. package/public/docs/components/chat-thread.md +242 -0
  22. package/public/docs/components/checkbox.md +147 -0
  23. package/public/docs/components/checkout-form.md +126 -0
  24. package/public/docs/components/command-palette.md +145 -0
  25. package/public/docs/components/data-table.md +399 -0
  26. package/public/docs/components/description-list.md +101 -0
  27. package/public/docs/components/dialog.md +195 -0
  28. package/public/docs/components/divider.md +95 -0
  29. package/public/docs/components/dropdown-menu.md +220 -0
  30. package/public/docs/components/empty-state.md +143 -0
  31. package/public/docs/components/footer.md +395 -0
  32. package/public/docs/components/form-field.md +213 -0
  33. package/public/docs/components/icon-button.md +125 -0
  34. package/public/docs/components/input.md +198 -0
  35. package/public/docs/components/kanban-board.md +298 -0
  36. package/public/docs/components/kbd.md +70 -0
  37. package/public/docs/components/link.md +96 -0
  38. package/public/docs/components/list-item.md +238 -0
  39. package/public/docs/components/loading-animation.md +70 -0
  40. package/public/docs/components/multi-select.md +200 -0
  41. package/public/docs/components/navbar.md +142 -0
  42. package/public/docs/components/newsletter-form.md +110 -0
  43. package/public/docs/components/page-header.md +149 -0
  44. package/public/docs/components/pagination.md +210 -0
  45. package/public/docs/components/product-card.md +169 -0
  46. package/public/docs/components/progress.md +99 -0
  47. package/public/docs/components/prompt-input.md +149 -0
  48. package/public/docs/components/quantity-stepper.md +138 -0
  49. package/public/docs/components/radio.md +140 -0
  50. package/public/docs/components/rating.md +104 -0
  51. package/public/docs/components/score-ring.md +93 -0
  52. package/public/docs/components/search.md +114 -0
  53. package/public/docs/components/section-heading.md +111 -0
  54. package/public/docs/components/segmented-control.md +124 -0
  55. package/public/docs/components/select.md +222 -0
  56. package/public/docs/components/sheet.md +143 -0
  57. package/public/docs/components/sidebar.md +191 -0
  58. package/public/docs/components/skeleton.md +133 -0
  59. package/public/docs/components/slider.md +112 -0
  60. package/public/docs/components/sparkline.md +126 -0
  61. package/public/docs/components/spinner.md +96 -0
  62. package/public/docs/components/stat.md +104 -0
  63. package/public/docs/components/status-tracker.md +143 -0
  64. package/public/docs/components/stepper.md +145 -0
  65. package/public/docs/components/switch.md +151 -0
  66. package/public/docs/components/tabs.md +178 -0
  67. package/public/docs/components/tag.md +103 -0
  68. package/public/docs/components/textarea.md +146 -0
  69. package/public/docs/components/timeline.md +150 -0
  70. package/public/docs/components/toast.md +192 -0
  71. package/public/docs/components/tool-call.md +105 -0
  72. package/public/docs/components/tooltip.md +106 -0
  73. package/public/manifest/components.json +182 -6
  74. package/public/manifest/tokens.json +39 -0
  75. package/scripts/division-ds.mjs +74 -38
  76. package/scripts/lib/agent-docs.mjs +53 -1
  77. package/scripts/lib/ai-copy.mjs +106 -0
  78. package/scripts/lib/button-spacing.mjs +5 -5
  79. package/scripts/lib/check-text.mjs +44 -0
  80. package/scripts/lib/contrast.mjs +111 -0
  81. package/scripts/lib/host-inline.mjs +59 -0
  82. package/scripts/lib/mcp-server.mjs +115 -0
  83. package/scripts/lib/mcp-tools.mjs +241 -0
  84. package/scripts/lib/props-validator.mjs +1 -1
  85. package/scripts/lib/rules-installer.mjs +42 -4
  86. package/skills/division-ds/SKILL.md +90 -0
  87. package/skills/division-ds/commands/add.md +25 -0
  88. package/skills/division-ds/commands/build.md +46 -0
  89. package/skills/division-ds/commands/fix.md +28 -0
  90. package/skills/division-ds/commands/review.md +25 -0
  91. package/skills/division-ds/topics/ai.md +7 -0
  92. package/skills/division-ds/topics/colour.md +11 -0
  93. package/skills/division-ds/topics/feedback.md +9 -0
  94. package/skills/division-ds/topics/forms.md +19 -0
  95. package/skills/division-ds/topics/layout.md +34 -0
  96. package/skills/division-ds/topics/motion.md +10 -0
  97. package/skills/division-ds/topics/overlays.md +9 -0
@@ -0,0 +1,103 @@
1
+ # Tag
2
+
3
+ > A removable chip for filters and selections. Its dismiss button is named after the thing it removes, so a row of them doesn't sound like six identical 'Remove' buttons to a screen reader.
4
+
5
+ - **Best for:** A removable chip, such as an active filter or a selected value.
6
+ - **Not for:** A status. Use Badge.
7
+ - Atom. Source: `src/components/atoms/tag/Tag.tsx`
8
+ - Rendered docs page: `/components/tag`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Tag } from 'src/components/atoms/tag/Tag'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `children` | `string` | | The tag text. Required. |
21
+ | `onDismiss` | `() => void` | | Adds the dismiss button. It runs once the tag has faded out, not on the click. Remove the tag from your list here, and the tags after it slide into its place (M7, a motion token). With reduced motion turned on, it runs at once. |
22
+ | `dismissLabel` | `(text: string) => string` | `t => `Remove ${t}`` | Replaces the built dismiss name, for example with another verb or another language. |
23
+ | `className` | `string` | | Merged with the component's classes. Later Tailwind utilities (the small styling classes this system uses) win. |
24
+ | `ref` | `Ref<HTMLSpanElement>` | | Forwarded to the span element. |
25
+ | `...rest` | `Omit<HTMLAttributes<HTMLSpanElement>, 'children'>` | | Every native span attribute except children, spread onto the root span. |
26
+
27
+ ## Examples
28
+
29
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
30
+
31
+ ### Static tags
32
+
33
+ ```tsx
34
+ <Tag>Holo sight</Tag>
35
+ <Tag>Spare magazine</Tag>
36
+ <Tag>Speed loader</Tag>
37
+ ```
38
+
39
+ ### Removable
40
+
41
+ ```tsx
42
+ const FIELD_KIT = ['Holo sight', 'Spare magazine', 'Speed loader', 'Field goggles']
43
+
44
+ function RemovableExample() {
45
+ const [tags, setTags] = useState(FIELD_KIT)
46
+
47
+ return (
48
+ <div className="flex flex-col gap-3">
49
+ <div className="flex flex-wrap gap-2">
50
+ {tags.length === 0 ? (
51
+ <span className="text-sm text-[var(--color-text-secondary)]">No filters applied.</span>
52
+ ) : (
53
+ tags.map(tag => (
54
+ <Tag key={tag} onDismiss={() => setTags(current => current.filter(t => t !== tag))}>
55
+ {tag}
56
+ </Tag>
57
+ ))
58
+ )}
59
+ </div>
60
+ {/* The Button atom, not a bare <button>: a hand-rolled one here had no system focus ring
61
+ at all, only the UA default. `ghost` is the existing variant for a text-weight action. */}
62
+ <Button variant="ghost" size="sm" wrapperClassName="self-start" onClick={() => setTags(FIELD_KIT)}>
63
+ Reset
64
+ </Button>
65
+ </div>
66
+ )
67
+ }
68
+
69
+ <RemovableExample />
70
+ ```
71
+
72
+ ### Long text
73
+
74
+ ```tsx
75
+ <Tag onDismiss={() => {}}>Stick battery 11.1 V, 1300 mAh, lithium polymer</Tag>
76
+ ```
77
+
78
+ ## Keyboard
79
+
80
+ | Keys | Result |
81
+ |---|---|
82
+ | `Tab` | Moves focus to the dismiss button, if there is one. The chip itself can't take focus. |
83
+ | `Enter` | Dismisses the tag. |
84
+ | `Space` | Dismisses the tag. |
85
+
86
+ ## Do
87
+
88
+ - Keep the text short enough to read as a chip.
89
+ - Move focus somewhere sensible after a dismissal: to the next tag, or to the container if the row is now empty.
90
+ - Override dismissLabel when 'Remove' is the wrong verb, for example 'Clear filter'.
91
+
92
+ ## Don't
93
+
94
+ - Don't make the whole chip clickable as well as its dismiss button. Two targets in one chip leaves people guessing.
95
+ - Don't shrink the dismiss button below 24×24.
96
+ - Don't use a Tag for status. That's a Badge.
97
+ - Don't move the bevel from the backing layer onto the chip. A Tag can hold a dismiss button that takes focus, and clip-path (the CSS, or Cascading Style Sheets, property that cuts the corner) on the chip would clip that button's focus ring, the outline that shows keyboard focus, off the corner it sits in.
98
+
99
+ ## Anatomy
100
+
101
+ - **Chip**: 28px (pixels) tall. The cut corner, the border.hairline-strong edge and the sunken fill sit on a backing layer, not on the chip itself. That layer is aria-hidden, a setting that hides it from screen readers.
102
+ - **Text**: Required, and a plain string, because the dismiss name is built from it.
103
+ - **Dismiss button**: A ghost IconButton at 24×24. That's the minimum target size in WCAG 2.2 (the Web Content Accessibility Guidelines, the public rulebook for making websites usable by people with disabilities). It only appears when onDismiss is given.
@@ -0,0 +1,146 @@
1
+ # Textarea
2
+
3
+ > A text box for several lines. It has the same chrome (border, fill and states) as Input, resizes vertically and shows four rows by default. Like Input, it doesn't label itself.
4
+
5
+ - **Best for:** Free text over several lines, inside a FormField.
6
+ - **Not for:** A single line. Use Input.
7
+ - Atom. Source: `src/components/atoms/textarea/Textarea.tsx`
8
+ - Rendered docs page: `/components/textarea`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Textarea, textareaVariants } from 'src/components/atoms/textarea/Textarea'
14
+ ```
15
+
16
+ ## Requires
17
+
18
+ - A <FormField> around it. On its own the control has no label. FormField supplies the label, the help text and the error, and links up their ids.
19
+
20
+ ## Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |---|---|---|---|
24
+ | `invalid` | `boolean` | `false` | Sets aria-invalid and the invalid border. aria-invalid is an ARIA (Accessible Rich Internet Applications) attribute that tells screen readers the value has a problem. It doesn't render error text. |
25
+ | `rows` | `number` | `4` | The browser's own rows attribute. Size it to the answer you expect. |
26
+ | `aiValue` | `string` | | The value an AI proposed, with the same contract as Input. The mark and the revert button sit in the top right corner. While the field holds exactly this value it shows the AI-filled state: an ai.border edge, a faint ai.wash tint and a sparkle mark that describes the field as Filled by AI. Once the person changes it, the field looks normal again and a Revert to suggestion button puts the value back through onChange (no button on a disabled or read-only field). An invalid field keeps its invalid border. |
27
+ | `className` | `string` | | Merged with the control's classes. Later Tailwind utilities (the small styling classes this system uses) win. |
28
+ | `ref` | `Ref<HTMLTextAreaElement>` | | Forwarded to the textarea element. |
29
+ | `...rest` | `Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'aria-invalid'>` | | Every native textarea prop except aria-invalid, spread onto the element. aria-invalid is removed from the type instead of being quietly overridden at runtime. invalid is the only source of both the attribute and the border, so letting a caller set one directly would let what people see and what screen readers hear drift apart. |
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 textareaVariants = cva( …
37
+ ```
38
+
39
+ ## Examples
40
+
41
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
42
+
43
+ ### Default
44
+
45
+ ```tsx
46
+ <label className="flex flex-col gap-1 text-sm w-full max-w-xs">
47
+ Field notes
48
+ <Textarea placeholder="What did you observe on site?" />
49
+ </label>
50
+ ```
51
+
52
+ ### Rows
53
+
54
+ ```tsx
55
+ <label className="flex flex-col gap-1 text-sm w-full max-w-xs">
56
+ Short (2 rows)
57
+ <Textarea rows={2} placeholder="One-line summary, wraps if needed" />
58
+ </label>
59
+ <label className="flex flex-col gap-1 text-sm w-full max-w-xs">
60
+ Tall (8 rows)
61
+ <Textarea rows={8} placeholder="Full incident report" />
62
+ </label>
63
+ ```
64
+
65
+ ### States
66
+
67
+ ```tsx
68
+ <label className="flex flex-col gap-1 text-sm w-full max-w-xs">
69
+ Invalid
70
+ <Textarea invalid defaultValue="Site coordinates unclear" />
71
+ </label>
72
+ <label className="flex flex-col gap-1 text-sm w-full max-w-xs">
73
+ Disabled
74
+ <Textarea disabled defaultValue="Locked until review closes" />
75
+ </label>
76
+ {/* read-only is a different state from disabled and looks different — the text stays
77
+ selectable and the control stays in the tab order, so `read-only:text-[...]` dims the
78
+ text without the disabled fill. It was styled but never demonstrated. */}
79
+ <label className="flex flex-col gap-1 text-sm w-full max-w-xs">
80
+ Read-only
81
+ <Textarea readOnly defaultValue="Submitted 14:02, no further edits" />
82
+ </label>
83
+ ```
84
+
85
+ ### Filled by AI, with a revert
86
+
87
+ ```tsx
88
+ const AI_NOTES = 'Ships with two midcaps and a sling. Ask for the 1.2 J spring if you play indoors.'
89
+
90
+ <div className="flex flex-col gap-2 w-full max-w-sm">
91
+ <label htmlFor="ai-notes" className="text-sm">Order notes</label>
92
+ <AiLabel explanation="Drafted from the replica's listing. Check the details before you send it." />
93
+ <Textarea id="ai-notes" aiValue={AI_NOTES} defaultValue={AI_NOTES} />
94
+ </div>
95
+ ```
96
+
97
+ ### With a counter
98
+
99
+ ```tsx
100
+ function CounterExample() {
101
+ const max = 140
102
+ const [value, setValue] = useState('')
103
+ return (
104
+ <label className="flex flex-col gap-1 text-sm w-full max-w-xs">
105
+ Field day summary
106
+ <Textarea
107
+ value={value}
108
+ onChange={e => setValue(e.target.value)}
109
+ maxLength={max}
110
+ aria-describedby="textarea-docs-counter"
111
+ />
112
+ <span id="textarea-docs-counter" className="text-xs text-[var(--color-text-secondary)]">
113
+ {value.length} / {max}
114
+ </span>
115
+ </label>
116
+ )
117
+ }
118
+
119
+ <CounterExample />
120
+ ```
121
+
122
+ ## Keyboard
123
+
124
+ | Keys | Result |
125
+ |---|---|
126
+ | `Tab` | Moves focus to it. |
127
+ | `Enter` | Starts a new line. It doesn't submit the form. |
128
+
129
+ ## Do
130
+
131
+ - Size rows to the answer you expect.
132
+ - Leave vertical resize on. Taking it away doesn't help anyone.
133
+ - Show a character counter as text the field points to with aria-describedby, not as a colour change.
134
+
135
+ ## Don't
136
+
137
+ - Don't allow horizontal resize. It breaks the layout around it.
138
+ - Don't bevel it. The cut would land exactly where the resize grip is, and clip-path (the CSS, or Cascading Style Sheets, property that cuts the corner) would take the focus ring with it. Input's page states the general rule.
139
+ - Don't reach for inputVariants here. The shared heights are wrong for a box with several lines, and the small duplication is on purpose.
140
+ - Don't trap Enter to submit unless there's another way to add a line.
141
+
142
+ ## Anatomy
143
+
144
+ - **Textarea**: Four rows, never shorter than 88px (pixels), vertical resize only.
145
+ - **Border**: The same three states as Input.
146
+ - **Focus ring**: The outline that shows keyboard focus. It sits on the control itself.
@@ -0,0 +1,150 @@
1
+ # Timeline
2
+
3
+ > Events in time order down a vertical line. Each one has a square marker with an icon, a title line, a time and an optional body. You can group them by day, or not.
4
+
5
+ - **Best for:** A record of what happened to one thing, newest or oldest first: a shipment, a depot, an account.
6
+ - **Not for:** Steps of a flow someone is going through: use Stepper. Rows people sort and compare: use DataTable.
7
+ - Molecule. Source: `src/components/molecules/timeline/Timeline.tsx`
8
+ - Rendered docs page: `/components/timeline`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Timeline, TimelineGroup, TimelineItem } from 'src/components/molecules/timeline/Timeline'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `label (Timeline)` | `string` | | Required. Names the list. |
21
+ | `className (Timeline)` | `string` | | Added to the root ol. |
22
+ | `title (TimelineGroup)` | `string` | | Required. A day or a period, for example "Today". |
23
+ | `level (TimelineGroup)` | `2 \| 3 \| 4` | `3` | The heading level of the group title (2 means an h2, and so on). |
24
+ | `icon (TimelineItem)` | `ReactNode` | | Required. A lucide icon, there for decoration only. |
25
+ | `tone (TimelineItem)` | `'neutral' \| 'signal' \| 'ok' \| 'warning' \| 'hostile' \| 'info'` | `'neutral'` | Colours the marker, with the same meanings as Badge. The title still says it in words. |
26
+ | `title (TimelineItem)` | `ReactNode` | | Required. What happened, in one line. |
27
+ | `meta (TimelineItem)` | `ReactNode` | | Beside the title: who did it, or a Badge with the outcome. |
28
+ | `time (TimelineItem)` | `string` | | Required. An ISO datetime (a date and time written in the international standard format), used for <time dateTime>. |
29
+ | `timeLabel (TimelineItem)` | `string` | | Required. What people see, for example "09:40" or "Mon 12 Oct". |
30
+ | `details (TimelineItem)` | `ReactNode` | | Extra information, folded away behind a "Show details" button. |
31
+
32
+ ## Types and helpers
33
+
34
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
35
+
36
+ ```ts
37
+ export type TimelineTone = 'neutral' | 'signal' | 'ok' | 'warning' | 'hostile' | 'info'
38
+ ```
39
+
40
+ ## Examples
41
+
42
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
43
+
44
+ ### Grouped by day
45
+
46
+ ```tsx
47
+ <div className="w-full max-w-xl">
48
+ <Timeline label="Activity on Depot 4">
49
+ <TimelineGroup title="Today" level={4}>
50
+ <TimelineItem
51
+ icon={<Truck />}
52
+ tone="info"
53
+ title="Shipment SHP-204 left for Sibiu"
54
+ meta="Dispatch"
55
+ time="2026-10-15T09:40:00+03:00"
56
+ timeLabel="09:40"
57
+ >
58
+ Nine items, 22.1 kg, with the regular courier.
59
+ </TimelineItem>
60
+ <TimelineItem
61
+ icon={<Phone />}
62
+ title="Call with Ilse Varga"
63
+ meta={
64
+ <>
65
+ <span>Oskar Behn</span>
66
+ <Badge tone="ok">Resolved</Badge>
67
+ </>
68
+ }
69
+ time="2026-10-15T08:15:00+03:00"
70
+ timeLabel="08:15"
71
+ details="Agreed to move the Friday pickup to Thursday morning. Bay 2 stays free until noon."
72
+ >
73
+ The late pallet from Arad is booked for tomorrow.
74
+ </TimelineItem>
75
+ </TimelineGroup>
76
+ <TimelineGroup title="Yesterday" level={4}>
77
+ <TimelineItem
78
+ icon={<TriangleAlert />}
79
+ tone="warning"
80
+ title="Floor scale out of calibration"
81
+ time="2026-10-14T16:05:00+03:00"
82
+ timeLabel="16:05"
83
+ >
84
+ Readings 0.4 kg high. Work order WO-32 opened.
85
+ </TimelineItem>
86
+ <TimelineItem
87
+ icon={<PackageCheck />}
88
+ tone="ok"
89
+ title="Inbound delivery checked in"
90
+ time="2026-10-14T10:30:00+03:00"
91
+ timeLabel="10:30"
92
+ />
93
+ </TimelineGroup>
94
+ </Timeline>
95
+ </div>
96
+ ```
97
+
98
+ ### Without groups
99
+
100
+ ```tsx
101
+ <div className="w-full max-w-xl">
102
+ <Timeline label="Work order WO-29">
103
+ <TimelineItem icon={<Wrench />} title="Opened" time="2026-10-09T08:00:00+03:00" timeLabel="Fri 9 Oct" />
104
+ <TimelineItem
105
+ icon={<Wrench />}
106
+ tone="signal"
107
+ title="Assigned to Mara Lind"
108
+ time="2026-10-12T11:20:00+03:00"
109
+ timeLabel="Mon 12 Oct"
110
+ />
111
+ <TimelineItem
112
+ icon={<CircleCheck />}
113
+ tone="ok"
114
+ title="Closed: pallet jack serviced"
115
+ time="2026-10-13T15:45:00+03:00"
116
+ timeLabel="Tue 13 Oct"
117
+ />
118
+ </Timeline>
119
+ </div>
120
+ ```
121
+
122
+ ## Keyboard
123
+
124
+ | Keys | Result |
125
+ |---|---|
126
+ | `Tab` | Moves to each "Show details" button, and to any link inside an event. |
127
+ | `Enter` `Space` | On "Show details": shows or hides the details. |
128
+
129
+ ## Do
130
+
131
+ - Label the timeline for what it records, for example "Activity on Depot 4".
132
+ - Group by day once there are more than a handful of events.
133
+ - Keep the title to one line and put the rest in the body or details.
134
+ - Pick a tone for what the event means (ok for done, hostile for failed), and say it in the title too.
135
+
136
+ ## Don't
137
+
138
+ - Don't use a Timeline for a flow someone is going through. That's what Stepper is for.
139
+ - Don't put the only copy of something important in details.
140
+ - Don't colour every marker. Neutral is the default for a reason.
141
+
142
+ ## Anatomy
143
+
144
+ - **List**: An ol (numbered list element) named by label. It holds items, groups, or both.
145
+ - **Group**: A list item with a heading in a monospaced font, a thin line running to the edge, and its own ol.
146
+ - **Marker**: A 28px (pixel) square holding the icon, outlined and coloured by tone.
147
+ - **Rail**: A thin line from each marker to the next. There's none after the last event in a list.
148
+ - **Title line**: The title, then meta, then the time on the right inside a <time dateTime> element, which gives machines the exact date.
149
+ - **Body**: Optional text under the title.
150
+ - **Details**: Optional, hidden behind a "Show details" button. The button has aria-expanded, which tells screen readers whether the details are open.
@@ -0,0 +1,192 @@
1
+ # Toast
2
+
3
+ > A short message that pops up, stays a few seconds and goes away. Hostile toasts (errors) interrupt and stay longer. Everything else waits its turn.
4
+
5
+ - **Best for:** Confirming something the person just did, for a few seconds.
6
+ - **Not for:** The only copy of important information: use Alert. Also not for a change the page already shows.
7
+ - Molecule. Source: `src/components/molecules/toast/Toast.tsx`
8
+ - Rendered docs page: `/components/toast`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Toaster, useToast } from 'src/components/molecules/toast/Toast'
14
+ ```
15
+
16
+ ## Requires
17
+
18
+ - One <Toaster> near the root of the app, wrapped around everything that calls useToast(). AppShell and the demo layout each mount one. Outside it, useToast() throws an error.
19
+
20
+ ## Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |---|---|---|---|
24
+ | `title (ToastOptions)` | `string` | | Required. Screen readers hear the tone word before it. |
25
+ | `description (ToastOptions)` | `string` | | Optional body text. |
26
+ | `tone (ToastOptions)` | `'info' \| 'ok' \| 'warning' \| 'hostile'` | `'info'` | Which colour, icon and word the toast has. |
27
+ | `duration (ToastOptions)` | `number` | `5000` | How many milliseconds before it goes away on its own. 8000 for hostile. |
28
+ | `action (ToastOptions)` | `{ label: string; altText: string; onAction: () => void }` | | Optional. Radix (the library the toast is built on) requires altText: what to do somewhere else if someone can't reach the toast in time. |
29
+ | `replace (ToastOptions)` | `string` | | Optional. Closes any open toast raised with the same string as this one opens, so an action repeated quickly ("Alert acknowledged" three times) shows one toast instead of a stack. |
30
+ | `useToast()` | `{ toast: (options: ToastOptions) => string; dismiss: (id: string) => void }` | | Shows a toast (and returns its id) or closes one. Throws an error outside a Toaster. |
31
+
32
+ ## Types and helpers
33
+
34
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
35
+
36
+ ```ts
37
+ export type ToastOptions = {
38
+ title: string
39
+ description?: string
40
+ tone?: Tone
41
+ /** ms before it leaves on its own. Default 5000; 8000 for hostile. */
42
+ duration?: number
43
+ /** Radix requires altText: the way to do the same thing for someone who cannot reach the toast
44
+ * before it leaves. */
45
+ action?: { label: string; altText: string; onAction: () => void }
46
+ /** Any open toast raised with the same string closes as this one opens, so a repeated action
47
+ * ("Alert acknowledged" three times) shows one toast, not a stack over the page. */
48
+ replace?: string
49
+ }
50
+ ```
51
+
52
+ ```ts
53
+ export function useToast(): ToastApi
54
+ ```
55
+
56
+ ## Examples
57
+
58
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
59
+
60
+ ### Tones
61
+
62
+ ```tsx
63
+ function TonesExample() {
64
+ const { toast } = useToast()
65
+
66
+ return (
67
+ <div className="flex flex-wrap gap-3">
68
+ <Button variant="secondary" size="sm" onClick={() => toast({ title: 'Depot hours changed', tone: 'info' })}>
69
+ Info
70
+ </Button>
71
+ <Button variant="secondary" size="sm" onClick={() => toast({ title: 'Manifest saved', tone: 'ok' })}>
72
+ Ok
73
+ </Button>
74
+ <Button variant="secondary" size="sm" onClick={() => toast({ title: 'Depot closes early', tone: 'warning' })}>
75
+ Warning
76
+ </Button>
77
+ <Button variant="secondary" size="sm" onClick={() => toast({ title: 'Sync failed', tone: 'hostile' })}>
78
+ Hostile
79
+ </Button>
80
+ </div>
81
+ )
82
+ }
83
+
84
+ <TonesExample />
85
+ ```
86
+
87
+ ### With a description
88
+
89
+ ```tsx
90
+ function WithDescriptionExample() {
91
+ const { toast } = useToast()
92
+
93
+ return (
94
+ <Button
95
+ variant="secondary"
96
+ size="sm"
97
+ onClick={() => toast({ title: 'Manifest saved', description: 'Twelve items, filed to Brasov north.', tone: 'ok' })}
98
+ >
99
+ Save manifest
100
+ </Button>
101
+ )
102
+ }
103
+
104
+ <WithDescriptionExample />
105
+ ```
106
+
107
+ ### With an action
108
+
109
+ ```tsx
110
+ function WithActionExample() {
111
+ const { toast } = useToast()
112
+
113
+ return (
114
+ <Button
115
+ variant="secondary"
116
+ size="sm"
117
+ onClick={() =>
118
+ toast({
119
+ title: 'Item removed',
120
+ action: {
121
+ label: 'Undo',
122
+ altText: 'Undo from the manifest history',
123
+ onAction: () => toast({ title: 'Item restored', tone: 'ok' }),
124
+ },
125
+ })
126
+ }
127
+ >
128
+ Remove item
129
+ </Button>
130
+ )
131
+ }
132
+
133
+ <WithActionExample />
134
+ ```
135
+
136
+ ## Keyboard
137
+
138
+ | Keys | Result |
139
+ |---|---|
140
+ | `F8` | Moves focus into the notification region. |
141
+ | `Tab` | Moves through the toast's action and close button. A toast with focus pauses its timer. |
142
+ | `Escape` | Closes the toast that has focus. |
143
+
144
+ ## Do
145
+
146
+ - Use a toast to confirm something the person just did.
147
+ - Give every action an altText that says where else it can be done.
148
+ - Keep the title under about six words.
149
+ - After an action runs, focus goes back to where it was before it entered the toast. If the action changes the page (Undo puts a row back), move focus to what changed yourself.
150
+ - A toast follows the innermost open overlay (a dialog or panel on top of the page). If it's raised while one is open, or is already showing when one opens, it shows inside that overlay, and it moves back to the page corner when the overlay closes. AlertDialog is the exception: its toasts always show in the page corner, above the scrim (the dimmed layer behind the dialog).
151
+
152
+ ## Don't
153
+
154
+ - Don't put the only copy of important information in a toast. It goes away.
155
+ - Don't show a toast for a change the page already shows.
156
+ - Don't line up more than three at once.
157
+
158
+ ## Common mistakes
159
+
160
+ Wrong:
161
+
162
+ ```tsx
163
+ function SettingsForm() {
164
+ return (
165
+ <Toaster>
166
+ <Form />
167
+ </Toaster>
168
+ )
169
+ }
170
+ ```
171
+
172
+ Right:
173
+
174
+ ```tsx
175
+ // once, at the root
176
+ <Toaster>
177
+ <App />
178
+ </Toaster>
179
+
180
+ // anywhere below it
181
+ const { toast } = useToast()
182
+ toast({ title: 'Settings saved', tone: 'ok' })
183
+ ```
184
+
185
+ A screen that mounts its own Toaster gets a second viewport (the corner where toasts appear) and a second live region (the area screen readers listen to for announcements). Rely on the one at the root and call useToast().
186
+
187
+ ## Anatomy
188
+
189
+ - **Toaster**: The provider and the viewport, mounted once in the app shell.
190
+ - **Viewport**: The area where toasts appear: bottom right, 360px (pixels) wide, full width on a phone. Pressing F8 moves focus into it.
191
+ - **Toast**: Built like Alert, on the sunken surface: icon, tone word, title, description, action and a close button. Hovering over it pauses its timer too.
192
+ - **Action**: Optional. Needs altText, which says how to do the same thing somewhere else.