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,193 @@
1
+ # Sidebar
2
+
3
+ > The navigation down the side of an app. It holds groups of pages, each page with an icon and an optional count. Groups can fold away, and there is room for a header and an account line.
4
+
5
+ - **Best for:** App navigation down the side: grouped pages with icons and counts.
6
+ - **Not for:** A site header with a few links: use Navbar.
7
+ - Organism. Source: `src/components/organisms/sidebar/Sidebar.tsx`
8
+ - Rendered docs page: `/components/sidebar`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Sidebar } from 'src/components/organisms/sidebar/Sidebar'
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` | | Required. The name screen readers give the navigation region. |
23
+ | `groups` | `SidebarGroup[]` | | Required. Each group has title, icon, items, collapsible and defaultOpen. |
24
+ | `header` | `ReactNode` | | Sits above the groups: a product name, a workspace switcher. |
25
+ | `footer` | `ReactNode` | | Pinned below the groups when the Sidebar has a height (h-full in a column layout): the signed-in account, a help link. |
26
+ | `renderLink` | `(props: LinkRenderProps) => ReactNode` | `a plain anchor` | Pass your router's link component here. Map href to its own prop, and pass className, aria-current and onClick through. |
27
+ | `className` | `string` | | Added to the root classes. Where two Tailwind utility classes clash, the later one wins. |
28
+ | `ref` | `Ref<HTMLElement>` | | The nav element. |
29
+
30
+ ## Types and helpers
31
+
32
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
33
+
34
+ ```ts
35
+ export type SidebarItem = {
36
+ label: string
37
+ href: string
38
+ /** An icon, drawn aria-hidden; the label carries the meaning. */
39
+ icon?: ReactNode
40
+ current?: boolean
41
+ /** A number shown at the end of the item and read as part of its name ("Orders 12"). */
42
+ count?: number
43
+ }
44
+ ```
45
+
46
+ ```ts
47
+ export type SidebarGroup = {
48
+ /** Also the React key for this group; unique within a Sidebar. */
49
+ title: string
50
+ /** Drawn before the title, aria-hidden; the title alone names the group. */
51
+ icon?: ReactNode
52
+ items: SidebarItem[]
53
+ /** @default true */
54
+ collapsible?: boolean
55
+ /** Ignored when the group holds the current item, which always starts open. @default true */
56
+ defaultOpen?: boolean
57
+ }
58
+ ```
59
+
60
+ ## Examples
61
+
62
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
63
+
64
+ ### Dashboard navigation
65
+
66
+ ```tsx
67
+ const GROUPS: SidebarGroup[] = [
68
+ {
69
+ title: 'Operations',
70
+ icon: <Warehouse />,
71
+ items: [
72
+ { label: 'Overview', href: '#overview', icon: <LayoutDashboard /> },
73
+ { label: 'Orders', href: '#orders', icon: <Package />, current: true, count: 12 },
74
+ { label: 'Shipments', href: '#shipments', icon: <Truck /> },
75
+ { label: 'Returns', href: '#returns', icon: <Inbox />, count: 3 },
76
+ ],
77
+ },
78
+ {
79
+ title: 'Catalogue',
80
+ icon: <Boxes />,
81
+ defaultOpen: false,
82
+ items: [
83
+ { label: 'Products', href: '#products', icon: <Box /> },
84
+ { label: 'Inventory', href: '#inventory', icon: <Map /> },
85
+ ],
86
+ },
87
+ {
88
+ title: 'Account',
89
+ icon: <UserCog />,
90
+ collapsible: false,
91
+ items: [
92
+ { label: 'Settings', href: '#settings', icon: <Settings /> },
93
+ { label: 'Team', href: '#team', icon: <Users /> },
94
+ ],
95
+ },
96
+ ]
97
+
98
+ const HEADER = <p className="font-display font-semibold">Hexline ops</p>
99
+
100
+ const FOOTER = <p className="text-xs text-[var(--color-text-secondary)]">Signed in as Ana Pop</p>
101
+
102
+ <div className="max-w-64">
103
+ <Sidebar label="Dashboard navigation" groups={GROUPS} header={HEADER} footer={FOOTER} />
104
+ </div>
105
+ ```
106
+
107
+ ### In a sheet on a narrow screen: picking a link closes the sheet, through renderLink
108
+
109
+ ```tsx
110
+ const GROUPS: SidebarGroup[] = [
111
+ {
112
+ title: 'Operations',
113
+ icon: <Warehouse />,
114
+ items: [
115
+ { label: 'Overview', href: '#overview', icon: <LayoutDashboard /> },
116
+ { label: 'Orders', href: '#orders', icon: <Package />, current: true, count: 12 },
117
+ { label: 'Shipments', href: '#shipments', icon: <Truck /> },
118
+ { label: 'Returns', href: '#returns', icon: <Inbox />, count: 3 },
119
+ ],
120
+ },
121
+ {
122
+ title: 'Catalogue',
123
+ icon: <Boxes />,
124
+ defaultOpen: false,
125
+ items: [
126
+ { label: 'Products', href: '#products', icon: <Box /> },
127
+ { label: 'Inventory', href: '#inventory', icon: <Map /> },
128
+ ],
129
+ },
130
+ {
131
+ title: 'Account',
132
+ icon: <UserCog />,
133
+ collapsible: false,
134
+ items: [
135
+ { label: 'Settings', href: '#settings', icon: <Settings /> },
136
+ { label: 'Team', href: '#team', icon: <Users /> },
137
+ ],
138
+ },
139
+ ]
140
+
141
+ const HEADER = <p className="font-display font-semibold">Hexline ops</p>
142
+
143
+ const FOOTER = <p className="text-xs text-[var(--color-text-secondary)]">Signed in as Ana Pop</p>
144
+
145
+ const closeOnChoose = ({ onClick, ...props }: LinkRenderProps) => (
146
+ <SheetClose asChild type={undefined}>
147
+ <a {...props} onClick={onClick} />
148
+ </SheetClose>
149
+ )
150
+
151
+ <Sheet>
152
+ <SheetTrigger asChild>
153
+ <Button variant="secondary">Open navigation</Button>
154
+ </SheetTrigger>
155
+ <SheetContent side="left" title="Navigation">
156
+ <Sidebar
157
+ label="Dashboard navigation, in a sheet"
158
+ groups={GROUPS}
159
+ header={HEADER}
160
+ footer={FOOTER}
161
+ renderLink={closeOnChoose}
162
+ />
163
+ </SheetContent>
164
+ </Sheet>
165
+ ```
166
+
167
+ ## Keyboard
168
+
169
+ | Keys | Result |
170
+ |---|---|
171
+ | `Tab` | Moves through each group title that can fold, then each visible item. |
172
+ | `Enter` | Follows an item, or folds and unfolds a group. |
173
+ | `Space` | Folds and unfolds a group. |
174
+
175
+ ## Do
176
+
177
+ - Only show counts for things that need attention: open orders, not total products.
178
+ - Keep the group that holds the current page open.
179
+ - On a narrow screen, put the whole sidebar in a Sheet behind a button.
180
+
181
+ ## Don't
182
+
183
+ - Don't nest groups inside groups.
184
+ - Don't show a count of zero. Leave it off.
185
+ - Don't rely on icons alone. Every item keeps its label.
186
+
187
+ ## Anatomy
188
+
189
+ - **nav**: The navigation region. Screen readers call it by the label prop.
190
+ - **Group title**: A disclosure button (one that shows and hides the list) when the group can fold, plain text when it can't. It's text.secondary, so it still reads on a raised panel. Its list opens and closes by animating its height. Opening also fades and slides each item in, each one a little later than the one before (capped, so a long group never feels slow). Closing fades everything out together, all at once. If someone has asked their system for less motion (prefers-reduced-motion: reduce), both happen instantly.
191
+ - **Item**: An icon, a label and an optional count. The current page gets the fill, plus aria-current, an ARIA attribute (Accessible Rich Internet Applications, the labels screen readers rely on) that tells screen readers this is the page you are on.
192
+ - **Header**: Sits above the groups: a product name, a workspace switcher.
193
+ - **Footer**: Pinned below the groups: the signed-in account, a help link.
@@ -0,0 +1,135 @@
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
+ 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
+ | `shape` | `'text' \| 'block' \| 'avatar'` | `'text'` | What the placeholder stands in for. |
23
+ | `lines` | `number` | `1` | Text only. The number of lines. Anything below 1 counts as 1. |
24
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Avatar only. Matches Avatar’s size prop. |
25
+ | `className` | `string` | | Merged with the component classes. A block takes its width and height from here. |
26
+ | `ref` | `Ref<HTMLDivElement>` | | Passed through to the root element. |
27
+ | `...rest` | `Omit<HTMLAttributes<HTMLDivElement>, 'children'>` | | Every native div prop except children, because a skeleton draws its own shapes. |
28
+ | `label (SkeletonGroup)` | `string` | `'Loading…'` | The hidden words a screen reader hears instead of the shapes. |
29
+ | `className (SkeletonGroup)` | `string` | | Lay the placeholders out here, the way the real content will be laid out. |
30
+ | `ref (SkeletonGroup)` | `Ref<HTMLDivElement>` | | Passed through to the container. |
31
+ | `...rest (SkeletonGroup)` | `HTMLAttributes<HTMLDivElement>` | | Every native div prop. |
32
+
33
+ ## Examples
34
+
35
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
36
+
37
+ ### Shapes: text lines, a block, the Avatar footprints
38
+
39
+ ```tsx
40
+ <div className="flex w-full flex-col gap-6">
41
+ <Skeleton shape="text" lines={3} className="max-w-md" />
42
+ <Skeleton shape="block" className="h-24 w-full max-w-md" />
43
+ <div className="flex items-center gap-3">
44
+ <Skeleton shape="avatar" size="sm" />
45
+ <Skeleton shape="avatar" size="md" />
46
+ <Skeleton shape="avatar" size="lg" />
47
+ </div>
48
+ </div>
49
+ ```
50
+
51
+ ### A card loading: image, title, two lines of copy
52
+
53
+ ```tsx
54
+ <SkeletonGroup label="Loading replicas" className="grid w-full max-w-md gap-4 sm:grid-cols-2">
55
+ {[0, 1].map(i => (
56
+ <Card key={i}>
57
+ <CardBody className="flex flex-col gap-4">
58
+ <Skeleton shape="block" className="aspect-[4/3] w-full" />
59
+ <Skeleton shape="text" className="w-2/3" />
60
+ <Skeleton shape="text" lines={2} />
61
+ </CardBody>
62
+ </Card>
63
+ ))}
64
+ </SkeletonGroup>
65
+ ```
66
+
67
+ ### Table rows loading
68
+
69
+ ```tsx
70
+ <SkeletonGroup label="Loading orders" className="w-full max-w-xl">
71
+ <div className="flex flex-col divide-y divide-[var(--color-border-hairline)]">
72
+ {[0, 1, 2].map(i => (
73
+ <div key={i} className="grid grid-cols-[1fr_2fr_1fr] items-center gap-4 py-3">
74
+ <Skeleton shape="text" />
75
+ <Skeleton shape="text" className="w-4/5" />
76
+ <Skeleton shape="text" className="w-1/2 justify-self-end" />
77
+ </div>
78
+ ))}
79
+ </div>
80
+ </SkeletonGroup>
81
+ ```
82
+
83
+ ### A profile row loading
84
+
85
+ ```tsx
86
+ <SkeletonGroup label="Loading the squad roster" className="flex w-full max-w-sm items-center gap-3">
87
+ <Skeleton shape="avatar" size="md" />
88
+ <div className="flex flex-1 flex-col gap-2">
89
+ <Skeleton shape="text" className="w-1/2" />
90
+ <Skeleton shape="text" className="w-1/3" />
91
+ </div>
92
+ </SkeletonGroup>
93
+ ```
94
+
95
+ ## Keyboard
96
+
97
+ Not focusable. No keyboard behaviour of its own.
98
+
99
+ ## Do
100
+
101
+ - Wrap one region’s placeholders in one SkeletonGroup, and remove it when the content arrives.
102
+ - Name what’s loading in the group label: Loading orders, not just Loading.
103
+ - Match the layout of the real content, so nothing jumps when it replaces the placeholder.
104
+ - Use shape="avatar" with the size the real Avatar will have.
105
+
106
+ ## Don't
107
+
108
+ - Don't round a skeleton or draw a circle. The system has square corners, and Avatar is a square too.
109
+ - Don't put a skeleton on a busy button or control. A busy Button takes loading.
110
+ - Don't nest one SkeletonGroup inside another. One busy region gets one announcement.
111
+ - Don't show a skeleton for a wait under a few hundred milliseconds. The flash is worse than the wait.
112
+
113
+ ## Common mistakes
114
+
115
+ Wrong:
116
+
117
+ ```tsx
118
+ <div><Skeleton lines={3} /></div>
119
+ ```
120
+
121
+ Right:
122
+
123
+ ```tsx
124
+ <SkeletonGroup label="Loading orders"><Skeleton lines={3} /></SkeletonGroup>
125
+ ```
126
+
127
+ 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.
128
+
129
+ ## Anatomy
130
+
131
+ - **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.
132
+ - **Text lines**: 12px (pixel) bars, 8px apart, full width. When there are several, the last one is three fifths wide.
133
+ - **Block**: Any rectangle, sized with className, with the 8px cut corner.
134
+ - **Avatar**: An Avatar’s exact footprint: 24, 36 or 48px, with the corner cut at md and lg only.
135
+ - **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,114 @@
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
+ 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` | | Required. Applied to each thumb. A single thumb gets it as-is, and a range adds "minimum" / "maximum" to the end. |
23
+ | `value` | `number[]` | | The value(s), when you control them from outside. One number makes a single slider; two make a range. |
24
+ | `defaultValue` | `number[]` | | The starting value(s), when the slider manages them itself (uncontrolled). |
25
+ | `onValueChange` | `(value: number[]) => void` | | Fires as the thumb moves. |
26
+ | `min` | `number` | `0` | The lowest value. |
27
+ | `max` | `number` | `100` | The highest value. |
28
+ | `step` | `number` | `1` | The increment for both snapping while dragging and the arrow keys. |
29
+ | `disabled` | `boolean` | | Takes it out of the tab order and blocks input. |
30
+ | `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. |
31
+ | `showValue` | `boolean` | `false` | Shows the current value(s) as a readout. |
32
+ | `className` | `string` | | Merged with the outer wrapper's classes. Later Tailwind utilities (Tailwind is the CSS framework the system uses) win. |
33
+ | `ref` | `Ref<HTMLSpanElement>` | | Passed through to the Radix Slider root. Radix is the library of accessible building blocks this sits on. |
34
+ | `...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. |
35
+
36
+ ## Examples
37
+
38
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
39
+
40
+ ### Single value
41
+
42
+ ```tsx
43
+ function SingleValueLive() {
44
+ const [weight, setWeight] = useState([8])
45
+ return (
46
+ <div className="w-full max-w-sm">
47
+ <Slider
48
+ label="Kit weight"
49
+ value={weight}
50
+ onValueChange={setWeight}
51
+ max={20}
52
+ formatValue={v => `${v} kg`}
53
+ showValue
54
+ />
55
+ </div>
56
+ )
57
+ }
58
+
59
+ <SingleValueLive />
60
+ ```
61
+
62
+ ### Range
63
+
64
+ ```tsx
65
+ <div className="w-full max-w-sm">
66
+ <Slider label="Weight range" defaultValue={[4, 12]} max={20} />
67
+ </div>
68
+ ```
69
+
70
+ ### Stepped
71
+
72
+ ```tsx
73
+ <div className="w-full max-w-sm">
74
+ <Slider label="BB bottles" defaultValue={[5]} max={20} step={5} />
75
+ </div>
76
+ ```
77
+
78
+ ### Disabled
79
+
80
+ ```tsx
81
+ <div className="w-full max-w-sm">
82
+ <Slider label="Locked ration" defaultValue={[8]} max={20} disabled />
83
+ </div>
84
+ ```
85
+
86
+ ## Keyboard
87
+
88
+ | Keys | Result |
89
+ |---|---|
90
+ | `→` `↑` | Goes up by one step. |
91
+ | `←` `↓` | Goes down by one step. |
92
+ | `Page Up` | Goes up by a bigger jump. |
93
+ | `Page Down` | Goes down by a bigger jump. |
94
+ | `Home` | Jumps to the minimum. |
95
+ | `End` | Jumps to the maximum. |
96
+
97
+ ## Do
98
+
99
+ - Use formatValue whenever the number has a unit.
100
+ - Show the value. A slider with no readout asks people to guess.
101
+ - Put a number input beside it when the exact value matters.
102
+
103
+ ## Don't
104
+
105
+ - Don't use a slider where a precise value is required and nothing else lets people enter one.
106
+ - Don't shrink the thumb below 24×24 of hit area.
107
+ - Don't rely on dragging alone. Every slider here works fully from the keyboard, and it should stay that way.
108
+
109
+ ## Anatomy
110
+
111
+ - **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.
112
+ - **Track**: 4px tall, sunken, no border.
113
+ - **Range**: The filled span, in accent.signal.indicator.
114
+ - **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,128 @@
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
+ 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
+ | `values` | `readonly number[]` | | Required. Oldest first. Non-finite values (such as NaN or Infinity) are dropped. |
23
+ | `label` | `string` | | Required. The chart as one sentence: "Temperature, last hour: 3.1 to 4.2 °C, rising". |
24
+ | `tone` | `'neutral' \| 'signal' \| 'ok' \| 'warning' \| 'hostile' \| 'info'` | `'neutral'` | The line's colour. Each tone means what it means on Badge. |
25
+ | `min` | `number` | | A fixed bottom for the scale. Without it, the scale follows the data (and the band). |
26
+ | `max` | `number` | | A fixed top for the scale. Without it, the scale follows the data (and the band). |
27
+ | `band` | `{ from: number; to: number }` | | A range drawn behind the line. |
28
+ | `showLast` | `boolean` | `false` | Puts a square on the last value. |
29
+ | `width` | `number` | `96` | In CSS px, the pixel unit of Cascading Style Sheets. className="w-full" overrides it. |
30
+ | `height` | `number` | `24` | In CSS px. |
31
+ | `className` | `string` | | Goes on the frame. |
32
+ | `ref` | `Ref<HTMLSpanElement>` | | Forwarded to the frame. |
33
+
34
+ ## Types and helpers
35
+
36
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
37
+
38
+ ```ts
39
+ export type SparklineTone = 'neutral' | 'signal' | 'ok' | 'warning' | 'hostile' | 'info'
40
+ ```
41
+
42
+ ## Examples
43
+
44
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
45
+
46
+ ### Beside the value it trends
47
+
48
+ ```tsx
49
+ <div className="flex items-center gap-3">
50
+ <span className="font-mono text-sm tabular-nums">4.2 °C</span>
51
+ <Sparkline
52
+ values={[3.1, 3.2, 3.2, 3.4, 3.3, 3.6, 3.8, 3.7, 4.0, 4.2]}
53
+ label="Cold room 2, last hour: 3.1 to 4.2 °C, rising"
54
+ band={{ from: 2, to: 8 }}
55
+ showLast
56
+ />
57
+ </div>
58
+ <div className="flex items-center gap-3">
59
+ <span className="font-mono text-sm tabular-nums">412</span>
60
+ <Sparkline
61
+ values={[380, 365, 402, 398, 371, 420, 412]}
62
+ label="Orders, last 7 days: 365 to 420, steady"
63
+ tone="signal"
64
+ showLast
65
+ />
66
+ </div>
67
+ ```
68
+
69
+ ### Tones
70
+
71
+ ```tsx
72
+ <Sparkline values={[5, 6, 5, 7, 6, 8]} label="Neutral trend, rising" />
73
+ <Sparkline values={[5, 6, 5, 7, 6, 8]} label="Healthy trend, rising" tone="ok" />
74
+ <Sparkline values={[5, 6, 5, 7, 6, 8]} label="Degraded trend, rising" tone="warning" />
75
+ <Sparkline values={[5, 6, 5, 7, 6, 8]} label="Failing trend, rising" tone="hostile" />
76
+ <Sparkline values={[5, 6, 5, 7, 6, 8]} label="Informational trend, rising" tone="info" />
77
+ ```
78
+
79
+ ### Stretched to its container, with a safe band from 2 to 8 °C
80
+
81
+ ```tsx
82
+ <div className="w-full max-w-md">
83
+ <Sparkline
84
+ 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]}
85
+ label="Cold room 3, last 12 hours: 4.0 to 9.1 °C, above the safe band for 3 hours, now back inside it"
86
+ tone="warning"
87
+ band={{ from: 2, to: 8 }}
88
+ min={0}
89
+ max={10}
90
+ height={40}
91
+ className="w-full"
92
+ showLast
93
+ />
94
+ </div>
95
+ ```
96
+
97
+ ### No values, one value, a flat run
98
+
99
+ ```tsx
100
+ <Sparkline values={[]} label="No readings yet" />
101
+ <Sparkline values={[12]} label="One reading: 12" showLast />
102
+ <Sparkline values={[7, 7, 7, 7]} label="Flat at 7" showLast />
103
+ ```
104
+
105
+ ## Keyboard
106
+
107
+ Not focusable. No keyboard behaviour of its own.
108
+
109
+ ## Do
110
+
111
+ - Write the label as the reading a person takes from the shape: the range and the direction.
112
+ - Fix min and max when several sparklines sit side by side and have to share a scale.
113
+ - Put the current value beside it in text. The line shows the trend, not the number.
114
+ - Use a tone for what the trend means (hostile for out of range), and say it in text too.
115
+
116
+ ## Don't
117
+
118
+ - Don't use a sparkline where someone has to read values off it. That's a job for ChartPanel.
119
+ - Don't leave the label as a title like "Temperature". It's the only thing a screen reader hears.
120
+ - Don't colour every sparkline on a page. Neutral is the default for a reason.
121
+
122
+ ## Anatomy
123
+
124
+ - **Frame**: An inline box. It's 96 x 24 by default, or the container's width with className="w-full".
125
+ - **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.
126
+ - **Band**: Optional. A hairline-filled range behind the line, such as a safe range.
127
+ - **Line**: 1.5px (pixels) in the tone colour. The stroke doesn't get thicker when the frame stretches.
128
+ - **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,98 @@
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
+ 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
+ | `variant` | `'ring' \| 'conveyor'` | `'ring'` | The rotating ring, or the block conveyor. |
23
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Ring: a 14px, 20px or 24px (pixel) box. Conveyor: 12px, 14px or 16px glyphs. |
24
+ | `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. |
25
+ | `className` | `string` | | Merged with the component's classes. Later Tailwind utilities (the small styling classes this system uses) win. |
26
+ | `ref` | `Ref<HTMLSpanElement>` | | Forwarded to the span element. |
27
+ | `...rest` | `Omit<HTMLAttributes<HTMLSpanElement>, '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. |
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 const spinnerVariants = cva(
35
+ // The arc takes `currentColor`, so a caller can recolour it with one text utility: Button does
36
+ // exactly that, so the arc matches whatever ink the button label is set in.
37
+ 'inline-block shrink-0 text-[var(--color-accent-signal-indicator)]',
38
+ {
39
+ variants: {
40
+ size: { sm: 'w-3.5 h-3.5', md: 'w-5 h-5', lg: 'w-6 h-6' },
41
+ },
42
+ defaultVariants: { size: 'md' },
43
+ }
44
+ )
45
+ ```
46
+
47
+ ## Examples
48
+
49
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
50
+
51
+ ### Ring, three sizes
52
+
53
+ ```tsx
54
+ <Spinner size="sm" label="Loading field kits" />
55
+ <Spinner size="md" label="Loading field kits" />
56
+ <Spinner size="lg" label="Loading field kits" />
57
+ ```
58
+
59
+ ### Conveyor, three sizes
60
+
61
+ ```tsx
62
+ <Spinner variant="conveyor" size="sm" label="Syncing manifest" />
63
+ <Spinner variant="conveyor" size="md" label="Syncing manifest" />
64
+ <Spinner variant="conveyor" size="lg" label="Syncing manifest" />
65
+ ```
66
+
67
+ ### Decorative, inside a control
68
+
69
+ ```tsx
70
+ <span className="inline-flex items-center gap-2 border border-[var(--color-border-hairline-strong)] px-3 py-2">
71
+ <Spinner aria-hidden="true" />
72
+ <span className="text-sm">Dispatching…</span>
73
+ </span>
74
+ ```
75
+
76
+ ## Keyboard
77
+
78
+ Not focusable. No keyboard behaviour of its own.
79
+
80
+ ## Do
81
+
82
+ - Pass label whenever the spinner is the only sign that something's happening.
83
+ - 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.
84
+ - Name what's loading, not the fact of loading: ‘Loading field kits’, not ‘Loading’.
85
+
86
+ ## Don't
87
+
88
+ - Don't use it for progress with a known amount. That's Progress.
89
+ - 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.
90
+ - Don't scale it past lg. A page-level wait needs a skeleton, not a bigger spinner.
91
+ - 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.
92
+
93
+ ## Anatomy
94
+
95
+ - **span**: The root. It gets role="status" only when it's labelled.
96
+ - **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.indicator by default.
97
+ - **Blocks (conveyor)**: Three block glyphs, from full to half shade, running over a ten-cell track in border.control.
98
+ - **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.