division-ds 0.1.1 → 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.
Files changed (97) hide show
  1. package/AGENTS.md +16 -2
  2. package/README.md +11 -0
  3. package/division-ds-0.2.0.tgz +0 -0
  4. package/package.json +2 -2
  5. package/public/docs/components/accordion.md +205 -0
  6. package/public/docs/components/ai-label.md +88 -0
  7. package/public/docs/components/alert-dialog.md +290 -0
  8. package/public/docs/components/alert.md +173 -0
  9. package/public/docs/components/approval-request.md +146 -0
  10. package/public/docs/components/auth-form.md +130 -0
  11. package/public/docs/components/avatar-group.md +150 -0
  12. package/public/docs/components/avatar.md +177 -0
  13. package/public/docs/components/badge.md +176 -0
  14. package/public/docs/components/breadcrumbs.md +110 -0
  15. package/public/docs/components/button.md +202 -0
  16. package/public/docs/components/card.md +255 -0
  17. package/public/docs/components/carousel.md +128 -0
  18. package/public/docs/components/cart.md +170 -0
  19. package/public/docs/components/chart-panel.md +356 -0
  20. package/public/docs/components/chat-message.md +154 -0
  21. package/public/docs/components/chat-thread.md +242 -0
  22. package/public/docs/components/checkbox.md +147 -0
  23. package/public/docs/components/checkout-form.md +126 -0
  24. package/public/docs/components/command-palette.md +145 -0
  25. package/public/docs/components/data-table.md +399 -0
  26. package/public/docs/components/description-list.md +101 -0
  27. package/public/docs/components/dialog.md +195 -0
  28. package/public/docs/components/divider.md +95 -0
  29. package/public/docs/components/dropdown-menu.md +220 -0
  30. package/public/docs/components/empty-state.md +143 -0
  31. package/public/docs/components/footer.md +395 -0
  32. package/public/docs/components/form-field.md +213 -0
  33. package/public/docs/components/icon-button.md +125 -0
  34. package/public/docs/components/input.md +198 -0
  35. package/public/docs/components/kanban-board.md +298 -0
  36. package/public/docs/components/kbd.md +70 -0
  37. package/public/docs/components/link.md +96 -0
  38. package/public/docs/components/list-item.md +238 -0
  39. package/public/docs/components/loading-animation.md +70 -0
  40. package/public/docs/components/multi-select.md +200 -0
  41. package/public/docs/components/navbar.md +142 -0
  42. package/public/docs/components/newsletter-form.md +110 -0
  43. package/public/docs/components/page-header.md +149 -0
  44. package/public/docs/components/pagination.md +210 -0
  45. package/public/docs/components/product-card.md +169 -0
  46. package/public/docs/components/progress.md +99 -0
  47. package/public/docs/components/prompt-input.md +149 -0
  48. package/public/docs/components/quantity-stepper.md +138 -0
  49. package/public/docs/components/radio.md +140 -0
  50. package/public/docs/components/rating.md +104 -0
  51. package/public/docs/components/score-ring.md +93 -0
  52. package/public/docs/components/search.md +114 -0
  53. package/public/docs/components/section-heading.md +111 -0
  54. package/public/docs/components/segmented-control.md +124 -0
  55. package/public/docs/components/select.md +222 -0
  56. package/public/docs/components/sheet.md +143 -0
  57. package/public/docs/components/sidebar.md +191 -0
  58. package/public/docs/components/skeleton.md +133 -0
  59. package/public/docs/components/slider.md +112 -0
  60. package/public/docs/components/sparkline.md +126 -0
  61. package/public/docs/components/spinner.md +96 -0
  62. package/public/docs/components/stat.md +104 -0
  63. package/public/docs/components/status-tracker.md +143 -0
  64. package/public/docs/components/stepper.md +145 -0
  65. package/public/docs/components/switch.md +151 -0
  66. package/public/docs/components/tabs.md +178 -0
  67. package/public/docs/components/tag.md +103 -0
  68. package/public/docs/components/textarea.md +146 -0
  69. package/public/docs/components/timeline.md +150 -0
  70. package/public/docs/components/toast.md +192 -0
  71. package/public/docs/components/tool-call.md +105 -0
  72. package/public/docs/components/tooltip.md +106 -0
  73. package/public/manifest/components.json +189 -7
  74. package/public/manifest/tokens.json +39 -0
  75. package/scripts/division-ds.mjs +76 -37
  76. package/scripts/lib/agent-docs.mjs +59 -2
  77. package/scripts/lib/ai-copy.mjs +106 -0
  78. package/scripts/lib/button-spacing.mjs +5 -5
  79. package/scripts/lib/check-text.mjs +44 -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 +115 -0
  83. package/scripts/lib/mcp-tools.mjs +241 -0
  84. package/scripts/lib/props-validator.mjs +1 -1
  85. package/scripts/lib/rules-installer.mjs +42 -4
  86. package/skills/division-ds/SKILL.md +90 -0
  87. package/skills/division-ds/commands/add.md +25 -0
  88. package/skills/division-ds/commands/build.md +46 -0
  89. package/skills/division-ds/commands/fix.md +28 -0
  90. package/skills/division-ds/commands/review.md +25 -0
  91. package/skills/division-ds/topics/ai.md +7 -0
  92. package/skills/division-ds/topics/colour.md +11 -0
  93. package/skills/division-ds/topics/feedback.md +9 -0
  94. package/skills/division-ds/topics/forms.md +19 -0
  95. package/skills/division-ds/topics/layout.md +34 -0
  96. package/skills/division-ds/topics/motion.md +10 -0
  97. package/skills/division-ds/topics/overlays.md +9 -0
