division-ds 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +23 -3
- package/README.md +11 -0
- package/division-ds-0.2.1.tgz +0 -0
- package/package.json +2 -2
- package/public/docs/components/accordion.md +207 -0
- package/public/docs/components/ai-label.md +90 -0
- package/public/docs/components/alert-dialog.md +292 -0
- package/public/docs/components/alert.md +175 -0
- package/public/docs/components/approval-request.md +148 -0
- package/public/docs/components/auth-form.md +132 -0
- package/public/docs/components/avatar-group.md +152 -0
- package/public/docs/components/avatar.md +179 -0
- package/public/docs/components/badge.md +178 -0
- package/public/docs/components/breadcrumbs.md +112 -0
- package/public/docs/components/button.md +204 -0
- package/public/docs/components/card.md +257 -0
- package/public/docs/components/carousel.md +130 -0
- package/public/docs/components/cart.md +172 -0
- package/public/docs/components/chart-panel.md +358 -0
- package/public/docs/components/chat-message.md +157 -0
- package/public/docs/components/chat-thread.md +244 -0
- package/public/docs/components/checkbox.md +149 -0
- package/public/docs/components/checkout-form.md +128 -0
- package/public/docs/components/command-palette.md +147 -0
- package/public/docs/components/data-table.md +401 -0
- package/public/docs/components/description-list.md +103 -0
- package/public/docs/components/dialog.md +197 -0
- package/public/docs/components/divider.md +97 -0
- package/public/docs/components/dropdown-menu.md +222 -0
- package/public/docs/components/empty-state.md +145 -0
- package/public/docs/components/footer.md +397 -0
- package/public/docs/components/form-field.md +216 -0
- package/public/docs/components/icon-button.md +127 -0
- package/public/docs/components/input.md +200 -0
- package/public/docs/components/kanban-board.md +300 -0
- package/public/docs/components/kbd.md +72 -0
- package/public/docs/components/link.md +98 -0
- package/public/docs/components/list-item.md +241 -0
- package/public/docs/components/loading-animation.md +72 -0
- package/public/docs/components/multi-select.md +202 -0
- package/public/docs/components/navbar.md +144 -0
- package/public/docs/components/newsletter-form.md +112 -0
- package/public/docs/components/page-header.md +151 -0
- package/public/docs/components/pagination.md +212 -0
- package/public/docs/components/product-card.md +172 -0
- package/public/docs/components/progress.md +101 -0
- package/public/docs/components/prompt-input.md +151 -0
- package/public/docs/components/quantity-stepper.md +140 -0
- package/public/docs/components/radio.md +177 -0
- package/public/docs/components/rating.md +106 -0
- package/public/docs/components/score-ring.md +95 -0
- package/public/docs/components/search.md +116 -0
- package/public/docs/components/section-heading.md +113 -0
- package/public/docs/components/segmented-control.md +126 -0
- package/public/docs/components/select.md +224 -0
- package/public/docs/components/sheet.md +145 -0
- package/public/docs/components/sidebar.md +193 -0
- package/public/docs/components/skeleton.md +135 -0
- package/public/docs/components/slider.md +114 -0
- package/public/docs/components/sparkline.md +128 -0
- package/public/docs/components/spinner.md +98 -0
- package/public/docs/components/stat.md +106 -0
- package/public/docs/components/status-tracker.md +145 -0
- package/public/docs/components/stepper.md +147 -0
- package/public/docs/components/switch.md +153 -0
- package/public/docs/components/tabs.md +180 -0
- package/public/docs/components/tag.md +105 -0
- package/public/docs/components/textarea.md +148 -0
- package/public/docs/components/timeline.md +152 -0
- package/public/docs/components/toast.md +194 -0
- package/public/docs/components/tool-call.md +107 -0
- package/public/docs/components/tooltip.md +108 -0
- package/public/manifest/components.json +198 -15
- package/public/manifest/tokens.json +75 -5
- package/scripts/division-ds.mjs +78 -42
- package/scripts/lib/agent-docs.mjs +80 -6
- package/scripts/lib/ai-copy.mjs +120 -0
- package/scripts/lib/button-spacing.mjs +5 -5
- package/scripts/lib/check-text.mjs +57 -0
- package/scripts/lib/contrast.mjs +111 -0
- package/scripts/lib/host-inline.mjs +59 -0
- package/scripts/lib/mcp-server.mjs +116 -0
- package/scripts/lib/mcp-tools.mjs +266 -0
- package/scripts/lib/props-validator.mjs +3 -2
- package/scripts/lib/rules-installer.mjs +42 -4
- package/scripts/lib/token-discipline.mjs +10 -3
- package/skills/division-ds/SKILL.md +90 -0
- package/skills/division-ds/commands/add.md +25 -0
- package/skills/division-ds/commands/build.md +46 -0
- package/skills/division-ds/commands/fix.md +28 -0
- package/skills/division-ds/commands/review.md +25 -0
- package/skills/division-ds/topics/ai.md +8 -0
- package/skills/division-ds/topics/colour.md +11 -0
- package/skills/division-ds/topics/feedback.md +9 -0
- package/skills/division-ds/topics/forms.md +22 -0
- package/skills/division-ds/topics/layout.md +36 -0
- package/skills/division-ds/topics/motion.md +10 -0
- package/skills/division-ds/topics/overlays.md +9 -0
|
@@ -0,0 +1,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.
|