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,212 @@
1
+ # Pagination
2
+
3
+ > Moves through a long list one page at a time. The current page gets the signal fill. Where there are too many pages to fit, a gap stands in for the run that's hidden.
4
+
5
+ - **Best for:** Moving through a long list or table one page at a time.
6
+ - **Not for:** Steps in a form: use Stepper. Fewer than two pages: don't render anything.
7
+ - Molecule. Source: `src/components/molecules/pagination/Pagination.tsx`
8
+ - Rendered docs page: `/components/pagination`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Pagination, paginationRange } from 'src/components/molecules/pagination/Pagination'
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
+ | `page` | `number` | | Counts from 1. A number outside the range is pulled back into it. |
23
+ | `pageCount` | `number` | | Below 1, nothing renders. |
24
+ | `onPageChange` | `(page: number) => void` | | It's never called with the page you're already on. |
25
+ | `variant` | `'numbered' \| 'compact' \| 'select'` | `'numbered'` | What sits between Previous and Next: the page buttons, a 'Page 3 of 20' readout (which screen readers announce politely, without interrupting, when it changes), or a page Select. Only use 'select' up to a few dozen pages. |
26
+ | `siblings` | `number` | `1` | How many pages show on each side of the current page, for 'numbered'. |
27
+ | `showFirstLast` | `boolean` | `false` | Adds First page and Last page icon buttons at the ends. Like Previous and Next, they go aria-disabled at the first and last page. |
28
+ | `showLabels` | `boolean` | `false` | Shows Previous and Next as text buttons ('Previous', 'Next') with their chevrons, instead of icons alone. |
29
+ | `showJump` | `boolean` | `false` | Adds a 'Go to page' field. Enter or Go jumps there, and a number past either end goes to that end. |
30
+ | `totalItems` | `number` | | Together with pageSize, it shows the range on view ('21–40 of 400') at the start and lays the control out as a table footer. |
31
+ | `pageSize` | `number` | | Items per page, used for the range text and the rows-per-page Select. |
32
+ | `onPageSizeChange` | `(pageSize: number) => void` | | Together with pageSize, it shows a rows-per-page Select. When it fires, reset page to 1: the old page may not exist any more. |
33
+ | `pageSizeOptions` | `number[]` | `[10, 20, 50, 100]` | The rows-per-page choices. |
34
+ | `label` | `string` | `'Pagination'` | The name of the nav landmark (a region screen readers can jump to). |
35
+ | `className` | `string` | | Added to the nav element. |
36
+ | `ref` | `Ref<HTMLElement>` | | Passed through to the nav DOM node (the element in the page). |
37
+
38
+ ## Types and helpers
39
+
40
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
41
+
42
+ ```ts
43
+ /** Pure. 1-based. Always includes page 1, the last page, the current page and `siblings` pages
44
+ * either side of it. A gap stands for two or more hidden pages; a single hidden page is shown
45
+ * instead, because a gap that hides one number costs the same space as the number. */
46
+ export function paginationRange(page: number, pageCount: number, siblings = 1): (number | 'gap')[]
47
+ ```
48
+
49
+ ## Examples
50
+
51
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
52
+
53
+ ### Few pages
54
+
55
+ ```tsx
56
+ <Pagination page={2} pageCount={5} onPageChange={() => {}} label="Pagination, few pages" />
57
+ ```
58
+
59
+ ### Many pages
60
+
61
+ ```tsx
62
+ function ManyPagesExample() {
63
+ const [page, setPage] = useState(1)
64
+ const perPage = 20
65
+ const total = 400
66
+ const from = (page - 1) * perPage + 1
67
+ const to = Math.min(page * perPage, total)
68
+
69
+ return (
70
+ <div>
71
+ <Pagination page={page} pageCount={20} onPageChange={setPage} label="Pagination, many pages" />
72
+ <p className="font-mono text-xs text-[var(--color-text-muted)] mt-3">
73
+ Showing {from} to {to} of {total} depots
74
+ </p>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ <ManyPagesExample />
80
+ ```
81
+
82
+ ### Wider context: two page numbers on each side of the current page
83
+
84
+ ```tsx
85
+ <Pagination
86
+ page={10}
87
+ pageCount={20}
88
+ siblings={2}
89
+ onPageChange={() => {}}
90
+ label="Pagination, wider context"
91
+ />
92
+ ```
93
+
94
+ ### First and last
95
+
96
+ ```tsx
97
+ function Live(props: Omit<Parameters<typeof Pagination>[0], 'page' | 'onPageChange'>) {
98
+ const [page, setPage] = useState(4)
99
+ return <Pagination {...props} page={page} onPageChange={setPage} />
100
+ }
101
+
102
+ <Live pageCount={20} showFirstLast label="Pagination, first and last" />
103
+ ```
104
+
105
+ ### With labels
106
+
107
+ ```tsx
108
+ function Live(props: Omit<Parameters<typeof Pagination>[0], 'page' | 'onPageChange'>) {
109
+ const [page, setPage] = useState(4)
110
+ return <Pagination {...props} page={page} onPageChange={setPage} />
111
+ }
112
+
113
+ <Live pageCount={20} showLabels label="Pagination, with labels" />
114
+ ```
115
+
116
+ ### Compact
117
+
118
+ ```tsx
119
+ function Live(props: Omit<Parameters<typeof Pagination>[0], 'page' | 'onPageChange'>) {
120
+ const [page, setPage] = useState(4)
121
+ return <Pagination {...props} page={page} onPageChange={setPage} />
122
+ }
123
+
124
+ <Live variant="compact" pageCount={20} showFirstLast label="Pagination, compact" />
125
+ ```
126
+
127
+ ### Page select
128
+
129
+ ```tsx
130
+ function Live(props: Omit<Parameters<typeof Pagination>[0], 'page' | 'onPageChange'>) {
131
+ const [page, setPage] = useState(4)
132
+ return <Pagination {...props} page={page} onPageChange={setPage} />
133
+ }
134
+
135
+ <Live variant="select" pageCount={12} label="Pagination, page select" />
136
+ ```
137
+
138
+ ### Go to page
139
+
140
+ ```tsx
141
+ function Live(props: Omit<Parameters<typeof Pagination>[0], 'page' | 'onPageChange'>) {
142
+ const [page, setPage] = useState(4)
143
+ return <Pagination {...props} page={page} onPageChange={setPage} />
144
+ }
145
+
146
+ <Live pageCount={48} showJump label="Pagination, go to page" />
147
+ ```
148
+
149
+ ### Table footer
150
+
151
+ ```tsx
152
+ function TableFooterExample() {
153
+ const total = 397
154
+ const [page, setPage] = useState(2)
155
+ const [pageSize, setPageSize] = useState(20)
156
+ return (
157
+ <div className="w-full">
158
+ <Pagination
159
+ page={page}
160
+ pageCount={Math.ceil(total / pageSize)}
161
+ onPageChange={setPage}
162
+ totalItems={total}
163
+ pageSize={pageSize}
164
+ onPageSizeChange={size => {
165
+ setPageSize(size)
166
+ setPage(1)
167
+ }}
168
+ showFirstLast
169
+ label="Pagination, table footer"
170
+ />
171
+ </div>
172
+ )
173
+ }
174
+
175
+ <TableFooterExample />
176
+ ```
177
+
178
+ ## Keyboard
179
+
180
+ | Keys | Result |
181
+ |---|---|
182
+ | `Tab` | Moves across Previous, each page, and Next. |
183
+ | `Enter` `Space` | Goes to that page. |
184
+ | `Enter` | In the Go to page field, jumps to the page you typed. |
185
+
186
+ ## Do
187
+
188
+ - Say how many results there are somewhere near it: 'Showing 41 to 60 of 312'.
189
+ - After a change, move focus to the top of the new page of results.
190
+ - Keep siblings at 1 on narrow layouts.
191
+ - Under a data table, pass totalItems and pageSize. The footer then says what's on view, so you don't need a separate line of text.
192
+ - Use variant 'compact' where space is tight, such as a card or a phone layout.
193
+
194
+ ## Don't
195
+
196
+ - Don't paginate fewer than two pages; render nothing.
197
+ - Don't disable the current page; Tab would skip it.
198
+ - Don't use pagination for steps in a form.
199
+ - Don't use variant 'select' for hundreds of pages. A list that long needs showJump instead.
200
+ - Don't round the buttons into circles or pills; the square and the cut corner are this system's shape.
201
+
202
+ ## Anatomy
203
+
204
+ - **nav**: The navigation region, named 'Pagination'.
205
+ - **Previous / Next**: Ghost IconButtons (icon-only buttons with no fill). At the first and last page they're aria-disabled rather than disabled. ARIA (Accessible Rich Internet Applications) attributes tell screen readers about a control, and aria-disabled only marks the button as unavailable. It stays a real stop when you press Tab, so keyboard focus never drops to the page body when one stops working.
206
+ - **Page**: A small Button named 'Page n', with tabular figures (digits that all take the same width).
207
+ - **Current page**: The primary fill, with aria-current=page so screen readers say it's the current one. It isn't disabled, so Tab still reaches it.
208
+ - **Gap**: An ellipsis that stands for two or more hidden pages. It's aria-hidden, so screen readers skip it.
209
+ - **First / Last**: Optional (showFirstLast). Ghost IconButtons, aria-disabled at the first and last page.
210
+ - **Readout or page Select**: Takes the place of the page buttons when variant is 'compact' or 'select'.
211
+ - **Range and rows per page**: Optional (totalItems, pageSize, onPageSizeChange). These turn the control into a table footer: the range at the start, the controls at the end.
212
+ - **Go to page**: Optional (showJump). A small number Input with a visible label, and a Go button.
@@ -0,0 +1,172 @@
1
+ # ProductCard
2
+
3
+ > One product as one link. Clicking anywhere on the card opens it, Add to cart stays a separate control, and a grid of cards adapts to the space it's given.
4
+
5
+ - **Best for:** One product in a shop grid, where the whole card opens it and Add to cart sits on it.
6
+ - **Not for:** A general content panel: use Card.
7
+ - **Also on this page:** `ProductGrid`
8
+ - Organism. Source: `src/components/organisms/product-card/ProductCard.tsx`
9
+ - Rendered docs page: `/components/product-card`
10
+
11
+ ## Import
12
+
13
+ ```tsx
14
+ import { ProductCard, ProductGrid } from 'src/components/organisms/product-card/ProductCard'
15
+ ```
16
+
17
+ 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.
18
+
19
+ ## Props
20
+
21
+ | Prop | Type | Default | Description |
22
+ |---|---|---|---|
23
+ | `product (ProductCard)` | `Product` | | Required. id, name, href and price (in the currency's minor unit, so cents for EUR). rating, image, badge and soldOut are optional. |
24
+ | `onAddToCart` | `(product: Product) => void` | | Draws Add to cart. Leave it out for a card that only links to the product. |
25
+ | `headingLevel` | `2 \| 3 \| 4` | `3` | Pick the level that fits the page's heading outline. |
26
+ | `currency` | `string` | `'EUR'` | Passed to formatMoney. |
27
+ | `locale` | `string` | `'en-IE'` | Passed to formatMoney. |
28
+ | `renderLink` | `(props: LinkRenderProps) => ReactNode` | `a plain anchor` | Draws the name's link. It must pass className through, because that class carries the stretched click area and the focus ring. |
29
+ | `renderMedia` | `(product: Product) => ReactNode` | `the product's image, 4:3, cover` | Draws the picture instead of the image or the placeholder, for example a staged cutout. Keep the 4:3 box and keep it decorative. The badge still sits over it. |
30
+ | `className (ProductCard)` | `string` | | Merged with the root classes. Later Tailwind utilities win. |
31
+ | `ref (ProductCard)` | `Ref<HTMLElement>` | | Points at the article element. |
32
+ | `products (Grid)` | `Product[]` | | Required. |
33
+ | `label (Grid)` | `string` | | Required. The list's accessible name, the name a screen reader announces for it. |
34
+ | `emptyMessage (Grid)` | `string` | `'No products match.'` | Announced by a status message that's always on the page when products is empty. The list itself keeps rendering (so its ref stays stable), but it's hidden while there's nothing in it. |
35
+ | `className (Grid)` | `string` | | Merged with the list's own classes. Later Tailwind utilities win. |
36
+ | `ref (Grid)` | `Ref<HTMLUListElement>` | | Points at the list element. |
37
+
38
+ ## Types and helpers
39
+
40
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
41
+
42
+ ```ts
43
+ export type Product = {
44
+ id: string
45
+ name: string
46
+ href: string
47
+ /** In the currency's minor unit (cents for EUR). */
48
+ price: number
49
+ /** The price before a sale, in the currency's minor unit (cents for EUR): struck through, and
50
+ * read as "was". */
51
+ compareAtPrice?: number
52
+ rating?: { value: number; count: number }
53
+ /** Without one, the card draws a decorative placeholder. `alt` defaults to '': the title names
54
+ * the product. */
55
+ image?: { src: string; alt?: string }
56
+ /** A short solid tag, e.g. "New" or "Sale". */
57
+ badge?: string
58
+ soldOut?: boolean
59
+ }
60
+ ```
61
+
62
+ ## Examples
63
+
64
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
65
+
66
+ ### One product
67
+
68
+ ```tsx
69
+ const REPLICA: Product = {
70
+ id: 'kestrel-k',
71
+ name: 'Kestrel K compact replica',
72
+ href: '#kestrel-k',
73
+ price: 12900,
74
+ compareAtPrice: 14900,
75
+ rating: { value: 4.5, count: 128 },
76
+ badge: 'Sale',
77
+ }
78
+
79
+ function OneProductExample() {
80
+ const { toast } = useToast()
81
+ return (
82
+ <div className="max-w-xs">
83
+ <ProductCard
84
+ product={REPLICA}
85
+ headingLevel={4}
86
+ onAddToCart={product => toast({ title: 'Added to cart', description: product.name, tone: 'ok' })}
87
+ />
88
+ </div>
89
+ )
90
+ }
91
+
92
+ <OneProductExample />
93
+ ```
94
+
95
+ ### A grid
96
+
97
+ ```tsx
98
+ const RANGE: Product[] = [
99
+ { id: 'kestrel-c', name: 'Kestrel C carbine replica', href: '#kestrel-c', price: 13900, rating: { value: 4.5, count: 212 } },
100
+ { id: 'prism-holo-sight', name: 'Prism holo sight', href: '#prism-holo-sight', price: 4900, badge: 'New' },
101
+ { id: 'speed-loader', name: 'Speed loader, 150 BBs', href: '#speed-loader', price: 790, rating: { value: 4, count: 87 } },
102
+ { id: 'kestrel-l', name: 'Kestrel L rifle replica', href: '#kestrel-l', price: 15900, soldOut: true },
103
+ { id: 'bbs-028', name: 'Precision BBs 0.28 g, 2,700', href: '#bbs-028', price: 590 },
104
+ { id: 'field-goggles', name: 'Field goggles', href: '#field-goggles', price: 3400, compareAtPrice: 3900, badge: 'Sale' },
105
+ ]
106
+
107
+ function AGridExample() {
108
+ const { toast } = useToast()
109
+ return (
110
+ <ProductGrid
111
+ label="Hexline range"
112
+ products={RANGE}
113
+ headingLevel={4}
114
+ onAddToCart={product => toast({ title: 'Added to cart', description: product.name, tone: 'ok' })}
115
+ />
116
+ )
117
+ }
118
+
119
+ <AGridExample />
120
+ ```
121
+
122
+ ### A custom picture through renderMedia
123
+
124
+ ```tsx
125
+ const STAGED: Product = { id: 'harrier-lr', name: 'Harrier LR bolt-action replica', href: '#harrier-lr', price: 17900, rating: { value: 4.5, count: 37 } }
126
+
127
+ <div className="max-w-xs">
128
+ <ProductCard
129
+ product={STAGED}
130
+ headingLevel={4}
131
+ renderMedia={product => (
132
+ <div aria-hidden="true" className="flex aspect-[4/3] items-center justify-center bg-[var(--color-surface-sunken)] p-4">
133
+ <img src={`/media/products/cutout/${product.id}-640.webp`} alt="" loading="lazy" className="max-h-full w-full object-contain" />
134
+ </div>
135
+ )}
136
+ />
137
+ </div>
138
+ ```
139
+
140
+ ### Nothing matches
141
+
142
+ ```tsx
143
+ <ProductGrid label="Hexline range, filtered" products={[]} emptyMessage="No products match these filters." />
144
+ ```
145
+
146
+ ## Keyboard
147
+
148
+ | Keys | Result |
149
+ |---|---|
150
+ | `Tab` | Moves to the product's link, then to Add to cart, then to the next card. |
151
+ | `Enter` | On the link, opens the product. On Add to cart, adds it. |
152
+
153
+ ## Do
154
+
155
+ - Give the grid a label that says what it holds.
156
+ - Store prices in the currency's minor unit (cents for EUR). The card formats them.
157
+ - Pass headingLevel so the names fit the page's heading outline.
158
+
159
+ ## Don't
160
+
161
+ - Don't put another link or button in the card body. Only Add to cart sits above the stretched link.
162
+ - Don't colour the price. It's text.primary.
163
+ - Don't write alt text (the image description for screen readers) that repeats the name.
164
+
165
+ ## Anatomy
166
+
167
+ - **Image**: Optional. Without one, there's a decorative placeholder. renderMedia can draw something else in the same box.
168
+ - **Badge**: Optional. A short solid tag over the corner of the picture, like New or Sale. It sits outside the text flow, so names line up along a row of cards.
169
+ - **Name**: The heading, and the link. Its click area and its focus ring (the outline keyboard users see) cover the whole card.
170
+ - **Rating**: Optional. Screen readers hear the stars as 'rated 4.5 out of 5', and the review count is plain text.
171
+ - **Price**: In text.primary, with tabular (equal-width) figures. A sale adds the old price struck through, which screen readers hear as "was".
172
+ - **Add to cart**: Sits above the link as its own target, and it's named after the product.
@@ -0,0 +1,101 @@
1
+ # Progress
2
+
3
+ > A progress bar that's either determinate (it knows how far along the task is) or indeterminate (it doesn't). The readout is real text in the DOM (the Document Object Model, the live page structure the browser builds) from the very first render. That's motion token M3's rule, and it means a screen reader, the software that reads the page aloud for blind and low-vision people, never hears a number roll.
4
+
5
+ - **Best for:** Showing how far a measurable task has got. Switch to indeterminate once you no longer know how much is left.
6
+ - **Not for:** A wait with no measurable work. Use Spinner.
7
+ - Atom. Source: `src/components/atoms/progress/Progress.tsx`
8
+ - Rendered docs page: `/components/progress`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Progress } from 'src/components/atoms/progress/Progress'
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 bar’s accessible name. |
23
+ | `value` | `number \| null` | | null means indeterminate. The value is clamped to [0, max], so an out-of-range value shows as complete instead of overflowing the track. |
24
+ | `max` | `number` | `100` | The value at which the bar is complete. |
25
+ | `showValue` | `boolean` | `false` | Shows the percentage (or "In progress") as a readout. |
26
+ | `tone` | `'signal' \| 'ok' \| 'warning' \| 'hostile'` | `'signal'` | The fill colour. signal paints accent.signal.indicator to match Slider. On the track in light theme that’s a 2.15:1 contrast ratio, below the 3:1 a bar needs when it’s the only sign of progress. So set showValue there. |
27
+ | `className` | `string` | | Merged with the outer wrapper’s classes. Later Tailwind utilities (Tailwind is the CSS framework the system is styled with) win. |
28
+ | `ref` | `Ref<HTMLDivElement>` | | Passed through to the Radix Progress root, which is the bar itself. Radix is the library of accessible building blocks this component sits on. |
29
+ | `...rest` | `Omit<HTMLAttributes<HTMLDivElement>, 'children'>` | | Every native div prop except children, spread onto the outer wrapper, so a molecule can attach a data-testid there. children is typed away because this component draws its own label row and track, and a children prop would be thrown away without any warning. |
30
+
31
+ ## Examples
32
+
33
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
34
+
35
+ ### Determinate
36
+
37
+ ```tsx
38
+ <div className="w-full max-w-sm">
39
+ <Progress label="Uploading manifest" value={40} />
40
+ </div>
41
+ ```
42
+
43
+ ### With a readout
44
+
45
+ ```tsx
46
+ <div className="w-full max-w-sm">
47
+ <Progress label="Uploading manifest" value={65} showValue />
48
+ </div>
49
+ ```
50
+
51
+ ### Custom max
52
+
53
+ ```tsx
54
+ <div className="w-full max-w-sm">
55
+ <Progress label="Packing crates" value={3} max={12} showValue />
56
+ </div>
57
+ ```
58
+
59
+ ### Indeterminate
60
+
61
+ ```tsx
62
+ <div className="w-full max-w-sm">
63
+ <Progress label="Syncing manifest" value={null} showValue />
64
+ </div>
65
+ ```
66
+
67
+ ### Tones
68
+
69
+ ```tsx
70
+ <div className="flex flex-col gap-4 w-full max-w-sm">
71
+ <Progress label="Signal supply run" value={40} tone="signal" showValue />
72
+ <Progress label="BB reserves" value={82} tone="ok" showValue />
73
+ <Progress label="Gas reserves" value={22} tone="warning" showValue />
74
+ <Progress label="Battery reserves" value={6} tone="hostile" showValue />
75
+ </div>
76
+ ```
77
+
78
+ ## Keyboard
79
+
80
+ Not focusable. No keyboard behaviour of its own.
81
+
82
+ ## Do
83
+
84
+ - Name what’s progressing, not the act of progressing.
85
+ - Use showValue whenever the number itself is useful to the reader.
86
+ - Switch to null the moment you stop knowing how much is left. Don’t freeze at 90%.
87
+
88
+ ## Don't
89
+
90
+ - Don't animate the number up from zero and call that animated value the readout. The real value belongs in the DOM straight away.
91
+ - Don't use Progress for a wait with no measurable work. That's a Spinner.
92
+ - Don't put the padded readout into the accessible name. That's exactly why the pad is a separate aria-hidden span, and label is what Radix announces.
93
+ - Don't rely on the tone alone to say something failed. Say it in text.
94
+
95
+ ## Anatomy
96
+
97
+ - **Label**: Required. Set in mono (a monospaced font, where every character is the same width) and uppercase. It’s also the bar’s accessible name, the name assistive tech such as a screen reader announces for it.
98
+ - **Readout**: Optional, and the star of the row: 24px (pixels) Oxanium against an 11px label. It uses tabular figures, digits that all take the same width, so the number doesn’t jitter as it changes.
99
+ - **Leading zeros**: These are padding, not part of the value. They’re dimmed to numeral.pad and marked aria-hidden (an ARIA attribute, from Accessible Rich Internet Applications, that hides an element from assistive tech). So the readout keeps a fixed three-digit width, while a screen reader still hears 40%, not zero four zero percent.
100
+ - **Track**: 6px tall, a sunken surface, no border. It’s the same track as Slider.
101
+ - **Indicator**: It’s moved rather than resized. That way the browser animates a transform, which is cheap, instead of recalculating the layout.
@@ -0,0 +1,151 @@
1
+ # PromptInput
2
+
3
+ > The box where a person writes to an assistant. It grows to fit the text, has one Send/Stop button and can show suggestions. Enter sends, and Shift+Enter starts a new line.
4
+
5
+ - **Best for:** The message box under a ChatThread.
6
+ - **Not for:** A long field in a form: use Textarea inside a FormField.
7
+ - Molecule. Source: `src/components/molecules/prompt-input/PromptInput.tsx`
8
+ - Rendered docs page: `/components/prompt-input`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { PromptInput } from 'src/components/molecules/prompt-input/PromptInput'
14
+ ```
15
+
16
+ Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
17
+
18
+ ## Requires
19
+
20
+ - A parent that keeps the draft in state and clears it in onSubmit.
21
+
22
+ ## Props
23
+
24
+ | Prop | Type | Default | Description |
25
+ |---|---|---|---|
26
+ | `label` | `string` | | Required. Names the box. It's hidden on screen. |
27
+ | `value` | `string` | | Required. The draft. Controlled, meaning your page holds it. |
28
+ | `onValueChange` | `(value: string) => void` | | Required. |
29
+ | `onSubmit` | `(value: string) => void` | | Required. Fires on Enter, Send or a suggestion, and never with a blank draft. Clear the draft here. |
30
+ | `busy` | `boolean` | `false` | Send turns into Stop, and Enter sends nothing. |
31
+ | `onStop` | `() => void` | | Runs on Stop, and on Escape in the box while busy. |
32
+ | `suggestions` | `readonly string[]` | | Ready-made prompts shown as chips. Clicking one submits it. |
33
+ | `editableSuggestions` | `boolean` | `false` | Clicking a suggestion puts its text in the box to edit instead of sending it. Use it when a person will likely change the prompt first. The chips then stay usable while busy. |
34
+ | `placeholder` | `string` | | Shown in the empty box. |
35
+ | `disabled` | `boolean` | `false` | Nothing can be typed or sent. Say why somewhere nearby. |
36
+ | `hint` | `string` | `'Enter to send, Shift+Enter for a new line'` | The line under the box, which is also its description. |
37
+ | `className` | `string` | | Added to the form. |
38
+
39
+ ## Examples
40
+
41
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
42
+
43
+ ### With suggestions
44
+
45
+ ```tsx
46
+ function DraftExample() {
47
+ const [value, setValue] = useState('')
48
+ const [sent, setSent] = useState<string | null>(null)
49
+ return (
50
+ <div className="flex w-full max-w-xl flex-col gap-3">
51
+ <PromptInput
52
+ label="Message the assistant"
53
+ placeholder="Ask about stock, orders or depots"
54
+ value={value}
55
+ onValueChange={setValue}
56
+ onSubmit={text => {
57
+ setSent(text)
58
+ setValue('')
59
+ }}
60
+ suggestions={['Summarise stock at Depot 4', 'Which replicas are running low?']}
61
+ />
62
+ <p className="font-body text-sm text-[var(--color-text-secondary)]" aria-live="polite">
63
+ {sent === null ? 'Nothing sent yet.' : `Sent: ${sent}`}
64
+ </p>
65
+ </div>
66
+ )
67
+ }
68
+
69
+ <DraftExample />
70
+ ```
71
+
72
+ ### Busy: a Stop button takes the place of Send, and pressing Escape stops it too
73
+
74
+ ```tsx
75
+ function BusyExample() {
76
+ const [value, setValue] = useState('')
77
+ const [busy, setBusy] = useState(true)
78
+ return (
79
+ <div className="flex w-full max-w-xl flex-col gap-3">
80
+ <PromptInput
81
+ label="Message the assistant, while it works"
82
+ value={value}
83
+ onValueChange={setValue}
84
+ onSubmit={() => setValue('')}
85
+ busy={busy}
86
+ onStop={() => setBusy(false)}
87
+ />
88
+ {busy ? null : (
89
+ <Button size="sm" variant="ghost" wrapperClassName="self-start" onClick={() => setBusy(true)}>
90
+ Make it busy again
91
+ </Button>
92
+ )}
93
+ </div>
94
+ )
95
+ }
96
+
97
+ <BusyExample />
98
+ ```
99
+
100
+ ### Disabled
101
+
102
+ ```tsx
103
+ <div className="flex w-full max-w-xl flex-col gap-2">
104
+ <PromptInput label="Message the assistant (disabled)" value="" onValueChange={() => {}} onSubmit={() => {}} disabled />
105
+ <p className="font-body text-sm text-[var(--color-text-secondary)]">The assistant is offline until 14:00.</p>
106
+ </div>
107
+ ```
108
+
109
+ ## Keyboard
110
+
111
+ | Keys | Result |
112
+ |---|---|
113
+ | `Enter` | Sends the draft. Does nothing while busy, or while someone's composing characters with an IME (Input Method Editor, the tool used to type languages like Chinese or Japanese). |
114
+ | `Shift` `Enter` | Starts a new line. |
115
+ | `Escape` | While busy, stops the reply. |
116
+
117
+ ## Do
118
+
119
+ - Clear the draft in onSubmit, and set busy as soon as the request starts.
120
+ - Wire up onStop whenever the reply can be stopped. A reply that streams in with no Stop is a wait with no way out.
121
+ - Offer two to four suggestions in an empty thread, and drop them once the conversation has started.
122
+
123
+ ## Don't
124
+
125
+ - Don't disable the box while busy; a person may be writing the next message while the reply arrives.
126
+ - Don't wrap it in a FormField. Its label is hidden on purpose, and it brings its own hint.
127
+ - Don't move focus into it when the reply finishes. It never left, unless the person moved it.
128
+
129
+ ## Common mistakes
130
+
131
+ Wrong:
132
+
133
+ ```tsx
134
+ {busy ? <Button onClick={stop}>Stop</Button> : <Button type="submit">Send</Button>}
135
+ ```
136
+
137
+ Right:
138
+
139
+ ```tsx
140
+ <PromptInput busy={busy} onStop={stop} ... />
141
+ ```
142
+
143
+ Swapping two buttons drops keyboard focus to the page at the moment they switch. PromptInput keeps one button and changes its label and what it does.
144
+
145
+ ## Anatomy
146
+
147
+ - **Form**: Paints surface.base itself, as every form does.
148
+ - **Suggestions**: A group of chips above the box. Clicking one submits its text (disabled while busy), or with editableSuggestions puts it in the box to edit.
149
+ - **Box**: A Textarea with a label that's hidden on screen but read by screen readers. It grows from one line to about eight, then scrolls inside.
150
+ - **Send / Stop**: One button: a primary Send with an arrow, or, while busy, a secondary Stop with a square.
151
+ - **Hint**: One line under the box, which is also the box's description: "Enter to send, Shift+Enter for a new line".