@@ -0,0 +1,133 @@
1
+ # Skeleton
2
+
3
+ > A loading placeholder shaped like the content it stands in for: lines of text, a cut-corner block, or an Avatar’s footprint. The shapes themselves are silent. SkeletonGroup is what tells people the region is loading.
4
+
5
+ - **Best for:** A region whose content is loading, shown as placeholders in its shape inside one SkeletonGroup.
6
+ - **Not for:** A busy button: use Button loading. A wait under a few hundred milliseconds: show nothing.
7
+ - Atom. Source: `src/components/atoms/skeleton/Skeleton.tsx`
8
+ - Rendered docs page: `/components/skeleton`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Skeleton, SkeletonGroup } from 'src/components/atoms/skeleton/Skeleton'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `shape` | `'text' \| 'block' \| 'avatar'` | `'text'` | What the placeholder stands in for. |
21
+ | `lines` | `number` | `1` | Text only. The number of lines. Anything below 1 counts as 1. |
22
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Avatar only. Matches Avatar’s size prop. |
23
+ | `className` | `string` | | Merged with the component classes. A block takes its width and height from here. |
24
+ | `ref` | `Ref<HTMLDivElement>` | | Passed through to the root element. |
25
+ | `...rest` | `Omit<HTMLAttributes<HTMLDivElement>, 'children'>` | | Every native div prop except children, because a skeleton draws its own shapes. |
26
+ | `label (SkeletonGroup)` | `string` | `'Loading…'` | The hidden words a screen reader hears instead of the shapes. |
27
+ | `className (SkeletonGroup)` | `string` | | Lay the placeholders out here, the way the real content will be laid out. |
28
+ | `ref (SkeletonGroup)` | `Ref<HTMLDivElement>` | | Passed through to the container. |
29
+ | `...rest (SkeletonGroup)` | `HTMLAttributes<HTMLDivElement>` | | Every native div prop. |
30
+
31
+ ## Examples
32
+
33
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
34
+
35
+ ### Shapes: text lines, a block, the Avatar footprints
36
+
37
+ ```tsx
38
+ <div className="flex w-full flex-col gap-6">
39
+ <Skeleton shape="text" lines={3} className="max-w-md" />
40
+ <Skeleton shape="block" className="h-24 w-full max-w-md" />
41
+ <div className="flex items-center gap-3">
42
+ <Skeleton shape="avatar" size="sm" />
43
+ <Skeleton shape="avatar" size="md" />
44
+ <Skeleton shape="avatar" size="lg" />
45
+ </div>
46
+ </div>
47
+ ```
48
+
49
+ ### A card loading: image, title, two lines of copy
50
+
51
+ ```tsx
52
+ <SkeletonGroup label="Loading replicas" className="grid w-full max-w-md gap-4 sm:grid-cols-2">
53
+ {[0, 1].map(i => (
54
+ <Card key={i}>
55
+ <CardBody className="flex flex-col gap-4">
56
+ <Skeleton shape="block" className="aspect-[4/3] w-full" />
57
+ <Skeleton shape="text" className="w-2/3" />
58
+ <Skeleton shape="text" lines={2} />
59
+ </CardBody>
60
+ </Card>
61
+ ))}
62
+ </SkeletonGroup>
63
+ ```
64
+
65
+ ### Table rows loading
66
+
67
+ ```tsx
68
+ <SkeletonGroup label="Loading orders" className="w-full max-w-xl">
69
+ <div className="flex flex-col divide-y divide-[var(--color-border-hairline)]">
70
+ {[0, 1, 2].map(i => (
71
+ <div key={i} className="grid grid-cols-[1fr_2fr_1fr] items-center gap-4 py-3">
72
+ <Skeleton shape="text" />
73
+ <Skeleton shape="text" className="w-4/5" />
74
+ <Skeleton shape="text" className="w-1/2 justify-self-end" />
75
+ </div>
76
+ ))}
77
+ </div>
78
+ </SkeletonGroup>
79
+ ```
80
+
81
+ ### A profile row loading
82
+
83
+ ```tsx
84
+ <SkeletonGroup label="Loading the squad roster" className="flex w-full max-w-sm items-center gap-3">
85
+ <Skeleton shape="avatar" size="md" />
86
+ <div className="flex flex-1 flex-col gap-2">
87
+ <Skeleton shape="text" className="w-1/2" />
88
+ <Skeleton shape="text" className="w-1/3" />
89
+ </div>
90
+ </SkeletonGroup>
91
+ ```
92
+
93
+ ## Keyboard
94
+
95
+ Not focusable. No keyboard behaviour of its own.
96
+
97
+ ## Do
98
+
99
+ - Wrap one region’s placeholders in one SkeletonGroup, and remove it when the content arrives.
100
+ - Name what’s loading in the group label: Loading orders, not just Loading.
101
+ - Match the layout of the real content, so nothing jumps when it replaces the placeholder.
102
+ - Use shape="avatar" with the size the real Avatar will have.
103
+
104
+ ## Don't
105
+
106
+ - Don't round a skeleton or draw a circle. The system has square corners, and Avatar is a square too.
107
+ - Don't put a skeleton on a busy button or control. A busy Button takes loading.
108
+ - Don't nest one SkeletonGroup inside another. One busy region gets one announcement.
109
+ - Don't show a skeleton for a wait under a few hundred milliseconds. The flash is worse than the wait.
110
+
111
+ ## Common mistakes
112
+
113
+ Wrong:
114
+
115
+ ```tsx
116
+ <div><Skeleton lines={3} /></div>
117
+ ```
118
+
119
+ Right:
120
+
121
+ ```tsx
122
+ <SkeletonGroup label="Loading orders"><Skeleton lines={3} /></SkeletonGroup>
123
+ ```
124
+
125
+ Every shape is aria-hidden, an ARIA (Accessible Rich Internet Applications) attribute that hides it from assistive tech. So without the group, a screen reader, the software that reads the page aloud, hears nothing at all while the region loads.
126
+
127
+ ## Anatomy
128
+
129
+ - **Shape**: A border.hairline fill with a border.hairline-strong band sweeping across it every 1.6s. It’s see-through, so it reads on the page, a Card or an overlay. aria-hidden.
130
+ - **Text lines**: 12px (pixel) bars, 8px apart, full width. When there are several, the last one is three fifths wide.
131
+ - **Block**: Any rectangle, sized with className, with the 8px cut corner.
132
+ - **Avatar**: An Avatar’s exact footprint: 24, 36 or 48px, with the corner cut at md and lg only.
133
+ - **SkeletonGroup**: The container. It has aria-busy="true", which tells assistive tech the region is busy, and a visually hidden label that reads Loading… unless you name what’s loading.
@@ -0,0 +1,112 @@
1
+ # Slider
2
+
3
+ > A value or a range picked on a track. Each thumb (the handle you drag) gets its own name. If a range slider shared one name, a screen-reader user (someone using software that reads the page aloud) would get two controls called the same thing.
4
+
5
+ - **Best for:** A value or range on a track, where the rough position matters and the value is shown.
6
+ - **Not for:** A precise value with no other way to enter it. Use Input.
7
+ - Atom. Source: `src/components/atoms/slider/Slider.tsx`
8
+ - Rendered docs page: `/components/slider`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Slider } from 'src/components/atoms/slider/Slider'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `label` | `string` | | Required. Applied to each thumb. A single thumb gets it as-is, and a range adds "minimum" / "maximum" to the end. |
21
+ | `value` | `number[]` | | The value(s), when you control them from outside. One number makes a single slider; two make a range. |
22
+ | `defaultValue` | `number[]` | | The starting value(s), when the slider manages them itself (uncontrolled). |
23
+ | `onValueChange` | `(value: number[]) => void` | | Fires as the thumb moves. |
24
+ | `min` | `number` | `0` | The lowest value. |
25
+ | `max` | `number` | `100` | The highest value. |
26
+ | `step` | `number` | `1` | The increment for both snapping while dragging and the arrow keys. |
27
+ | `disabled` | `boolean` | | Takes it out of the tab order and blocks input. |
28
+ | `formatValue` | `(value: number) => string` | | One function feeds both the readout and aria-valuetext (the ARIA, or Accessible Rich Internet Applications, attribute a screen reader speaks as the value). So the spoken value and the printed one can’t drift apart, whether the slider is controlled or not. |
29
+ | `showValue` | `boolean` | `false` | Shows the current value(s) as a readout. |
30
+ | `className` | `string` | | Merged with the outer wrapper's classes. Later Tailwind utilities (Tailwind is the CSS framework the system uses) win. |
31
+ | `ref` | `Ref<HTMLSpanElement>` | | Passed through to the Radix Slider root. Radix is the library of accessible building blocks this sits on. |
32
+ | `...rest` | `Omit<Slider.Root props, 'aria-label' \| 'aria-labelledby' \| 'children'>` | | Every Radix Slider.Root prop except those three, spread onto the root. aria-label and aria-labelledby are left out because the accessible name lives on each thumb, where label puts it. One on the root would do nothing at all, not override anything. children is typed away because this component draws its own Track/Range/Thumbs, and a children prop would be thrown away without any warning. |
33
+
34
+ ## Examples
35
+
36
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
37
+
38
+ ### Single value
39
+
40
+ ```tsx
41
+ function SingleValueLive() {
42
+ const [weight, setWeight] = useState([8])
43
+ return (
44
+ <div className="w-full max-w-sm">
45
+ <Slider
46
+ label="Kit weight"
47
+ value={weight}
48
+ onValueChange={setWeight}
49
+ max={20}
50
+ formatValue={v => `${v} kg`}
51
+ showValue
52
+ />
53
+ </div>
54
+ )
55
+ }
56
+
57
+ <SingleValueLive />
58
+ ```
59
+
60
+ ### Range
61
+
62
+ ```tsx
63
+ <div className="w-full max-w-sm">
64
+ <Slider label="Weight range" defaultValue={[4, 12]} max={20} />
65
+ </div>
66
+ ```
67
+
68
+ ### Stepped
69
+
70
+ ```tsx
71
+ <div className="w-full max-w-sm">
72
+ <Slider label="BB bottles" defaultValue={[5]} max={20} step={5} />
73
+ </div>
74
+ ```
75
+
76
+ ### Disabled
77
+
78
+ ```tsx
79
+ <div className="w-full max-w-sm">
80
+ <Slider label="Locked ration" defaultValue={[8]} max={20} disabled />
81
+ </div>
82
+ ```
83
+
84
+ ## Keyboard
85
+
86
+ | Keys | Result |
87
+ |---|---|
88
+ | `→` `↑` | Goes up by one step. |
89
+ | `←` `↓` | Goes down by one step. |
90
+ | `Page Up` | Goes up by a bigger jump. |
91
+ | `Page Down` | Goes down by a bigger jump. |
92
+ | `Home` | Jumps to the minimum. |
93
+ | `End` | Jumps to the maximum. |
94
+
95
+ ## Do
96
+
97
+ - Use formatValue whenever the number has a unit.
98
+ - Show the value. A slider with no readout asks people to guess.
99
+ - Put a number input beside it when the exact value matters.
100
+
101
+ ## Don't
102
+
103
+ - Don't use a slider where a precise value is required and nothing else lets people enter one.
104
+ - Don't shrink the thumb below 24×24 of hit area.
105
+ - Don't rely on dragging alone. Every slider here works fully from the keyboard, and it should stay that way.
106
+
107
+ ## Anatomy
108
+
109
+ - **Label and readout**: An 11px (pixel) mono label, in a monospaced font, and a 24px Oxanium readout with tabular figures, digits that all take the same width. Unlike Progress, there are no dimmed leading zeros. min and max belong to the caller and formatValue can return anything, so a fixed digit width would be one the value doesn’t actually have.
110
+ - **Track**: 4px tall, sunken, no border.
111
+ - **Range**: The filled span, in signal.fill.
112
+ - **Thumb**: A 20px square inside a 24px hit area, the region that responds to a click or tap. There’s one per value.
@@ -0,0 +1,126 @@
1
+ # Sparkline
2
+
3
+ > A small trend line with no axes. It sits beside a Stat or in a table cell. It's plain SVG (Scalable Vector Graphics, the web's format for drawn shapes): one image named by a sentence, with an optional safe band behind it and a square on the last value.
4
+
5
+ - **Best for:** You want the shape of a recent series, not its values. Think of a reading over the last hour.
6
+ - **Not for:** Values people need to read, compare or hover over. Use ChartPanel for those.
7
+ - Atom. Source: `src/components/atoms/sparkline/Sparkline.tsx`
8
+ - Rendered docs page: `/components/sparkline`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Sparkline } from 'src/components/atoms/sparkline/Sparkline'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `values` | `readonly number[]` | | Required. Oldest first. Non-finite values (such as NaN or Infinity) are dropped. |
21
+ | `label` | `string` | | Required. The chart as one sentence: "Temperature, last hour: 3.1 to 4.2 °C, rising". |
22
+ | `tone` | `'neutral' \| 'signal' \| 'ok' \| 'warning' \| 'hostile' \| 'info'` | `'neutral'` | The line's colour. Each tone means what it means on Badge. |
23
+ | `min` | `number` | | A fixed bottom for the scale. Without it, the scale follows the data (and the band). |
24
+ | `max` | `number` | | A fixed top for the scale. Without it, the scale follows the data (and the band). |
25
+ | `band` | `{ from: number; to: number }` | | A range drawn behind the line. |
26
+ | `showLast` | `boolean` | `false` | Puts a square on the last value. |
27
+ | `width` | `number` | `96` | In CSS px, the pixel unit of Cascading Style Sheets. className="w-full" overrides it. |
28
+ | `height` | `number` | `24` | In CSS px. |
29
+ | `className` | `string` | | Goes on the frame. |
30
+ | `ref` | `Ref<HTMLSpanElement>` | | Forwarded to the frame. |
31
+
32
+ ## Types and helpers
33
+
34
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
35
+
36
+ ```ts
37
+ export type SparklineTone = 'neutral' | 'signal' | 'ok' | 'warning' | 'hostile' | 'info'
38
+ ```
39
+
40
+ ## Examples
41
+
42
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
43
+
44
+ ### Beside the value it trends
45
+
46
+ ```tsx
47
+ <div className="flex items-center gap-3">
48
+ <span className="font-mono text-sm tabular-nums">4.2 °C</span>
49
+ <Sparkline
50
+ values={[3.1, 3.2, 3.2, 3.4, 3.3, 3.6, 3.8, 3.7, 4.0, 4.2]}
51
+ label="Cold room 2, last hour: 3.1 to 4.2 °C, rising"
52
+ band={{ from: 2, to: 8 }}
53
+ showLast
54
+ />
55
+ </div>
56
+ <div className="flex items-center gap-3">
57
+ <span className="font-mono text-sm tabular-nums">412</span>
58
+ <Sparkline
59
+ values={[380, 365, 402, 398, 371, 420, 412]}
60
+ label="Orders, last 7 days: 365 to 420, steady"
61
+ tone="signal"
62
+ showLast
63
+ />
64
+ </div>
65
+ ```
66
+
67
+ ### Tones
68
+
69
+ ```tsx
70
+ <Sparkline values={[5, 6, 5, 7, 6, 8]} label="Neutral trend, rising" />
71
+ <Sparkline values={[5, 6, 5, 7, 6, 8]} label="Healthy trend, rising" tone="ok" />
72
+ <Sparkline values={[5, 6, 5, 7, 6, 8]} label="Degraded trend, rising" tone="warning" />
73
+ <Sparkline values={[5, 6, 5, 7, 6, 8]} label="Failing trend, rising" tone="hostile" />
74
+ <Sparkline values={[5, 6, 5, 7, 6, 8]} label="Informational trend, rising" tone="info" />
75
+ ```
76
+
77
+ ### Stretched to its container, with a safe band from 2 to 8 °C
78
+
79
+ ```tsx
80
+ <div className="w-full max-w-md">
81
+ <Sparkline
82
+ values={[4.1, 4.3, 4.0, 5.2, 6.8, 8.4, 9.1, 8.7, 7.2, 5.9, 5.0, 4.6]}
83
+ label="Cold room 3, last 12 hours: 4.0 to 9.1 °C, above the safe band for 3 hours, now back inside it"
84
+ tone="warning"
85
+ band={{ from: 2, to: 8 }}
86
+ min={0}
87
+ max={10}
88
+ height={40}
89
+ className="w-full"
90
+ showLast
91
+ />
92
+ </div>
93
+ ```
94
+
95
+ ### No values, one value, a flat run
96
+
97
+ ```tsx
98
+ <Sparkline values={[]} label="No readings yet" />
99
+ <Sparkline values={[12]} label="One reading: 12" showLast />
100
+ <Sparkline values={[7, 7, 7, 7]} label="Flat at 7" showLast />
101
+ ```
102
+
103
+ ## Keyboard
104
+
105
+ Not focusable. No keyboard behaviour of its own.
106
+
107
+ ## Do
108
+
109
+ - Write the label as the reading a person takes from the shape: the range and the direction.
110
+ - Fix min and max when several sparklines sit side by side and have to share a scale.
111
+ - Put the current value beside it in text. The line shows the trend, not the number.
112
+ - Use a tone for what the trend means (hostile for out of range), and say it in text too.
113
+
114
+ ## Don't
115
+
116
+ - Don't use a sparkline where someone has to read values off it. That's a job for ChartPanel.
117
+ - Don't leave the label as a title like "Temperature". It's the only thing a screen reader hears.
118
+ - Don't colour every sparkline on a page. Neutral is the default for a reason.
119
+
120
+ ## Anatomy
121
+
122
+ - **Frame**: An inline box. It's 96 x 24 by default, or the container's width with className="w-full".
123
+ - **Image**: An svg with role="img", named by label. Screen readers (software that reads the page aloud) don't get the points one by one.
124
+ - **Band**: Optional. A hairline-filled range behind the line, such as a safe range.
125
+ - **Line**: 1.5px (pixels) in the tone colour. The stroke doesn't get thicker when the frame stretches.
126
+ - **Last value**: An optional 6px square on the newest point. It's aria-hidden, a setting that hides it from screen readers.
@@ -0,0 +1,96 @@
1
+ # Spinner
2
+
3
+ > A busy indicator for an indeterminate wait, meaning one with no known end or percentage. It comes in two forms. The ring has a lit arc that grows and slides as it turns. The conveyor is a row of shaded blocks, for a wait that sits inside a line of text.
4
+
5
+ - **Best for:** A short wait with no known end, inside a control or a line of text.
6
+ - **Not for:** Progress with a known amount (use Progress), or a wait for a whole page (use Skeleton or LoadingAnimation).
7
+ - Atom. Source: `src/components/atoms/spinner/Spinner.tsx`
8
+ - Rendered docs page: `/components/spinner`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Spinner, spinnerVariants } from 'src/components/atoms/spinner/Spinner'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `variant` | `'ring' \| 'conveyor'` | `'ring'` | The rotating ring, or the block conveyor. |
21
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Ring: a 14px, 20px or 24px (pixel) box. Conveyor: 12px, 14px or 16px glyphs. |
22
+ | `label` | `string` | | Status text that's hidden on screen. Passing it makes the spinner a live region, an area whose changes screen readers announce on their own. |
23
+ | `className` | `string` | | Merged with the component's classes. Later Tailwind utilities (the small styling classes this system uses) win. |
24
+ | `ref` | `Ref<HTMLSpanElement>` | | Forwarded to the span element. |
25
+ | `...rest` | `Omit<HTMLAttributes<HTMLSpanElement>, 'role'>` | | Every native span prop except role, spread onto the span. role is removed from the type instead of being quietly overridden at runtime. Here's why: label is the only source of role="status", and a role in rest would silence the live region a caller asked for when they passed label. |
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 const spinnerVariants = cva(
33
+ // The arc takes `currentColor`, so a caller can recolour it with one text utility: Button does
34
+ // exactly that, so the arc matches whatever ink the button label is set in.
35
+ 'inline-block shrink-0 text-[var(--color-accent-signal-fill)]',
36
+ {
37
+ variants: {
38
+ size: { sm: 'w-3.5 h-3.5', md: 'w-5 h-5', lg: 'w-6 h-6' },
39
+ },
40
+ defaultVariants: { size: 'md' },
41
+ }
42
+ )
43
+ ```
44
+
45
+ ## Examples
46
+
47
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
48
+
49
+ ### Ring, three sizes
50
+
51
+ ```tsx
52
+ <Spinner size="sm" label="Loading field kits" />
53
+ <Spinner size="md" label="Loading field kits" />
54
+ <Spinner size="lg" label="Loading field kits" />
55
+ ```
56
+
57
+ ### Conveyor, three sizes
58
+
59
+ ```tsx
60
+ <Spinner variant="conveyor" size="sm" label="Syncing manifest" />
61
+ <Spinner variant="conveyor" size="md" label="Syncing manifest" />
62
+ <Spinner variant="conveyor" size="lg" label="Syncing manifest" />
63
+ ```
64
+
65
+ ### Decorative, inside a control
66
+
67
+ ```tsx
68
+ <span className="inline-flex items-center gap-2 border border-[var(--color-border-hairline-strong)] px-3 py-2">
69
+ <Spinner aria-hidden="true" />
70
+ <span className="text-sm">Dispatching…</span>
71
+ </span>
72
+ ```
73
+
74
+ ## Keyboard
75
+
76
+ Not focusable. No keyboard behaviour of its own.
77
+
78
+ ## Do
79
+
80
+ - Pass label whenever the spinner is the only sign that something's happening.
81
+ - Leave it decorative inside a control that already announces aria-busy, like Button. aria-busy is an ARIA (Accessible Rich Internet Applications) attribute that tells screen readers a part of the page is still loading.
82
+ - Name what's loading, not the fact of loading: ‘Loading field kits’, not ‘Loading’.
83
+
84
+ ## Don't
85
+
86
+ - Don't use it for progress with a known amount. That's Progress.
87
+ - Don't rely on the rotation alone. It stops when someone has turned on reduced motion in their system settings, so the label carries the meaning.
88
+ - Don't scale it past lg. A page-level wait needs a skeleton, not a bigger spinner.
89
+ - Don't bevel it. It's the one round shape in the system. A cut corner on something that rotates would travel around the edge instead of marking one spot.
90
+
91
+ ## Anatomy
92
+
93
+ - **span**: The root. It gets role="status" only when it's labelled.
94
+ - **Ring (ring)**: An SVG (Scalable Vector Graphics) track at 20% of currentColor, which is the surrounding text colour, plus an arc in currentColor. That's accent.signal.fill by default.
95
+ - **Blocks (conveyor)**: Three block glyphs, from full to half shade, running over a ten-cell track in border.control.
96
+ - **sr-only label**: The text a screen reader announces. sr-only means it's hidden on screen but still read aloud. It's only there when label is passed.
@@ -0,0 +1,104 @@
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
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `label` | `string` | | The stat's name, for example "Kits dispatched". |
21
+ | `value` | `number \| string` | | Whole numbers get padded. Anything else (for example "4.2k") is shown exactly as given. |
22
+ | `unit` | `string` | | Shown beside the value, for example "kg". |
23
+ | `padTo` | `number` | | Pads a whole-number value to this many digits with dimmed zeros that screen readers skip (aria-hidden). |
24
+ | `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. |
25
+ | `className` | `string` | | Merged with the root classes. If two Tailwind utilities clash, the later one wins. |
26
+ | `ref` | `Ref<HTMLDivElement>` | | Passed through to the root div. |
27
+ | `...rest` | `HTMLAttributes<HTMLDivElement>` | | Any standard div attribute, applied to the root div. |
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 StatDelta = {
35
+ /** Already formatted, e.g. "12%". */
36
+ value: string
37
+ direction: 'up' | 'down'
38
+ /** Whether this movement is good. Independent of direction: fewer failed syncs is down AND good. */
39
+ sentiment: 'positive' | 'negative' | 'neutral'
40
+ /** What it is compared with, e.g. "last week". Spoken and shown. */
41
+ period: string
42
+ }
43
+ ```
44
+
45
+ ## Examples
46
+
47
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
48
+
49
+ ### A row of stats
50
+
51
+ ```tsx
52
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-6 w-full">
53
+ <Stat
54
+ label="Kits dispatched"
55
+ value={18}
56
+ padTo={3}
57
+ delta={{ value: '12%', direction: 'up', sentiment: 'positive', period: 'last week' }}
58
+ />
59
+ <Stat
60
+ label="Failed syncs"
61
+ value={3}
62
+ delta={{ value: '40%', direction: 'down', sentiment: 'positive', period: 'last week' }}
63
+ />
64
+ <Stat
65
+ label="Depots offline"
66
+ value={2}
67
+ delta={{ value: '1', direction: 'up', sentiment: 'negative', period: 'last week' }}
68
+ />
69
+ <Stat label="Payload" value={24} unit="kg" />
70
+ </div>
71
+ ```
72
+
73
+ ### Without a delta
74
+
75
+ ```tsx
76
+ <div className="max-w-[10rem] w-full">
77
+ <Stat label="Kits dispatched" value={18} padTo={3} />
78
+ </div>
79
+ ```
80
+
81
+ ## Keyboard
82
+
83
+ Not focusable. No keyboard behaviour of its own.
84
+
85
+ ## Do
86
+
87
+ - 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.
88
+ - Set sentiment by what the change means, not by its direction: fewer failed syncs is down, and that's positive.
89
+ - Say what the delta is compared with.
90
+ - Only pad when every value in a row has the same fixed width.
91
+
92
+ ## Don't
93
+
94
+ - Don't pad a value whose width isn't fixed. The zeros would be made up.
95
+ - Don't put more than four stats in a row.
96
+ - Don't use a Stat for a value that only makes sense as a chart.
97
+
98
+ ## Anatomy
99
+
100
+ - **Label**: A dt (the "term" element of an HTML description list), in text.secondary.
101
+ - **Value**: A dd (the matching "description" element) at 36px (pixels), set in Oxanium with tabular figures, so every digit is the same width.
102
+ - **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.
103
+ - **Unit**: A short suffix beside the value, for example kg.
104
+ - **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.