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,180 @@
1
+ # Tabs
2
+
3
+ > Sections of one view, with only one visible at a time. The active tab is filled with the signal colour, the same way every other selection in the system is.
4
+
5
+ - **Best for:** Switching between equally important sections of one view.
6
+ - **Not for:** Steps in a sequence: use Stepper. Moving between pages: use links (Navbar, Sidebar).
7
+ - Molecule. Source: `src/components/molecules/tabs/Tabs.tsx`
8
+ - Rendered docs page: `/components/tabs`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from 'src/components/molecules/tabs/Tabs'
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
+ | `value (Root)` | `string` | | The active tab, when the page controls it. |
23
+ | `variant` | `'fill' \| 'line'` | `'fill'` | How the active tab is shown (Root). 'line' underlines it, for a row inside a panel that already has lots of fills. |
24
+ | `defaultValue` | `string` | | The tab that starts active, when the component manages it (Root). |
25
+ | `onValueChange` | `(value: string) => void` | | Called when the active tab changes (Root). |
26
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Which arrow keys move between tabs (Root). Vertical also puts the list beside the panels. |
27
+ | `activationMode` | `'automatic' \| 'manual'` | `'automatic'` | 'manual' waits for Enter or Space before switching, for panels that are slow to draw (Root). |
28
+ | `value (Trigger)` | `string` | | This tab's own value. |
29
+ | `disabled` | `boolean` | `false` | Turns off one tab (Trigger). |
30
+ | `icon (Trigger)` | `ReactNode` | | A small symbol before the label, hidden from screen readers (aria-hidden). |
31
+ | `count (Trigger)` | `number` | | A number after the label, included in the name screen readers announce. |
32
+ | `className` | `string` | | Merged with the root classes. If two Tailwind utilities clash, the later one wins. Available on every part. |
33
+ | `ref` | `Ref<HTMLElement>` | | Passed through to the underlying page element. Available on every part. |
34
+
35
+ ## Examples
36
+
37
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
38
+
39
+ ### Default
40
+
41
+ ```tsx
42
+ <div className="max-w-sm w-full">
43
+ <Tabs defaultValue="replica">
44
+ <TabsList aria-label="Kit sections">
45
+ <TabsTrigger value="replica">Replica</TabsTrigger>
46
+ <TabsTrigger value="optics">Optics</TabsTrigger>
47
+ <TabsTrigger value="parts">Parts</TabsTrigger>
48
+ </TabsList>
49
+ <TabsContent value="replica">Electric carbine replica.</TabsContent>
50
+ <TabsContent value="optics">Tube sight and holo sight.</TabsContent>
51
+ <TabsContent value="parts">Magazines, a battery and a charger.</TabsContent>
52
+ </Tabs>
53
+ </div>
54
+ ```
55
+
56
+ ### With a disabled tab
57
+
58
+ ```tsx
59
+ <div className="max-w-sm w-full">
60
+ <Tabs defaultValue="replica">
61
+ <TabsList aria-label="Kit sections">
62
+ <TabsTrigger value="replica">Replica</TabsTrigger>
63
+ <TabsTrigger value="optics">Optics</TabsTrigger>
64
+ <TabsTrigger value="parts" disabled>
65
+ Parts
66
+ </TabsTrigger>
67
+ </TabsList>
68
+ <TabsContent value="replica">Electric carbine replica.</TabsContent>
69
+ <TabsContent value="optics">Tube sight and holo sight.</TabsContent>
70
+ <TabsContent value="parts">Magazines, a battery and a charger.</TabsContent>
71
+ </Tabs>
72
+ </div>
73
+ ```
74
+
75
+ ### Line, with icons and counts
76
+
77
+ ```tsx
78
+ <div className="max-w-md w-full">
79
+ <Tabs defaultValue="open" variant="line">
80
+ <TabsList aria-label="Orders">
81
+ <TabsTrigger value="open" icon={<Inbox />} count={12}>
82
+ Open
83
+ </TabsTrigger>
84
+ <TabsTrigger value="packed" icon={<Package />} count={4}>
85
+ Packed
86
+ </TabsTrigger>
87
+ <TabsTrigger value="archived" icon={<Archive />} count={0}>
88
+ Archived
89
+ </TabsTrigger>
90
+ </TabsList>
91
+ <TabsContent value="open">Twelve orders waiting to be packed.</TabsContent>
92
+ <TabsContent value="packed">Four orders packed, waiting for the courier.</TabsContent>
93
+ <TabsContent value="archived">No archived orders yet.</TabsContent>
94
+ </Tabs>
95
+ </div>
96
+ ```
97
+
98
+ ### Vertical, line
99
+
100
+ ```tsx
101
+ <div className="max-w-md w-full">
102
+ <Tabs defaultValue="profile" orientation="vertical" variant="line">
103
+ <TabsList aria-label="Settings">
104
+ <TabsTrigger value="profile" icon={<User />}>
105
+ Profile
106
+ </TabsTrigger>
107
+ <TabsTrigger value="security" icon={<Shield />}>
108
+ Security
109
+ </TabsTrigger>
110
+ </TabsList>
111
+ <TabsContent value="profile">Callsign, squad and home field.</TabsContent>
112
+ <TabsContent value="security">Password and signed-in devices.</TabsContent>
113
+ </Tabs>
114
+ </div>
115
+ ```
116
+
117
+ ### Controlled: the page keeps track of which tab is open
118
+
119
+ ```tsx
120
+ function ControlledExample() {
121
+ const [value, setValue] = useState('replica')
122
+
123
+ return (
124
+ <div className="max-w-sm w-full">
125
+ <div className="flex gap-2 mb-4">
126
+ <Button size="sm" variant="secondary" onClick={() => setValue('replica')}>
127
+ Show replica
128
+ </Button>
129
+ <Button size="sm" variant="secondary" onClick={() => setValue('optics')}>
130
+ Show optics
131
+ </Button>
132
+ </div>
133
+ <Tabs value={value} onValueChange={setValue}>
134
+ <TabsList aria-label="Kit sections">
135
+ <TabsTrigger value="replica">Replica</TabsTrigger>
136
+ <TabsTrigger value="optics">Optics</TabsTrigger>
137
+ </TabsList>
138
+ <TabsContent value="replica">Electric carbine replica.</TabsContent>
139
+ <TabsContent value="optics">Tube sight and holo sight.</TabsContent>
140
+ </Tabs>
141
+ </div>
142
+ )
143
+ }
144
+
145
+ <ControlledExample />
146
+ ```
147
+
148
+ ## Keyboard
149
+
150
+ | Keys | Result |
151
+ |---|---|
152
+ | `Tab` | Moves into the tab row, onto the active tab. Pressing Tab again moves into the panel. |
153
+ | `ArrowLeft` `ArrowRight` | Previous or next tab. It wraps round at the ends and skips turned-off tabs. |
154
+ | `ArrowUp` `ArrowDown` | The same, when orientation is vertical. |
155
+ | `Home` `End` | First or last tab. |
156
+
157
+ ## Do
158
+
159
+ - Keep tab labels to one or two words.
160
+ - Use tabs for equals: sections a person would move between freely.
161
+ - Give the tab row an aria-label that names the whole set.
162
+ - Use the line variant inside a card or settings panel, and the fill variant at the top of a view.
163
+ - Use a count for something a person would act on, like open orders or unread reports.
164
+
165
+ ## Don't
166
+
167
+ - Don't use tabs for steps in a sequence. That's what Stepper is for.
168
+ - Don't put a second row of tabs inside a panel.
169
+ - Don't mix fill and line tabs on one page.
170
+ - Don't give every tab an icon just because one has one. Icons go on all of them or none.
171
+ - Don't use tabs to move between pages. A tab changes what's shown, not where you are.
172
+
173
+ ## Anatomy
174
+
175
+ - **Tabs**: The root. Holds which tab is active, whether the page controls that or the component does.
176
+ - **TabsList**: The tablist, the row of tabs. It needs an aria-label (a name read out by screen readers). On a narrow screen it scrolls sideways inside itself.
177
+ - **TabsTrigger**: A single tab. Fill variant: the active tab is the signal fill with ink-coloured text, and hovering adds a 12% signal tint. Line variant: the active tab is primary text over a 2px (pixel) signal bar, which moves to the inline end (the side text runs towards) when the tabs are vertical.
178
+ - **Icon**: Optional, before the label. aria-hidden, so screen readers skip it.
179
+ - **Count**: Optional, after the label. You see it as a number, and screen readers read it as part of the name: Open (12).
180
+ - **TabsContent**: A tabpanel, the area a tab shows. It's labelled by its tab and can take focus, so a keyboard user can reach panels that hold only text.
@@ -0,0 +1,105 @@
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
+ 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
+ | `children` | `string` | | The tag text. Required. |
23
+ | `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. |
24
+ | `dismissLabel` | `(text: string) => string` | `t => `Remove ${t}`` | Replaces the built dismiss name, for example with another verb or another language. |
25
+ | `className` | `string` | | Merged with the component's classes. Later Tailwind utilities (the small styling classes this system uses) win. |
26
+ | `ref` | `Ref<HTMLSpanElement>` | | Forwarded to the span element. |
27
+ | `...rest` | `Omit<HTMLAttributes<HTMLSpanElement>, 'children'>` | | Every native span attribute except children, spread onto the root span. |
28
+
29
+ ## Examples
30
+
31
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
32
+
33
+ ### Static tags
34
+
35
+ ```tsx
36
+ <Tag>Holo sight</Tag>
37
+ <Tag>Spare magazine</Tag>
38
+ <Tag>Speed loader</Tag>
39
+ ```
40
+
41
+ ### Removable
42
+
43
+ ```tsx
44
+ const FIELD_KIT = ['Holo sight', 'Spare magazine', 'Speed loader', 'Field goggles']
45
+
46
+ function RemovableExample() {
47
+ const [tags, setTags] = useState(FIELD_KIT)
48
+
49
+ return (
50
+ <div className="flex flex-col gap-3">
51
+ <div className="flex flex-wrap gap-2">
52
+ {tags.length === 0 ? (
53
+ <span className="text-sm text-[var(--color-text-secondary)]">No filters applied.</span>
54
+ ) : (
55
+ tags.map(tag => (
56
+ <Tag key={tag} onDismiss={() => setTags(current => current.filter(t => t !== tag))}>
57
+ {tag}
58
+ </Tag>
59
+ ))
60
+ )}
61
+ </div>
62
+ {/* The Button atom, not a bare <button>: a hand-rolled one here had no system focus ring
63
+ at all, only the UA default. `ghost` is the existing variant for a text-weight action. */}
64
+ <Button variant="ghost" size="sm" wrapperClassName="self-start" onClick={() => setTags(FIELD_KIT)}>
65
+ Reset
66
+ </Button>
67
+ </div>
68
+ )
69
+ }
70
+
71
+ <RemovableExample />
72
+ ```
73
+
74
+ ### Long text
75
+
76
+ ```tsx
77
+ <Tag onDismiss={() => {}}>Stick battery 11.1 V, 1300 mAh, lithium polymer</Tag>
78
+ ```
79
+
80
+ ## Keyboard
81
+
82
+ | Keys | Result |
83
+ |---|---|
84
+ | `Tab` | Moves focus to the dismiss button, if there is one. The chip itself can't take focus. |
85
+ | `Enter` | Dismisses the tag. |
86
+ | `Space` | Dismisses the tag. |
87
+
88
+ ## Do
89
+
90
+ - Keep the text short enough to read as a chip.
91
+ - Move focus somewhere sensible after a dismissal: to the next tag, or to the container if the row is now empty.
92
+ - Override dismissLabel when 'Remove' is the wrong verb, for example 'Clear filter'.
93
+
94
+ ## Don't
95
+
96
+ - Don't make the whole chip clickable as well as its dismiss button. Two targets in one chip leaves people guessing.
97
+ - Don't shrink the dismiss button below 24×24.
98
+ - Don't use a Tag for status. That's a Badge.
99
+ - 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.
100
+
101
+ ## Anatomy
102
+
103
+ - **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.
104
+ - **Text**: Required, and a plain string, because the dismiss name is built from it.
105
+ - **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,148 @@
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
+ 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
+ - 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.
21
+
22
+ ## Props
23
+
24
+ | Prop | Type | Default | Description |
25
+ |---|---|---|---|
26
+ | `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. |
27
+ | `rows` | `number` | `4` | The browser's own rows attribute. Size it to the answer you expect. |
28
+ | `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. |
29
+ | `className` | `string` | | Merged with the control's classes. Later Tailwind utilities (the small styling classes this system uses) win. |
30
+ | `ref` | `Ref<HTMLTextAreaElement>` | | Forwarded to the textarea element. |
31
+ | `...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. |
32
+
33
+ ## Types and helpers
34
+
35
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
36
+
37
+ ```ts
38
+ export const textareaVariants = cva( …
39
+ ```
40
+
41
+ ## Examples
42
+
43
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
44
+
45
+ ### Default
46
+
47
+ ```tsx
48
+ <label className="flex flex-col gap-1 text-sm w-full max-w-xs">
49
+ Field notes
50
+ <Textarea placeholder="What did you observe on site?" />
51
+ </label>
52
+ ```
53
+
54
+ ### Rows
55
+
56
+ ```tsx
57
+ <label className="flex flex-col gap-1 text-sm w-full max-w-xs">
58
+ Short (2 rows)
59
+ <Textarea rows={2} placeholder="One-line summary, wraps if needed" />
60
+ </label>
61
+ <label className="flex flex-col gap-1 text-sm w-full max-w-xs">
62
+ Tall (8 rows)
63
+ <Textarea rows={8} placeholder="Full incident report" />
64
+ </label>
65
+ ```
66
+
67
+ ### States
68
+
69
+ ```tsx
70
+ <label className="flex flex-col gap-1 text-sm w-full max-w-xs">
71
+ Invalid
72
+ <Textarea invalid defaultValue="Site coordinates unclear" />
73
+ </label>
74
+ <label className="flex flex-col gap-1 text-sm w-full max-w-xs">
75
+ Disabled
76
+ <Textarea disabled defaultValue="Locked until review closes" />
77
+ </label>
78
+ {/* read-only is a different state from disabled and looks different — the text stays
79
+ selectable and the control stays in the tab order, so `read-only:text-[...]` dims the
80
+ text without the disabled fill. It was styled but never demonstrated. */}
81
+ <label className="flex flex-col gap-1 text-sm w-full max-w-xs">
82
+ Read-only
83
+ <Textarea readOnly defaultValue="Submitted 14:02, no further edits" />
84
+ </label>
85
+ ```
86
+
87
+ ### Filled by AI, with a revert
88
+
89
+ ```tsx
90
+ const AI_NOTES = 'Ships with two midcaps and a sling. Ask for the 1.2 J spring if you play indoors.'
91
+
92
+ <div className="flex flex-col gap-2 w-full max-w-sm">
93
+ <label htmlFor="ai-notes" className="text-sm">Order notes</label>
94
+ <AiLabel explanation="Drafted from the replica's listing. Check the details before you send it." />
95
+ <Textarea id="ai-notes" aiValue={AI_NOTES} defaultValue={AI_NOTES} />
96
+ </div>
97
+ ```
98
+
99
+ ### With a counter
100
+
101
+ ```tsx
102
+ function CounterExample() {
103
+ const max = 140
104
+ const [value, setValue] = useState('')
105
+ return (
106
+ <label className="flex flex-col gap-1 text-sm w-full max-w-xs">
107
+ Field day summary
108
+ <Textarea
109
+ value={value}
110
+ onChange={e => setValue(e.target.value)}
111
+ maxLength={max}
112
+ aria-describedby="textarea-docs-counter"
113
+ />
114
+ <span id="textarea-docs-counter" className="text-xs text-[var(--color-text-secondary)]">
115
+ {value.length} / {max}
116
+ </span>
117
+ </label>
118
+ )
119
+ }
120
+
121
+ <CounterExample />
122
+ ```
123
+
124
+ ## Keyboard
125
+
126
+ | Keys | Result |
127
+ |---|---|
128
+ | `Tab` | Moves focus to it. |
129
+ | `Enter` | Starts a new line. It doesn't submit the form. |
130
+
131
+ ## Do
132
+
133
+ - Size rows to the answer you expect.
134
+ - Leave vertical resize on. Taking it away doesn't help anyone.
135
+ - Show a character counter as text the field points to with aria-describedby, not as a colour change.
136
+
137
+ ## Don't
138
+
139
+ - Don't allow horizontal resize. It breaks the layout around it.
140
+ - 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.
141
+ - Don't reach for inputVariants here. The shared heights are wrong for a box with several lines, and the small duplication is on purpose.
142
+ - Don't trap Enter to submit unless there's another way to add a line.
143
+
144
+ ## Anatomy
145
+
146
+ - **Textarea**: Four rows, never shorter than 88px (pixels), vertical resize only.
147
+ - **Border**: The same three states as Input.
148
+ - **Focus ring**: The outline that shows keyboard focus. It sits on the control itself.
@@ -0,0 +1,152 @@
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
+ 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
+ | `label (Timeline)` | `string` | | Required. Names the list. |
23
+ | `className (Timeline)` | `string` | | Added to the root ol. |
24
+ | `title (TimelineGroup)` | `string` | | Required. A day or a period, for example "Today". |
25
+ | `level (TimelineGroup)` | `2 \| 3 \| 4` | `3` | The heading level of the group title (2 means an h2, and so on). |
26
+ | `icon (TimelineItem)` | `ReactNode` | | Required. A lucide icon, there for decoration only. |
27
+ | `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. |
28
+ | `title (TimelineItem)` | `ReactNode` | | Required. What happened, in one line. |
29
+ | `meta (TimelineItem)` | `ReactNode` | | Beside the title: who did it, or a Badge with the outcome. |
30
+ | `time (TimelineItem)` | `string` | | Required. An ISO datetime (a date and time written in the international standard format), used for <time dateTime>. |
31
+ | `timeLabel (TimelineItem)` | `string` | | Required. What people see, for example "09:40" or "Mon 12 Oct". |
32
+ | `details (TimelineItem)` | `ReactNode` | | Extra information, folded away behind a "Show details" button. |
33
+
34
+ ## Types and helpers
35
+
36
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
37
+
38
+ ```ts
39
+ export type TimelineTone = 'neutral' | 'signal' | 'ok' | 'warning' | 'hostile' | 'info'
40
+ ```
41
+
42
+ ## Examples
43
+
44
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
45
+
46
+ ### Grouped by day
47
+
48
+ ```tsx
49
+ <div className="w-full max-w-xl">
50
+ <Timeline label="Activity on Depot 4">
51
+ <TimelineGroup title="Today" level={4}>
52
+ <TimelineItem
53
+ icon={<Truck />}
54
+ tone="info"
55
+ title="Shipment SHP-204 left for Sibiu"
56
+ meta="Dispatch"
57
+ time="2026-10-15T09:40:00+03:00"
58
+ timeLabel="09:40"
59
+ >
60
+ Nine items, 22.1 kg, with the regular courier.
61
+ </TimelineItem>
62
+ <TimelineItem
63
+ icon={<Phone />}
64
+ title="Call with Ilse Varga"
65
+ meta={
66
+ <>
67
+ <span>Oskar Behn</span>
68
+ <Badge tone="ok">Resolved</Badge>
69
+ </>
70
+ }
71
+ time="2026-10-15T08:15:00+03:00"
72
+ timeLabel="08:15"
73
+ details="Agreed to move the Friday pickup to Thursday morning. Bay 2 stays free until noon."
74
+ >
75
+ The late pallet from Arad is booked for tomorrow.
76
+ </TimelineItem>
77
+ </TimelineGroup>
78
+ <TimelineGroup title="Yesterday" level={4}>
79
+ <TimelineItem
80
+ icon={<TriangleAlert />}
81
+ tone="warning"
82
+ title="Floor scale out of calibration"
83
+ time="2026-10-14T16:05:00+03:00"
84
+ timeLabel="16:05"
85
+ >
86
+ Readings 0.4 kg high. Work order WO-32 opened.
87
+ </TimelineItem>
88
+ <TimelineItem
89
+ icon={<PackageCheck />}
90
+ tone="ok"
91
+ title="Inbound delivery checked in"
92
+ time="2026-10-14T10:30:00+03:00"
93
+ timeLabel="10:30"
94
+ />
95
+ </TimelineGroup>
96
+ </Timeline>
97
+ </div>
98
+ ```
99
+
100
+ ### Without groups
101
+
102
+ ```tsx
103
+ <div className="w-full max-w-xl">
104
+ <Timeline label="Work order WO-29">
105
+ <TimelineItem icon={<Wrench />} title="Opened" time="2026-10-09T08:00:00+03:00" timeLabel="Fri 9 Oct" />
106
+ <TimelineItem
107
+ icon={<Wrench />}
108
+ tone="signal"
109
+ title="Assigned to Mara Lind"
110
+ time="2026-10-12T11:20:00+03:00"
111
+ timeLabel="Mon 12 Oct"
112
+ />
113
+ <TimelineItem
114
+ icon={<CircleCheck />}
115
+ tone="ok"
116
+ title="Closed: pallet jack serviced"
117
+ time="2026-10-13T15:45:00+03:00"
118
+ timeLabel="Tue 13 Oct"
119
+ />
120
+ </Timeline>
121
+ </div>
122
+ ```
123
+
124
+ ## Keyboard
125
+
126
+ | Keys | Result |
127
+ |---|---|
128
+ | `Tab` | Moves to each "Show details" button, and to any link inside an event. |
129
+ | `Enter` `Space` | On "Show details": shows or hides the details. |
130
+
131
+ ## Do
132
+
133
+ - Label the timeline for what it records, for example "Activity on Depot 4".
134
+ - Group by day once there are more than a handful of events.
135
+ - Keep the title to one line and put the rest in the body or details.
136
+ - Pick a tone for what the event means (ok for done, hostile for failed), and say it in the title too.
137
+
138
+ ## Don't
139
+
140
+ - Don't use a Timeline for a flow someone is going through. That's what Stepper is for.
141
+ - Don't put the only copy of something important in details.
142
+ - Don't colour every marker. Neutral is the default for a reason.
143
+
144
+ ## Anatomy
145
+
146
+ - **List**: An ol (numbered list element) named by label. It holds items, groups, or both.
147
+ - **Group**: A list item with a heading in a monospaced font, a thin line running to the edge, and its own ol.
148
+ - **Marker**: A 28px (pixel) square holding the icon, outlined and coloured by tone.
149
+ - **Rail**: A thin line from each marker to the next. There's none after the last event in a list.
150
+ - **Title line**: The title, then meta, then the time on the right inside a <time dateTime> element, which gives machines the exact date.
151
+ - **Body**: Optional text under the title.
152
+ - **Details**: Optional, hidden behind a "Show details" button. The button has aria-expanded, which tells screen readers whether the details are open.