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,143 @@
|
|
|
1
|
+
# StatusTracker
|
|
2
|
+
|
|
3
|
+
> Health over time, drawn as a row of thin bars with one bar per period. The oldest is on the left. Each bar says its status in words and shows them in a tooltip when you hover over it or reach it with the keyboard. The whole row is a single Tab stop, and the arrow keys move along it.
|
|
4
|
+
|
|
5
|
+
- **Best for:** The health history of one service or device, such as uptime over 30 or 90 days.
|
|
6
|
+
- **Not for:** A current status: use Badge. A measured value: use Sparkline.
|
|
7
|
+
- Molecule. Source: `src/components/molecules/status-tracker/StatusTracker.tsx`
|
|
8
|
+
- Rendered docs page: `/components/status-tracker`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { StatusTracker, statusTrackerText } from 'src/components/molecules/status-tracker/StatusTracker'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Requires
|
|
17
|
+
|
|
18
|
+
- The root TooltipProvider (AppShell and DemoLayout mount it).
|
|
19
|
+
|
|
20
|
+
## Props
|
|
21
|
+
|
|
22
|
+
| Prop | Type | Default | Description |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `label` | `string` | | Required. Names the row, for example "Uptime, last 30 days". |
|
|
25
|
+
| `items` | `readonly StatusTrackerItem[]` | | Required. Oldest first. |
|
|
26
|
+
| `summary` | `ReactNode` | | Shown on the right of the label row, for example "99.2% up". |
|
|
27
|
+
| `statusLabels` | `Partial<Record<StatusTrackerStatus, string>>` | | Replaces the status words: Operational, Degraded, Down, No data. |
|
|
28
|
+
| `className` | `string` | | Added to the root. |
|
|
29
|
+
| `ref` | `Ref<HTMLDivElement>` | | Passed through to the root. |
|
|
30
|
+
| `id (StatusTrackerItem)` | `string` | | Required. A key that never changes for that item. |
|
|
31
|
+
| `status (StatusTrackerItem)` | `'ok' \| 'warning' \| 'hostile' \| 'neutral'` | | Required. ok means up. warning means degraded. hostile means down or failed. neutral means no data. |
|
|
32
|
+
| `label (StatusTrackerItem)` | `string` | | Required. The period and what happened, for example "12 Oct: 2 alerts, 14 min degraded". |
|
|
33
|
+
| `statusTrackerText()` | `(item, words?) => string` | | Builds the text a bar uses for its name and tooltip, for example "Degraded. 12 Oct: 2 alerts". |
|
|
34
|
+
|
|
35
|
+
## Types and helpers
|
|
36
|
+
|
|
37
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
export type StatusTrackerStatus = 'ok' | 'warning' | 'hostile' | 'neutral'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export type StatusTrackerItem = {
|
|
45
|
+
/** Stable key. */
|
|
46
|
+
id: string
|
|
47
|
+
/** ok up or healthy; warning degraded; hostile down or failed; neutral no data. */
|
|
48
|
+
status: StatusTrackerStatus
|
|
49
|
+
/** The period and what happened in it: "12 Oct: 2 alerts, 14 min degraded". */
|
|
50
|
+
label: string
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
/** Text of one bar: the status word first, then the caller's label. Shared by the accessible name
|
|
56
|
+
* and the tooltip, so what is heard and what is seen are the same sentence. */
|
|
57
|
+
export function statusTrackerText(item: StatusTrackerItem, words: Record<StatusTrackerStatus, string> = STATUS_WORDS)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
63
|
+
|
|
64
|
+
### Thirty days, with a summary
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
const THIRTY: StatusTrackerItem[] = Array.from({ length: 30 }, (_, index) => {
|
|
68
|
+
const day = new Date(Date.UTC(2026, 8, 16 + index))
|
|
69
|
+
const date = day.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' })
|
|
70
|
+
if (index === 8) return { id: `d${index}`, status: 'warning', label: `${date}: 2 alerts, 14 min degraded` }
|
|
71
|
+
if (index === 19) return { id: `d${index}`, status: 'hostile', label: `${date}: 41 min down` }
|
|
72
|
+
if (index === 23) return { id: `d${index}`, status: 'warning', label: `${date}: 1 alert, 6 min degraded` }
|
|
73
|
+
if (index === 26) return { id: `d${index}`, status: 'neutral', label: `${date}: no readings` }
|
|
74
|
+
return { id: `d${index}`, status: 'ok', label: `${date}: no alerts` }
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
<div className="w-full max-w-xl">
|
|
78
|
+
<StatusTracker label="Uptime, last 30 days" summary="99.2% up" items={THIRTY} />
|
|
79
|
+
</div>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Ninety days in the same width
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
const NINETY: StatusTrackerItem[] = Array.from({ length: 90 }, (_, index) => ({
|
|
86
|
+
id: `n${index}`,
|
|
87
|
+
status: index % 37 === 12 ? 'hostile' : index % 23 === 5 ? 'warning' : 'ok',
|
|
88
|
+
label: `Day ${index + 1} of 90`,
|
|
89
|
+
}))
|
|
90
|
+
|
|
91
|
+
<div className="w-full max-w-xl">
|
|
92
|
+
<StatusTracker label="Uptime, last 90 days" summary="99.6% up" items={NINETY} />
|
|
93
|
+
</div>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Other status words
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<div className="w-full max-w-xs">
|
|
100
|
+
<StatusTracker
|
|
101
|
+
label="Door sensor, last 8 hours"
|
|
102
|
+
statusLabels={{ ok: 'Closed', warning: 'Left open', hostile: 'Forced', neutral: 'Offline' }}
|
|
103
|
+
items={[
|
|
104
|
+
{ id: 'h1', status: 'ok', label: '02:00 to 03:00' },
|
|
105
|
+
{ id: 'h2', status: 'ok', label: '03:00 to 04:00' },
|
|
106
|
+
{ id: 'h3', status: 'warning', label: '04:00 to 05:00, open 9 min' },
|
|
107
|
+
{ id: 'h4', status: 'ok', label: '05:00 to 06:00' },
|
|
108
|
+
{ id: 'h5', status: 'neutral', label: '06:00 to 07:00' },
|
|
109
|
+
{ id: 'h6', status: 'ok', label: '07:00 to 08:00' },
|
|
110
|
+
{ id: 'h7', status: 'ok', label: '08:00 to 09:00' },
|
|
111
|
+
{ id: 'h8', status: 'ok', label: '09:00 to 10:00' },
|
|
112
|
+
]}
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Keyboard
|
|
118
|
+
|
|
119
|
+
| Keys | Result |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `Tab` | Moves into the row, landing on the last bar that had focus (the newest one the first time), and moves out again. |
|
|
122
|
+
| `ArrowLeft` `ArrowRight` | Moves one bar older or newer and shows its tooltip. |
|
|
123
|
+
| `Home` `End` | Jumps to the oldest or the newest bar. |
|
|
124
|
+
| `Escape` | Hides the tooltip. Focus stays on the bar. |
|
|
125
|
+
|
|
126
|
+
## Do
|
|
127
|
+
|
|
128
|
+
- Write each item label with its period and what happened, not just the status. The status word is added for you.
|
|
129
|
+
- Put the headline figure in summary, so the row has a number people can read before they explore it.
|
|
130
|
+
- Use neutral for periods with no data, so a gap looks like a gap and not like a healthy day.
|
|
131
|
+
|
|
132
|
+
## Don't
|
|
133
|
+
|
|
134
|
+
- Don't start an item label with the status word as well. It's already said first.
|
|
135
|
+
- Don't use it for more than about 90 bars. At a 320px-wide screen the bars get thinner than 2px.
|
|
136
|
+
- Don't make a bar do anything when clicked. It's a reading, not a control.
|
|
137
|
+
|
|
138
|
+
## Anatomy
|
|
139
|
+
|
|
140
|
+
- **Label row**: The label in a monospaced font on the left, and the optional summary on the right.
|
|
141
|
+
- **List**: A ul (bulleted list element) named by the label, with one li (list item) per bar, so a screen reader hears how many bars there are and where each one sits.
|
|
142
|
+
- **Bar**: A block 32px (pixels) tall in the status colour. It has role="img" and aria-roledescription="status bar", which tell screen readers what it is, and its name is the status word plus the label.
|
|
143
|
+
- **Tooltip**: The same text as the name, shown on hover and on keyboard focus.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Stepper
|
|
2
|
+
|
|
3
|
+
> Shows where someone is in a flow of several steps. Finished steps get a check mark, the current one is filled with the signal colour, and the rest wait as outlines.
|
|
4
|
+
|
|
5
|
+
- **Best for:** Showing where someone is in a flow of two to five steps.
|
|
6
|
+
- **Not for:** Sections that are equally important: use Tabs. Also not for a single step.
|
|
7
|
+
- Molecule. Source: `src/components/molecules/stepper/Stepper.tsx`
|
|
8
|
+
- Rendered docs page: `/components/stepper`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Stepper } from 'src/components/molecules/stepper/Stepper'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| `steps` | `Step[]` | | Required. { label: string; description?: string }. |
|
|
21
|
+
| `current` | `number` | | Counts from 0. steps.length means every step is complete. |
|
|
22
|
+
| `label` | `string` | `'Progress'` | The list's accessible name, the name screen readers announce. |
|
|
23
|
+
| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Horizontal runs in a row from sm up and stacks below it. Vertical is a column at every width, with connectors. |
|
|
24
|
+
| `className` | `string` | | Merged with the root classes. If two Tailwind utilities clash, the later one wins. |
|
|
25
|
+
| `ref` | `Ref<HTMLOListElement>` | | Passed through to the root ol. |
|
|
26
|
+
|
|
27
|
+
## Types and helpers
|
|
28
|
+
|
|
29
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
export type Step = { label: string; description?: string }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
38
|
+
|
|
39
|
+
### Checkout
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
const CHECKOUT_STEPS: Step[] = [
|
|
43
|
+
{ label: 'Shipping' },
|
|
44
|
+
{ label: 'Payment' },
|
|
45
|
+
{ label: 'Review', description: 'Check the manifest' },
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
<div className="w-full">
|
|
49
|
+
<Stepper steps={CHECKOUT_STEPS} current={1} label="Checkout progress" />
|
|
50
|
+
</div>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Complete
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
const CHECKOUT_STEPS: Step[] = [
|
|
57
|
+
{ label: 'Shipping' },
|
|
58
|
+
{ label: 'Payment' },
|
|
59
|
+
{ label: 'Review', description: 'Check the manifest' },
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
<div className="w-full">
|
|
63
|
+
<Stepper steps={CHECKOUT_STEPS} current={3} label="Checkout progress" />
|
|
64
|
+
</div>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Vertical, with descriptions
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
const SHIPMENT_STEPS: Step[] = [
|
|
71
|
+
{ label: 'Ordered', description: 'Paid on 12 October' },
|
|
72
|
+
{ label: 'Packed', description: 'At the Brasov depot' },
|
|
73
|
+
{ label: 'Shipped', description: 'With the courier' },
|
|
74
|
+
{ label: 'Delivered' },
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
<div className="w-full max-w-xs">
|
|
78
|
+
<Stepper steps={SHIPMENT_STEPS} current={2} orientation="vertical" label="Shipment progress" />
|
|
79
|
+
</div>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Live: move through the steps with Back and Continue
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
const CHECKOUT_STEPS: Step[] = [
|
|
86
|
+
{ label: 'Shipping' },
|
|
87
|
+
{ label: 'Payment' },
|
|
88
|
+
{ label: 'Review', description: 'Check the manifest' },
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
function LiveStepper() {
|
|
92
|
+
const [current, setCurrent] = useState(0)
|
|
93
|
+
return (
|
|
94
|
+
<div className="flex flex-col gap-6 w-full">
|
|
95
|
+
<Stepper steps={CHECKOUT_STEPS} current={current} label="Checkout progress" />
|
|
96
|
+
<div className="flex gap-3">
|
|
97
|
+
<Button
|
|
98
|
+
variant="secondary"
|
|
99
|
+
size="sm"
|
|
100
|
+
disabled={current === 0}
|
|
101
|
+
onClick={() => setCurrent(c => Math.max(0, c - 1))}
|
|
102
|
+
>
|
|
103
|
+
Back
|
|
104
|
+
</Button>
|
|
105
|
+
<Button
|
|
106
|
+
size="sm"
|
|
107
|
+
disabled={current >= CHECKOUT_STEPS.length}
|
|
108
|
+
onClick={() => setCurrent(c => Math.min(CHECKOUT_STEPS.length, c + 1))}
|
|
109
|
+
>
|
|
110
|
+
Continue
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
<LiveStepper />
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Keyboard
|
|
121
|
+
|
|
122
|
+
| Keys | Result |
|
|
123
|
+
|---|---|
|
|
124
|
+
| | Not interactive. Put Back and Continue buttons in the flow itself. |
|
|
125
|
+
|
|
126
|
+
## Do
|
|
127
|
+
|
|
128
|
+
- Keep it to five steps or fewer.
|
|
129
|
+
- Name steps for what happens in them: 'Shipping', not 'Step 1'.
|
|
130
|
+
- Label the list after the flow: 'Checkout progress'.
|
|
131
|
+
- Use vertical beside other content, or when steps have descriptions, like a shipment on an order page.
|
|
132
|
+
|
|
133
|
+
## Don't
|
|
134
|
+
|
|
135
|
+
- Don't make steps clickable so people can skip ahead.
|
|
136
|
+
- Don't use a Stepper for equally important sections. That's what Tabs is for.
|
|
137
|
+
- Don't show a Stepper for a single step.
|
|
138
|
+
|
|
139
|
+
## Anatomy
|
|
140
|
+
|
|
141
|
+
- **List**: An ol (numbered list element) named by label.
|
|
142
|
+
- **Marker**: A 28px (pixel) square. Done: signal fill with a check. Current: signal fill with its number, without leading zeros, because dimmed zeros can't keep enough contrast against the fill. Upcoming: an outline with the two-digit number, leading zero included.
|
|
143
|
+
- **Connector**: A 2px line that turns the signal colour once the step before it is done. Horizontal: hidden when the steps stack, below the sm breakpoint (the small-screen width). Vertical: always shown, running down between the markers.
|
|
144
|
+
- **Label**: In a monospaced font, with its status spelled out in words for screen readers.
|
|
145
|
+
- **Description**: Optional, under the label. The vertical layout gives it the most room.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Switch
|
|
2
|
+
|
|
3
|
+
> An on/off control that acts right away. The thumb's position shows the state, so the meaning still comes through with colour turned off. It takes effect the moment it's flipped.
|
|
4
|
+
|
|
5
|
+
- **Best for:** An on/off setting that takes effect the moment it's flipped.
|
|
6
|
+
- **Not for:** A choice in a form that needs a Save. Use Checkbox.
|
|
7
|
+
- Atom. Source: `src/components/atoms/switch/Switch.tsx`
|
|
8
|
+
- Rendered docs page: `/components/switch`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Switch } from 'src/components/atoms/switch/Switch'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Requires
|
|
17
|
+
|
|
18
|
+
- Its own label. A Switch isn't a FormField child (FormField takes Input, Textarea, Select or QuantityStepper). Give it an id and put a <label htmlFor> beside it.
|
|
19
|
+
|
|
20
|
+
## Props
|
|
21
|
+
|
|
22
|
+
| Prop | Type | Default | Description |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `checked` | `boolean` | | The checked state, when your code controls it. |
|
|
25
|
+
| `defaultChecked` | `boolean` | | The starting state, when the switch manages itself. |
|
|
26
|
+
| `onCheckedChange` | `(checked: boolean) => void` | | Fires the moment it's flipped. |
|
|
27
|
+
| `disabled` | `boolean` | | Takes it out of the tab order and blocks toggling. |
|
|
28
|
+
| `required` | `boolean` | | The browser's own form validation. |
|
|
29
|
+
| `name` | `string` | | The browser's own form field name. |
|
|
30
|
+
| `value` | `string` | | The browser's own form field value. |
|
|
31
|
+
| `id` | `string` | | Pair it with a real label through htmlFor. |
|
|
32
|
+
| `className` | `string` | | Merged with the control's classes. Later Tailwind utilities (the small styling classes this system uses) win. |
|
|
33
|
+
| `ref` | `Ref<HTMLButtonElement>` | | Forwarded to the root button from Radix, the open-source library this switch is built on. |
|
|
34
|
+
| `...rest` | `ButtonHTMLAttributes<HTMLButtonElement>` | | Every other Radix Switch.Root prop, which means every native button attribute, spread onto the root button. For example aria-describedby, or aria-label when no label element points at it. aria-* attributes are ARIA (Accessible Rich Internet Applications) settings that give screen readers extra information. |
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
39
|
+
|
|
40
|
+
### Off and on
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
<div className="flex items-center gap-3">
|
|
44
|
+
<Switch id="state-off" />
|
|
45
|
+
<label htmlFor="state-off">Off</label>
|
|
46
|
+
</div>
|
|
47
|
+
<div className="flex items-center gap-3">
|
|
48
|
+
<Switch id="state-on" defaultChecked />
|
|
49
|
+
<label htmlFor="state-on">On</label>
|
|
50
|
+
</div>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Live
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
function LiveExample() {
|
|
57
|
+
const [on, setOn] = useState(false)
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex items-center gap-4">
|
|
60
|
+
<div className="flex items-center gap-3">
|
|
61
|
+
<Switch id="live-beacon" checked={on} onCheckedChange={setOn} />
|
|
62
|
+
<label htmlFor="live-beacon">Field beacon</label>
|
|
63
|
+
</div>
|
|
64
|
+
<p className="text-sm text-[var(--color-text-secondary)]">
|
|
65
|
+
{on ? 'Broadcasting position.' : 'Beacon is off.'}
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
<LiveExample />
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Disabled
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
<div className="flex items-center gap-3">
|
|
78
|
+
<Switch id="state-disabled-off" disabled />
|
|
79
|
+
<label htmlFor="state-disabled-off">Disabled, off</label>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="flex items-center gap-3">
|
|
82
|
+
<Switch id="state-disabled-on" disabled defaultChecked />
|
|
83
|
+
<label htmlFor="state-disabled-on">Disabled, on</label>
|
|
84
|
+
</div>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### A settings row
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
<div className="flex w-full items-start justify-between gap-4">
|
|
91
|
+
<div>
|
|
92
|
+
<label htmlFor="settings-offline-maps" className="font-medium">
|
|
93
|
+
Offline maps
|
|
94
|
+
</label>
|
|
95
|
+
<p className="text-sm text-[var(--color-text-secondary)]">
|
|
96
|
+
Download the current route area before you lose signal.
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
<Switch id="settings-offline-maps" defaultChecked />
|
|
100
|
+
</div>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Keyboard
|
|
104
|
+
|
|
105
|
+
| Keys | Result |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `Tab` | Moves focus to it. |
|
|
108
|
+
| `Space` | Toggles it. |
|
|
109
|
+
| `Enter` | Toggles it. Radix allows this on a switch, though not on a checkbox. |
|
|
110
|
+
|
|
111
|
+
## Do
|
|
112
|
+
|
|
113
|
+
- Use it only when flipping it takes effect right away.
|
|
114
|
+
- Label it with the thing being switched, not with ‘on’ or ‘off’.
|
|
115
|
+
- Show the result of the switch near it if the effect isn't visible.
|
|
116
|
+
|
|
117
|
+
## Don't
|
|
118
|
+
|
|
119
|
+
- Don't put a switch in a form that needs a Save. Use a Checkbox.
|
|
120
|
+
- Don't put ‘on’/‘off’ text inside the track. The position already says it, and the text won't fit at 24px.
|
|
121
|
+
- Don't make the thumb take longer than --duration-base (a timing token) to arrive. A slow switch feels broken. The settle after it arrives only changes its shape, and it ends within --duration-slow, because the state is already readable.
|
|
122
|
+
- Don't use the squash on anything that isn't a small physical object under the user's finger. On a panel or a page it looks like a wobble.
|
|
123
|
+
- Don't add a rebound to the thumb. It gives once against the edge and stops. A switch that keeps moving after the state has landed looks unsure.
|
|
124
|
+
- Don't fill the track with the accent when it's on. Thumb and track changing colour together, mid-flight, is what made the flip look like a twitch.
|
|
125
|
+
|
|
126
|
+
## Common mistakes
|
|
127
|
+
|
|
128
|
+
Wrong:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
<FormField label="Email notifications">
|
|
132
|
+
<Switch />
|
|
133
|
+
</FormField>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Right:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
<div className="flex items-center gap-3">
|
|
140
|
+
<Switch id="notify" checked={on} onCheckedChange={setOn} />
|
|
141
|
+
<label htmlFor="notify">Email notifications</label>
|
|
142
|
+
</div>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
FormField passes its child an invalid prop, and Switch doesn't take one. A switch acts at once, so it has no error state to show.
|
|
146
|
+
|
|
147
|
+
## Anatomy
|
|
148
|
+
|
|
149
|
+
- **Root**: role=switch, which tells screen readers (software that reads the page aloud) what it is. A 44×24 track.
|
|
150
|
+
- **Thumb**: A 16px (pixel) square, 3px from the track edge in both states, travelling 22px. Its position is the state signal. It's text.primary when off and accent.signal.fill when on. Held down, it stretches 4px away from the edge it rests on: to the right when off, to the left when on, so it never leaves the track. Flipped, it speeds up across the track, gets a little longer in flight and a little shorter against the far edge, then goes back to its own shape. One give, no bounce.
|
|
151
|
+
- **Track fill**: The control fill, on and off. When it's on, only the border turns to signal.fill. The track doesn't fill with the accent, so the thumb is the one thing that changes colour during a flip.
|
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| `value (Root)` | `string` | | The active tab, when the page controls it. |
|
|
21
|
+
| `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. |
|
|
22
|
+
| `defaultValue` | `string` | | The tab that starts active, when the component manages it (Root). |
|
|
23
|
+
| `onValueChange` | `(value: string) => void` | | Called when the active tab changes (Root). |
|
|
24
|
+
| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Which arrow keys move between tabs (Root). Vertical also puts the list beside the panels. |
|
|
25
|
+
| `activationMode` | `'automatic' \| 'manual'` | `'automatic'` | 'manual' waits for Enter or Space before switching, for panels that are slow to draw (Root). |
|
|
26
|
+
| `value (Trigger)` | `string` | | This tab's own value. |
|
|
27
|
+
| `disabled` | `boolean` | `false` | Turns off one tab (Trigger). |
|
|
28
|
+
| `icon (Trigger)` | `ReactNode` | | A small symbol before the label, hidden from screen readers (aria-hidden). |
|
|
29
|
+
| `count (Trigger)` | `number` | | A number after the label, included in the name screen readers announce. |
|
|
30
|
+
| `className` | `string` | | Merged with the root classes. If two Tailwind utilities clash, the later one wins. Available on every part. |
|
|
31
|
+
| `ref` | `Ref<HTMLElement>` | | Passed through to the underlying page element. Available on every part. |
|
|
32
|
+
|
|
33
|
+
## Examples
|
|
34
|
+
|
|
35
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
36
|
+
|
|
37
|
+
### Default
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<div className="max-w-sm w-full">
|
|
41
|
+
<Tabs defaultValue="replica">
|
|
42
|
+
<TabsList aria-label="Kit sections">
|
|
43
|
+
<TabsTrigger value="replica">Replica</TabsTrigger>
|
|
44
|
+
<TabsTrigger value="optics">Optics</TabsTrigger>
|
|
45
|
+
<TabsTrigger value="parts">Parts</TabsTrigger>
|
|
46
|
+
</TabsList>
|
|
47
|
+
<TabsContent value="replica">Electric carbine replica.</TabsContent>
|
|
48
|
+
<TabsContent value="optics">Tube sight and holo sight.</TabsContent>
|
|
49
|
+
<TabsContent value="parts">Magazines, a battery and a charger.</TabsContent>
|
|
50
|
+
</Tabs>
|
|
51
|
+
</div>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### With a disabled tab
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
<div className="max-w-sm w-full">
|
|
58
|
+
<Tabs defaultValue="replica">
|
|
59
|
+
<TabsList aria-label="Kit sections">
|
|
60
|
+
<TabsTrigger value="replica">Replica</TabsTrigger>
|
|
61
|
+
<TabsTrigger value="optics">Optics</TabsTrigger>
|
|
62
|
+
<TabsTrigger value="parts" disabled>
|
|
63
|
+
Parts
|
|
64
|
+
</TabsTrigger>
|
|
65
|
+
</TabsList>
|
|
66
|
+
<TabsContent value="replica">Electric carbine replica.</TabsContent>
|
|
67
|
+
<TabsContent value="optics">Tube sight and holo sight.</TabsContent>
|
|
68
|
+
<TabsContent value="parts">Magazines, a battery and a charger.</TabsContent>
|
|
69
|
+
</Tabs>
|
|
70
|
+
</div>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Line, with icons and counts
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<div className="max-w-md w-full">
|
|
77
|
+
<Tabs defaultValue="open" variant="line">
|
|
78
|
+
<TabsList aria-label="Orders">
|
|
79
|
+
<TabsTrigger value="open" icon={<Inbox />} count={12}>
|
|
80
|
+
Open
|
|
81
|
+
</TabsTrigger>
|
|
82
|
+
<TabsTrigger value="packed" icon={<Package />} count={4}>
|
|
83
|
+
Packed
|
|
84
|
+
</TabsTrigger>
|
|
85
|
+
<TabsTrigger value="archived" icon={<Archive />} count={0}>
|
|
86
|
+
Archived
|
|
87
|
+
</TabsTrigger>
|
|
88
|
+
</TabsList>
|
|
89
|
+
<TabsContent value="open">Twelve orders waiting to be packed.</TabsContent>
|
|
90
|
+
<TabsContent value="packed">Four orders packed, waiting for the courier.</TabsContent>
|
|
91
|
+
<TabsContent value="archived">No archived orders yet.</TabsContent>
|
|
92
|
+
</Tabs>
|
|
93
|
+
</div>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Vertical, line
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<div className="max-w-md w-full">
|
|
100
|
+
<Tabs defaultValue="profile" orientation="vertical" variant="line">
|
|
101
|
+
<TabsList aria-label="Settings">
|
|
102
|
+
<TabsTrigger value="profile" icon={<User />}>
|
|
103
|
+
Profile
|
|
104
|
+
</TabsTrigger>
|
|
105
|
+
<TabsTrigger value="security" icon={<Shield />}>
|
|
106
|
+
Security
|
|
107
|
+
</TabsTrigger>
|
|
108
|
+
</TabsList>
|
|
109
|
+
<TabsContent value="profile">Callsign, squad and home field.</TabsContent>
|
|
110
|
+
<TabsContent value="security">Password and signed-in devices.</TabsContent>
|
|
111
|
+
</Tabs>
|
|
112
|
+
</div>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Controlled: the page keeps track of which tab is open
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
function ControlledExample() {
|
|
119
|
+
const [value, setValue] = useState('replica')
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className="max-w-sm w-full">
|
|
123
|
+
<div className="flex gap-2 mb-4">
|
|
124
|
+
<Button size="sm" variant="secondary" onClick={() => setValue('replica')}>
|
|
125
|
+
Show replica
|
|
126
|
+
</Button>
|
|
127
|
+
<Button size="sm" variant="secondary" onClick={() => setValue('optics')}>
|
|
128
|
+
Show optics
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
<Tabs value={value} onValueChange={setValue}>
|
|
132
|
+
<TabsList aria-label="Kit sections">
|
|
133
|
+
<TabsTrigger value="replica">Replica</TabsTrigger>
|
|
134
|
+
<TabsTrigger value="optics">Optics</TabsTrigger>
|
|
135
|
+
</TabsList>
|
|
136
|
+
<TabsContent value="replica">Electric carbine replica.</TabsContent>
|
|
137
|
+
<TabsContent value="optics">Tube sight and holo sight.</TabsContent>
|
|
138
|
+
</Tabs>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
<ControlledExample />
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Keyboard
|
|
147
|
+
|
|
148
|
+
| Keys | Result |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `Tab` | Moves into the tab row, onto the active tab. Pressing Tab again moves into the panel. |
|
|
151
|
+
| `ArrowLeft` `ArrowRight` | Previous or next tab. It wraps round at the ends and skips turned-off tabs. |
|
|
152
|
+
| `ArrowUp` `ArrowDown` | The same, when orientation is vertical. |
|
|
153
|
+
| `Home` `End` | First or last tab. |
|
|
154
|
+
|
|
155
|
+
## Do
|
|
156
|
+
|
|
157
|
+
- Keep tab labels to one or two words.
|
|
158
|
+
- Use tabs for equals: sections a person would move between freely.
|
|
159
|
+
- Give the tab row an aria-label that names the whole set.
|
|
160
|
+
- Use the line variant inside a card or settings panel, and the fill variant at the top of a view.
|
|
161
|
+
- Use a count for something a person would act on, like open orders or unread reports.
|
|
162
|
+
|
|
163
|
+
## Don't
|
|
164
|
+
|
|
165
|
+
- Don't use tabs for steps in a sequence. That's what Stepper is for.
|
|
166
|
+
- Don't put a second row of tabs inside a panel.
|
|
167
|
+
- Don't mix fill and line tabs on one page.
|
|
168
|
+
- Don't give every tab an icon just because one has one. Icons go on all of them or none.
|
|
169
|
+
- Don't use tabs to move between pages. A tab changes what's shown, not where you are.
|
|
170
|
+
|
|
171
|
+
## Anatomy
|
|
172
|
+
|
|
173
|
+
- **Tabs**: The root. Holds which tab is active, whether the page controls that or the component does.
|
|
174
|
+
- **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.
|
|
175
|
+
- **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.
|
|
176
|
+
- **Icon**: Optional, before the label. aria-hidden, so screen readers skip it.
|
|
177
|
+
- **Count**: Optional, after the label. You see it as a number, and screen readers read it as part of the name: Open (12).
|
|
178
|
+
- **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.
|