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.
- package/AGENTS.md +23 -3
- package/README.md +11 -0
- package/division-ds-0.2.1.tgz +0 -0
- package/package.json +2 -2
- package/public/docs/components/accordion.md +207 -0
- package/public/docs/components/ai-label.md +90 -0
- package/public/docs/components/alert-dialog.md +292 -0
- package/public/docs/components/alert.md +175 -0
- package/public/docs/components/approval-request.md +148 -0
- package/public/docs/components/auth-form.md +132 -0
- package/public/docs/components/avatar-group.md +152 -0
- package/public/docs/components/avatar.md +179 -0
- package/public/docs/components/badge.md +178 -0
- package/public/docs/components/breadcrumbs.md +112 -0
- package/public/docs/components/button.md +204 -0
- package/public/docs/components/card.md +257 -0
- package/public/docs/components/carousel.md +130 -0
- package/public/docs/components/cart.md +172 -0
- package/public/docs/components/chart-panel.md +358 -0
- package/public/docs/components/chat-message.md +157 -0
- package/public/docs/components/chat-thread.md +244 -0
- package/public/docs/components/checkbox.md +149 -0
- package/public/docs/components/checkout-form.md +128 -0
- package/public/docs/components/command-palette.md +147 -0
- package/public/docs/components/data-table.md +401 -0
- package/public/docs/components/description-list.md +103 -0
- package/public/docs/components/dialog.md +197 -0
- package/public/docs/components/divider.md +97 -0
- package/public/docs/components/dropdown-menu.md +222 -0
- package/public/docs/components/empty-state.md +145 -0
- package/public/docs/components/footer.md +397 -0
- package/public/docs/components/form-field.md +216 -0
- package/public/docs/components/icon-button.md +127 -0
- package/public/docs/components/input.md +200 -0
- package/public/docs/components/kanban-board.md +300 -0
- package/public/docs/components/kbd.md +72 -0
- package/public/docs/components/link.md +98 -0
- package/public/docs/components/list-item.md +241 -0
- package/public/docs/components/loading-animation.md +72 -0
- package/public/docs/components/multi-select.md +202 -0
- package/public/docs/components/navbar.md +144 -0
- package/public/docs/components/newsletter-form.md +112 -0
- package/public/docs/components/page-header.md +151 -0
- package/public/docs/components/pagination.md +212 -0
- package/public/docs/components/product-card.md +172 -0
- package/public/docs/components/progress.md +101 -0
- package/public/docs/components/prompt-input.md +151 -0
- package/public/docs/components/quantity-stepper.md +140 -0
- package/public/docs/components/radio.md +177 -0
- package/public/docs/components/rating.md +106 -0
- package/public/docs/components/score-ring.md +95 -0
- package/public/docs/components/search.md +116 -0
- package/public/docs/components/section-heading.md +113 -0
- package/public/docs/components/segmented-control.md +126 -0
- package/public/docs/components/select.md +224 -0
- package/public/docs/components/sheet.md +145 -0
- package/public/docs/components/sidebar.md +193 -0
- package/public/docs/components/skeleton.md +135 -0
- package/public/docs/components/slider.md +114 -0
- package/public/docs/components/sparkline.md +128 -0
- package/public/docs/components/spinner.md +98 -0
- package/public/docs/components/stat.md +106 -0
- package/public/docs/components/status-tracker.md +145 -0
- package/public/docs/components/stepper.md +147 -0
- package/public/docs/components/switch.md +153 -0
- package/public/docs/components/tabs.md +180 -0
- package/public/docs/components/tag.md +105 -0
- package/public/docs/components/textarea.md +148 -0
- package/public/docs/components/timeline.md +152 -0
- package/public/docs/components/toast.md +194 -0
- package/public/docs/components/tool-call.md +107 -0
- package/public/docs/components/tooltip.md +108 -0
- package/public/manifest/components.json +198 -15
- package/public/manifest/tokens.json +75 -5
- package/scripts/division-ds.mjs +78 -42
- package/scripts/lib/agent-docs.mjs +80 -6
- package/scripts/lib/ai-copy.mjs +120 -0
- package/scripts/lib/button-spacing.mjs +5 -5
- package/scripts/lib/check-text.mjs +57 -0
- package/scripts/lib/contrast.mjs +111 -0
- package/scripts/lib/host-inline.mjs +59 -0
- package/scripts/lib/mcp-server.mjs +116 -0
- package/scripts/lib/mcp-tools.mjs +266 -0
- package/scripts/lib/props-validator.mjs +3 -2
- package/scripts/lib/rules-installer.mjs +42 -4
- package/scripts/lib/token-discipline.mjs +10 -3
- package/skills/division-ds/SKILL.md +90 -0
- package/skills/division-ds/commands/add.md +25 -0
- package/skills/division-ds/commands/build.md +46 -0
- package/skills/division-ds/commands/fix.md +28 -0
- package/skills/division-ds/commands/review.md +25 -0
- package/skills/division-ds/topics/ai.md +8 -0
- package/skills/division-ds/topics/colour.md +11 -0
- package/skills/division-ds/topics/feedback.md +9 -0
- package/skills/division-ds/topics/forms.md +22 -0
- package/skills/division-ds/topics/layout.md +36 -0
- package/skills/division-ds/topics/motion.md +10 -0
- 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".
|