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,106 @@
1
+ # Stat
2
+
3
+ > One number that matters, and how it has moved. The number is the star of the show. The label is small, set in a monospaced font and kept quiet.
4
+
5
+ - **Best for:** One headline number and how it has moved. Up to four in a row.
6
+ - **Not for:** A value that only makes sense as a chart: use ChartPanel.
7
+ - Molecule. Source: `src/components/molecules/stat/Stat.tsx`
8
+ - Rendered docs page: `/components/stat`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Stat } from 'src/components/molecules/stat/Stat'
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` | `string` | | The stat's name, for example "Kits dispatched". |
23
+ | `value` | `number \| string` | | Whole numbers get padded. Anything else (for example "4.2k") is shown exactly as given. |
24
+ | `unit` | `string` | | Shown beside the value, for example "kg". |
25
+ | `padTo` | `number` | | Pads a whole-number value to this many digits with dimmed zeros that screen readers skip (aria-hidden). |
26
+ | `delta` | `{ value: string; direction: 'up' \| 'down'; sentiment: 'positive' \| 'negative' \| 'neutral'; period: string }` | | value comes already formatted (for example "12%"). direction picks the arrow. sentiment picks the colour, and it doesn't depend on direction: fewer failed syncs is down, and that's positive. period is what the number is compared with, for example "last week". It's both shown and spoken. |
27
+ | `className` | `string` | | Merged with the root classes. If two Tailwind utilities clash, the later one wins. |
28
+ | `ref` | `Ref<HTMLDivElement>` | | Passed through to the root div. |
29
+ | `...rest` | `HTMLAttributes<HTMLDivElement>` | | Any standard div attribute, applied to the root div. |
30
+
31
+ ## Types and helpers
32
+
33
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
34
+
35
+ ```ts
36
+ export type StatDelta = {
37
+ /** Already formatted, e.g. "12%". */
38
+ value: string
39
+ direction: 'up' | 'down'
40
+ /** Whether this movement is good. Independent of direction: fewer failed syncs is down AND good. */
41
+ sentiment: 'positive' | 'negative' | 'neutral'
42
+ /** What it is compared with, e.g. "last week". Spoken and shown. */
43
+ period: string
44
+ }
45
+ ```
46
+
47
+ ## Examples
48
+
49
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
50
+
51
+ ### A row of stats
52
+
53
+ ```tsx
54
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-6 w-full">
55
+ <Stat
56
+ label="Kits dispatched"
57
+ value={18}
58
+ padTo={3}
59
+ delta={{ value: '12%', direction: 'up', sentiment: 'positive', period: 'last week' }}
60
+ />
61
+ <Stat
62
+ label="Failed syncs"
63
+ value={3}
64
+ delta={{ value: '40%', direction: 'down', sentiment: 'positive', period: 'last week' }}
65
+ />
66
+ <Stat
67
+ label="Depots offline"
68
+ value={2}
69
+ delta={{ value: '1', direction: 'up', sentiment: 'negative', period: 'last week' }}
70
+ />
71
+ <Stat label="Payload" value={24} unit="kg" />
72
+ </div>
73
+ ```
74
+
75
+ ### Without a delta
76
+
77
+ ```tsx
78
+ <div className="max-w-[10rem] w-full">
79
+ <Stat label="Kits dispatched" value={18} padTo={3} />
80
+ </div>
81
+ ```
82
+
83
+ ## Keyboard
84
+
85
+ Not focusable. No keyboard behaviour of its own.
86
+
87
+ ## Do
88
+
89
+ - Lay several Stats out in a plain div grid. Each one renders its own dl (description list), so you don't need a list element around them.
90
+ - Set sentiment by what the change means, not by its direction: fewer failed syncs is down, and that's positive.
91
+ - Say what the delta is compared with.
92
+ - Only pad when every value in a row has the same fixed width.
93
+
94
+ ## Don't
95
+
96
+ - Don't pad a value whose width isn't fixed. The zeros would be made up.
97
+ - Don't put more than four stats in a row.
98
+ - Don't use a Stat for a value that only makes sense as a chart.
99
+
100
+ ## Anatomy
101
+
102
+ - **Label**: A dt (the "term" element of an HTML description list), in text.secondary.
103
+ - **Value**: A dd (the matching "description" element) at 36px (pixels), set in Oxanium with tabular figures, so every digit is the same width.
104
+ - **Pad**: Optional leading zeros in numeral.pad. They're aria-hidden, so screen readers skip them, and they only appear when the width is real.
105
+ - **Unit**: A short suffix beside the value, for example kg.
106
+ - **Delta**: The change. An arrow shows the direction, colour shows whether it's good or bad, and a spoken sentence says both for screen reader users.
@@ -0,0 +1,145 @@
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
+ 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
+ - The root TooltipProvider (AppShell and DemoLayout mount it).
21
+
22
+ ## Props
23
+
24
+ | Prop | Type | Default | Description |
25
+ |---|---|---|---|
26
+ | `label` | `string` | | Required. Names the row, for example "Uptime, last 30 days". |
27
+ | `items` | `readonly StatusTrackerItem[]` | | Required. Oldest first. |
28
+ | `summary` | `ReactNode` | | Shown on the right of the label row, for example "99.2% up". |
29
+ | `statusLabels` | `Partial<Record<StatusTrackerStatus, string>>` | | Replaces the status words: Operational, Degraded, Down, No data. |
30
+ | `className` | `string` | | Added to the root. |
31
+ | `ref` | `Ref<HTMLDivElement>` | | Passed through to the root. |
32
+ | `id (StatusTrackerItem)` | `string` | | Required. A key that never changes for that item. |
33
+ | `status (StatusTrackerItem)` | `'ok' \| 'warning' \| 'hostile' \| 'neutral'` | | Required. ok means up. warning means degraded. hostile means down or failed. neutral means no data. |
34
+ | `label (StatusTrackerItem)` | `string` | | Required. The period and what happened, for example "12 Oct: 2 alerts, 14 min degraded". |
35
+ | `statusTrackerText()` | `(item, words?) => string` | | Builds the text a bar uses for its name and tooltip, for example "Degraded. 12 Oct: 2 alerts". |
36
+
37
+ ## Types and helpers
38
+
39
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
40
+
41
+ ```ts
42
+ export type StatusTrackerStatus = 'ok' | 'warning' | 'hostile' | 'neutral'
43
+ ```
44
+
45
+ ```ts
46
+ export type StatusTrackerItem = {
47
+ /** Stable key. */
48
+ id: string
49
+ /** ok up or healthy; warning degraded; hostile down or failed; neutral no data. */
50
+ status: StatusTrackerStatus
51
+ /** The period and what happened in it: "12 Oct: 2 alerts, 14 min degraded". */
52
+ label: string
53
+ }
54
+ ```
55
+
56
+ ```ts
57
+ /** Text of one bar: the status word first, then the caller's label. Shared by the accessible name
58
+ * and the tooltip, so what is heard and what is seen are the same sentence. */
59
+ export function statusTrackerText(item: StatusTrackerItem, words: Record<StatusTrackerStatus, string> = STATUS_WORDS)
60
+ ```
61
+
62
+ ## Examples
63
+
64
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
65
+
66
+ ### Thirty days, with a summary
67
+
68
+ ```tsx
69
+ const THIRTY: StatusTrackerItem[] = Array.from({ length: 30 }, (_, index) => {
70
+ const day = new Date(Date.UTC(2026, 8, 16 + index))
71
+ const date = day.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' })
72
+ if (index === 8) return { id: `d${index}`, status: 'warning', label: `${date}: 2 alerts, 14 min degraded` }
73
+ if (index === 19) return { id: `d${index}`, status: 'hostile', label: `${date}: 41 min down` }
74
+ if (index === 23) return { id: `d${index}`, status: 'warning', label: `${date}: 1 alert, 6 min degraded` }
75
+ if (index === 26) return { id: `d${index}`, status: 'neutral', label: `${date}: no readings` }
76
+ return { id: `d${index}`, status: 'ok', label: `${date}: no alerts` }
77
+ })
78
+
79
+ <div className="w-full max-w-xl">
80
+ <StatusTracker label="Uptime, last 30 days" summary="99.2% up" items={THIRTY} />
81
+ </div>
82
+ ```
83
+
84
+ ### Ninety days in the same width
85
+
86
+ ```tsx
87
+ const NINETY: StatusTrackerItem[] = Array.from({ length: 90 }, (_, index) => ({
88
+ id: `n${index}`,
89
+ status: index % 37 === 12 ? 'hostile' : index % 23 === 5 ? 'warning' : 'ok',
90
+ label: `Day ${index + 1} of 90`,
91
+ }))
92
+
93
+ <div className="w-full max-w-xl">
94
+ <StatusTracker label="Uptime, last 90 days" summary="99.6% up" items={NINETY} />
95
+ </div>
96
+ ```
97
+
98
+ ### Other status words
99
+
100
+ ```tsx
101
+ <div className="w-full max-w-xs">
102
+ <StatusTracker
103
+ label="Door sensor, last 8 hours"
104
+ statusLabels={{ ok: 'Closed', warning: 'Left open', hostile: 'Forced', neutral: 'Offline' }}
105
+ items={[
106
+ { id: 'h1', status: 'ok', label: '02:00 to 03:00' },
107
+ { id: 'h2', status: 'ok', label: '03:00 to 04:00' },
108
+ { id: 'h3', status: 'warning', label: '04:00 to 05:00, open 9 min' },
109
+ { id: 'h4', status: 'ok', label: '05:00 to 06:00' },
110
+ { id: 'h5', status: 'neutral', label: '06:00 to 07:00' },
111
+ { id: 'h6', status: 'ok', label: '07:00 to 08:00' },
112
+ { id: 'h7', status: 'ok', label: '08:00 to 09:00' },
113
+ { id: 'h8', status: 'ok', label: '09:00 to 10:00' },
114
+ ]}
115
+ />
116
+ </div>
117
+ ```
118
+
119
+ ## Keyboard
120
+
121
+ | Keys | Result |
122
+ |---|---|
123
+ | `Tab` | Moves into the row, landing on the last bar that had focus (the newest one the first time), and moves out again. |
124
+ | `ArrowLeft` `ArrowRight` | Moves one bar older or newer and shows its tooltip. |
125
+ | `Home` `End` | Jumps to the oldest or the newest bar. |
126
+ | `Escape` | Hides the tooltip. Focus stays on the bar. |
127
+
128
+ ## Do
129
+
130
+ - Write each item label with its period and what happened, not just the status. The status word is added for you.
131
+ - Put the headline figure in summary, so the row has a number people can read before they explore it.
132
+ - Use neutral for periods with no data, so a gap looks like a gap and not like a healthy day.
133
+
134
+ ## Don't
135
+
136
+ - Don't start an item label with the status word as well. It's already said first.
137
+ - Don't use it for more than about 90 bars. At a 320px-wide screen the bars get thinner than 2px.
138
+ - Don't make a bar do anything when clicked. It's a reading, not a control.
139
+
140
+ ## Anatomy
141
+
142
+ - **Label row**: The label in a monospaced font on the left, and the optional summary on the right.
143
+ - **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.
144
+ - **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.
145
+ - **Tooltip**: The same text as the name, shown on hover and on keyboard focus.
@@ -0,0 +1,147 @@
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
+ 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
+ | `steps` | `Step[]` | | Required. { label: string; description?: string }. |
23
+ | `current` | `number` | | Counts from 0. steps.length means every step is complete. |
24
+ | `label` | `string` | `'Progress'` | The list's accessible name, the name screen readers announce. |
25
+ | `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. |
26
+ | `className` | `string` | | Merged with the root classes. If two Tailwind utilities clash, the later one wins. |
27
+ | `ref` | `Ref<HTMLOListElement>` | | Passed through to the root ol. |
28
+
29
+ ## Types and helpers
30
+
31
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
32
+
33
+ ```ts
34
+ export type Step = { label: string; description?: string }
35
+ ```
36
+
37
+ ## Examples
38
+
39
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
40
+
41
+ ### Checkout
42
+
43
+ ```tsx
44
+ const CHECKOUT_STEPS: Step[] = [
45
+ { label: 'Shipping' },
46
+ { label: 'Payment' },
47
+ { label: 'Review', description: 'Check the manifest' },
48
+ ]
49
+
50
+ <div className="w-full">
51
+ <Stepper steps={CHECKOUT_STEPS} current={1} label="Checkout progress" />
52
+ </div>
53
+ ```
54
+
55
+ ### Complete
56
+
57
+ ```tsx
58
+ const CHECKOUT_STEPS: Step[] = [
59
+ { label: 'Shipping' },
60
+ { label: 'Payment' },
61
+ { label: 'Review', description: 'Check the manifest' },
62
+ ]
63
+
64
+ <div className="w-full">
65
+ <Stepper steps={CHECKOUT_STEPS} current={3} label="Checkout progress" />
66
+ </div>
67
+ ```
68
+
69
+ ### Vertical, with descriptions
70
+
71
+ ```tsx
72
+ const SHIPMENT_STEPS: Step[] = [
73
+ { label: 'Ordered', description: 'Paid on 12 October' },
74
+ { label: 'Packed', description: 'At the Brasov depot' },
75
+ { label: 'Shipped', description: 'With the courier' },
76
+ { label: 'Delivered' },
77
+ ]
78
+
79
+ <div className="w-full max-w-xs">
80
+ <Stepper steps={SHIPMENT_STEPS} current={2} orientation="vertical" label="Shipment progress" />
81
+ </div>
82
+ ```
83
+
84
+ ### Live: move through the steps with Back and Continue
85
+
86
+ ```tsx
87
+ const CHECKOUT_STEPS: Step[] = [
88
+ { label: 'Shipping' },
89
+ { label: 'Payment' },
90
+ { label: 'Review', description: 'Check the manifest' },
91
+ ]
92
+
93
+ function LiveStepper() {
94
+ const [current, setCurrent] = useState(0)
95
+ return (
96
+ <div className="flex flex-col gap-6 w-full">
97
+ <Stepper steps={CHECKOUT_STEPS} current={current} label="Checkout progress" />
98
+ <div className="flex gap-3">
99
+ <Button
100
+ variant="secondary"
101
+ size="sm"
102
+ disabled={current === 0}
103
+ onClick={() => setCurrent(c => Math.max(0, c - 1))}
104
+ >
105
+ Back
106
+ </Button>
107
+ <Button
108
+ size="sm"
109
+ disabled={current >= CHECKOUT_STEPS.length}
110
+ onClick={() => setCurrent(c => Math.min(CHECKOUT_STEPS.length, c + 1))}
111
+ >
112
+ Continue
113
+ </Button>
114
+ </div>
115
+ </div>
116
+ )
117
+ }
118
+
119
+ <LiveStepper />
120
+ ```
121
+
122
+ ## Keyboard
123
+
124
+ | Keys | Result |
125
+ |---|---|
126
+ | | Not interactive. Put Back and Continue buttons in the flow itself. |
127
+
128
+ ## Do
129
+
130
+ - Keep it to five steps or fewer.
131
+ - Name steps for what happens in them: 'Shipping', not 'Step 1'.
132
+ - Label the list after the flow: 'Checkout progress'.
133
+ - Use vertical beside other content, or when steps have descriptions, like a shipment on an order page.
134
+
135
+ ## Don't
136
+
137
+ - Don't make steps clickable so people can skip ahead.
138
+ - Don't use a Stepper for equally important sections. That's what Tabs is for.
139
+ - Don't show a Stepper for a single step.
140
+
141
+ ## Anatomy
142
+
143
+ - **List**: An ol (numbered list element) named by label.
144
+ - **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.
145
+ - **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.
146
+ - **Label**: In a monospaced font, with its status spelled out in words for screen readers.
147
+ - **Description**: Optional, under the label. The vertical layout gives it the most room.
@@ -0,0 +1,153 @@
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
+ 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
+ - 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.
21
+
22
+ ## Props
23
+
24
+ | Prop | Type | Default | Description |
25
+ |---|---|---|---|
26
+ | `checked` | `boolean` | | The checked state, when your code controls it. |
27
+ | `defaultChecked` | `boolean` | | The starting state, when the switch manages itself. |
28
+ | `onCheckedChange` | `(checked: boolean) => void` | | Fires the moment it's flipped. |
29
+ | `disabled` | `boolean` | | Takes it out of the tab order and blocks toggling. |
30
+ | `required` | `boolean` | | The browser's own form validation. |
31
+ | `name` | `string` | | The browser's own form field name. |
32
+ | `value` | `string` | | The browser's own form field value. |
33
+ | `id` | `string` | | Pair it with a real label through htmlFor. |
34
+ | `className` | `string` | | Merged with the control's classes. Later Tailwind utilities (the small styling classes this system uses) win. |
35
+ | `ref` | `Ref<HTMLButtonElement>` | | Forwarded to the root button from Radix, the open-source library this switch is built on. |
36
+ | `...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. |
37
+
38
+ ## Examples
39
+
40
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
41
+
42
+ ### Off and on
43
+
44
+ ```tsx
45
+ <div className="flex items-center gap-3">
46
+ <Switch id="state-off" />
47
+ <label htmlFor="state-off">Off</label>
48
+ </div>
49
+ <div className="flex items-center gap-3">
50
+ <Switch id="state-on" defaultChecked />
51
+ <label htmlFor="state-on">On</label>
52
+ </div>
53
+ ```
54
+
55
+ ### Live
56
+
57
+ ```tsx
58
+ function LiveExample() {
59
+ const [on, setOn] = useState(false)
60
+ return (
61
+ <div className="flex items-center gap-4">
62
+ <div className="flex items-center gap-3">
63
+ <Switch id="live-beacon" checked={on} onCheckedChange={setOn} />
64
+ <label htmlFor="live-beacon">Field beacon</label>
65
+ </div>
66
+ <p className="text-sm text-[var(--color-text-secondary)]">
67
+ {on ? 'Broadcasting position.' : 'Beacon is off.'}
68
+ </p>
69
+ </div>
70
+ )
71
+ }
72
+
73
+ <LiveExample />
74
+ ```
75
+
76
+ ### Disabled
77
+
78
+ ```tsx
79
+ <div className="flex items-center gap-3">
80
+ <Switch id="state-disabled-off" disabled />
81
+ <label htmlFor="state-disabled-off">Disabled, off</label>
82
+ </div>
83
+ <div className="flex items-center gap-3">
84
+ <Switch id="state-disabled-on" disabled defaultChecked />
85
+ <label htmlFor="state-disabled-on">Disabled, on</label>
86
+ </div>
87
+ ```
88
+
89
+ ### A settings row
90
+
91
+ ```tsx
92
+ <div className="flex w-full items-start justify-between gap-4">
93
+ <div>
94
+ <label htmlFor="settings-offline-maps" className="font-medium">
95
+ Offline maps
96
+ </label>
97
+ <p className="text-sm text-[var(--color-text-secondary)]">
98
+ Download the current route area before you lose signal.
99
+ </p>
100
+ </div>
101
+ <Switch id="settings-offline-maps" defaultChecked />
102
+ </div>
103
+ ```
104
+
105
+ ## Keyboard
106
+
107
+ | Keys | Result |
108
+ |---|---|
109
+ | `Tab` | Moves focus to it. |
110
+ | `Space` | Toggles it. |
111
+ | `Enter` | Toggles it. Radix allows this on a switch, though not on a checkbox. |
112
+
113
+ ## Do
114
+
115
+ - Use it only when flipping it takes effect right away.
116
+ - Label it with the thing being switched, not with ‘on’ or ‘off’.
117
+ - Show the result of the switch near it if the effect isn't visible.
118
+
119
+ ## Don't
120
+
121
+ - Don't put a switch in a form that needs a Save. Use a Checkbox.
122
+ - Don't put ‘on’/‘off’ text inside the track. The position already says it, and the text won't fit at 24px.
123
+ - 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.
124
+ - 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.
125
+ - 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.
126
+ - 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.
127
+
128
+ ## Common mistakes
129
+
130
+ Wrong:
131
+
132
+ ```tsx
133
+ <FormField label="Email notifications">
134
+ <Switch />
135
+ </FormField>
136
+ ```
137
+
138
+ Right:
139
+
140
+ ```tsx
141
+ <div className="flex items-center gap-3">
142
+ <Switch id="notify" checked={on} onCheckedChange={setOn} />
143
+ <label htmlFor="notify">Email notifications</label>
144
+ </div>
145
+ ```
146
+
147
+ 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.
148
+
149
+ ## Anatomy
150
+
151
+ - **Root**: role=switch, which tells screen readers (software that reads the page aloud) what it is. A 44×24 track.
152
+ - **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.indicator 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.
153
+ - **Track fill**: The control fill, on and off. When it's on, only the border turns to accent.signal.indicator. The track doesn't fill with the accent, so the thumb is the one thing that changes colour during a flip.