division-ds 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +16 -2
- package/README.md +11 -0
- package/division-ds-0.2.0.tgz +0 -0
- package/package.json +2 -2
- package/public/docs/components/accordion.md +205 -0
- package/public/docs/components/ai-label.md +88 -0
- package/public/docs/components/alert-dialog.md +290 -0
- package/public/docs/components/alert.md +173 -0
- package/public/docs/components/approval-request.md +146 -0
- package/public/docs/components/auth-form.md +130 -0
- package/public/docs/components/avatar-group.md +150 -0
- package/public/docs/components/avatar.md +177 -0
- package/public/docs/components/badge.md +176 -0
- package/public/docs/components/breadcrumbs.md +110 -0
- package/public/docs/components/button.md +202 -0
- package/public/docs/components/card.md +255 -0
- package/public/docs/components/carousel.md +128 -0
- package/public/docs/components/cart.md +170 -0
- package/public/docs/components/chart-panel.md +356 -0
- package/public/docs/components/chat-message.md +154 -0
- package/public/docs/components/chat-thread.md +242 -0
- package/public/docs/components/checkbox.md +147 -0
- package/public/docs/components/checkout-form.md +126 -0
- package/public/docs/components/command-palette.md +145 -0
- package/public/docs/components/data-table.md +399 -0
- package/public/docs/components/description-list.md +101 -0
- package/public/docs/components/dialog.md +195 -0
- package/public/docs/components/divider.md +95 -0
- package/public/docs/components/dropdown-menu.md +220 -0
- package/public/docs/components/empty-state.md +143 -0
- package/public/docs/components/footer.md +395 -0
- package/public/docs/components/form-field.md +213 -0
- package/public/docs/components/icon-button.md +125 -0
- package/public/docs/components/input.md +198 -0
- package/public/docs/components/kanban-board.md +298 -0
- package/public/docs/components/kbd.md +70 -0
- package/public/docs/components/link.md +96 -0
- package/public/docs/components/list-item.md +238 -0
- package/public/docs/components/loading-animation.md +70 -0
- package/public/docs/components/multi-select.md +200 -0
- package/public/docs/components/navbar.md +142 -0
- package/public/docs/components/newsletter-form.md +110 -0
- package/public/docs/components/page-header.md +149 -0
- package/public/docs/components/pagination.md +210 -0
- package/public/docs/components/product-card.md +169 -0
- package/public/docs/components/progress.md +99 -0
- package/public/docs/components/prompt-input.md +149 -0
- package/public/docs/components/quantity-stepper.md +138 -0
- package/public/docs/components/radio.md +140 -0
- package/public/docs/components/rating.md +104 -0
- package/public/docs/components/score-ring.md +93 -0
- package/public/docs/components/search.md +114 -0
- package/public/docs/components/section-heading.md +111 -0
- package/public/docs/components/segmented-control.md +124 -0
- package/public/docs/components/select.md +222 -0
- package/public/docs/components/sheet.md +143 -0
- package/public/docs/components/sidebar.md +191 -0
- package/public/docs/components/skeleton.md +133 -0
- package/public/docs/components/slider.md +112 -0
- package/public/docs/components/sparkline.md +126 -0
- package/public/docs/components/spinner.md +96 -0
- package/public/docs/components/stat.md +104 -0
- package/public/docs/components/status-tracker.md +143 -0
- package/public/docs/components/stepper.md +145 -0
- package/public/docs/components/switch.md +151 -0
- package/public/docs/components/tabs.md +178 -0
- package/public/docs/components/tag.md +103 -0
- package/public/docs/components/textarea.md +146 -0
- package/public/docs/components/timeline.md +150 -0
- package/public/docs/components/toast.md +192 -0
- package/public/docs/components/tool-call.md +105 -0
- package/public/docs/components/tooltip.md +106 -0
- package/public/manifest/components.json +182 -6
- package/public/manifest/tokens.json +39 -0
- package/scripts/division-ds.mjs +74 -38
- package/scripts/lib/agent-docs.mjs +53 -1
- package/scripts/lib/ai-copy.mjs +106 -0
- package/scripts/lib/button-spacing.mjs +5 -5
- package/scripts/lib/check-text.mjs +44 -0
- package/scripts/lib/contrast.mjs +111 -0
- package/scripts/lib/host-inline.mjs +59 -0
- package/scripts/lib/mcp-server.mjs +115 -0
- package/scripts/lib/mcp-tools.mjs +241 -0
- package/scripts/lib/props-validator.mjs +1 -1
- package/scripts/lib/rules-installer.mjs +42 -4
- package/skills/division-ds/SKILL.md +90 -0
- package/skills/division-ds/commands/add.md +25 -0
- package/skills/division-ds/commands/build.md +46 -0
- package/skills/division-ds/commands/fix.md +28 -0
- package/skills/division-ds/commands/review.md +25 -0
- package/skills/division-ds/topics/ai.md +7 -0
- package/skills/division-ds/topics/colour.md +11 -0
- package/skills/division-ds/topics/feedback.md +9 -0
- package/skills/division-ds/topics/forms.md +19 -0
- package/skills/division-ds/topics/layout.md +34 -0
- package/skills/division-ds/topics/motion.md +10 -0
- package/skills/division-ds/topics/overlays.md +9 -0
|
@@ -0,0 +1,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.
|