division-ds 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +16 -2
- package/README.md +11 -0
- package/division-ds-0.2.0.tgz +0 -0
- package/package.json +2 -2
- package/public/docs/components/accordion.md +205 -0
- package/public/docs/components/ai-label.md +88 -0
- package/public/docs/components/alert-dialog.md +290 -0
- package/public/docs/components/alert.md +173 -0
- package/public/docs/components/approval-request.md +146 -0
- package/public/docs/components/auth-form.md +130 -0
- package/public/docs/components/avatar-group.md +150 -0
- package/public/docs/components/avatar.md +177 -0
- package/public/docs/components/badge.md +176 -0
- package/public/docs/components/breadcrumbs.md +110 -0
- package/public/docs/components/button.md +202 -0
- package/public/docs/components/card.md +255 -0
- package/public/docs/components/carousel.md +128 -0
- package/public/docs/components/cart.md +170 -0
- package/public/docs/components/chart-panel.md +356 -0
- package/public/docs/components/chat-message.md +154 -0
- package/public/docs/components/chat-thread.md +242 -0
- package/public/docs/components/checkbox.md +147 -0
- package/public/docs/components/checkout-form.md +126 -0
- package/public/docs/components/command-palette.md +145 -0
- package/public/docs/components/data-table.md +399 -0
- package/public/docs/components/description-list.md +101 -0
- package/public/docs/components/dialog.md +195 -0
- package/public/docs/components/divider.md +95 -0
- package/public/docs/components/dropdown-menu.md +220 -0
- package/public/docs/components/empty-state.md +143 -0
- package/public/docs/components/footer.md +395 -0
- package/public/docs/components/form-field.md +213 -0
- package/public/docs/components/icon-button.md +125 -0
- package/public/docs/components/input.md +198 -0
- package/public/docs/components/kanban-board.md +298 -0
- package/public/docs/components/kbd.md +70 -0
- package/public/docs/components/link.md +96 -0
- package/public/docs/components/list-item.md +238 -0
- package/public/docs/components/loading-animation.md +70 -0
- package/public/docs/components/multi-select.md +200 -0
- package/public/docs/components/navbar.md +142 -0
- package/public/docs/components/newsletter-form.md +110 -0
- package/public/docs/components/page-header.md +149 -0
- package/public/docs/components/pagination.md +210 -0
- package/public/docs/components/product-card.md +169 -0
- package/public/docs/components/progress.md +99 -0
- package/public/docs/components/prompt-input.md +149 -0
- package/public/docs/components/quantity-stepper.md +138 -0
- package/public/docs/components/radio.md +140 -0
- package/public/docs/components/rating.md +104 -0
- package/public/docs/components/score-ring.md +93 -0
- package/public/docs/components/search.md +114 -0
- package/public/docs/components/section-heading.md +111 -0
- package/public/docs/components/segmented-control.md +124 -0
- package/public/docs/components/select.md +222 -0
- package/public/docs/components/sheet.md +143 -0
- package/public/docs/components/sidebar.md +191 -0
- package/public/docs/components/skeleton.md +133 -0
- package/public/docs/components/slider.md +112 -0
- package/public/docs/components/sparkline.md +126 -0
- package/public/docs/components/spinner.md +96 -0
- package/public/docs/components/stat.md +104 -0
- package/public/docs/components/status-tracker.md +143 -0
- package/public/docs/components/stepper.md +145 -0
- package/public/docs/components/switch.md +151 -0
- package/public/docs/components/tabs.md +178 -0
- package/public/docs/components/tag.md +103 -0
- package/public/docs/components/textarea.md +146 -0
- package/public/docs/components/timeline.md +150 -0
- package/public/docs/components/toast.md +192 -0
- package/public/docs/components/tool-call.md +105 -0
- package/public/docs/components/tooltip.md +106 -0
- package/public/manifest/components.json +182 -6
- package/public/manifest/tokens.json +39 -0
- package/scripts/division-ds.mjs +74 -38
- package/scripts/lib/agent-docs.mjs +53 -1
- package/scripts/lib/ai-copy.mjs +106 -0
- package/scripts/lib/button-spacing.mjs +5 -5
- package/scripts/lib/check-text.mjs +44 -0
- package/scripts/lib/contrast.mjs +111 -0
- package/scripts/lib/host-inline.mjs +59 -0
- package/scripts/lib/mcp-server.mjs +115 -0
- package/scripts/lib/mcp-tools.mjs +241 -0
- package/scripts/lib/props-validator.mjs +1 -1
- package/scripts/lib/rules-installer.mjs +42 -4
- 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 +7 -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 +19 -0
- package/skills/division-ds/topics/layout.md +34 -0
- package/skills/division-ds/topics/motion.md +10 -0
- package/skills/division-ds/topics/overlays.md +9 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# DataTable
|
|
2
|
+
|
|
3
|
+
> Rows of records that people read, compare and sort. It's a real HTML table with a caption, column and row headers, and sortable columns. When there's a job to do, each row gets a checkbox.
|
|
4
|
+
|
|
5
|
+
- **Best for:** Records people read, compare and sort across columns, with optional bulk selection.
|
|
6
|
+
- **Not for:** A list people scan row by row: use ListItem. A form in every cell.
|
|
7
|
+
- Organism. Source: `src/components/organisms/data-table/DataTable.tsx`
|
|
8
|
+
- Rendered docs page: `/components/data-table`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { DataTable, sortRows } from 'src/components/organisms/data-table/DataTable'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| `caption` | `string` | | Required. Names the table and its scroll region. |
|
|
21
|
+
| `hideCaption` | `boolean` | `false` | Hides the caption visually. It still names the table and its scroll region for screen readers. |
|
|
22
|
+
| `columns` | `DataTableColumn<Row>[]` | | Required. Each column has key, header, cell, sortValue and numeric. |
|
|
23
|
+
| `rows` | `readonly Row[]` | | Required. |
|
|
24
|
+
| `getRowId` | `(row: Row) => string` | | Required. |
|
|
25
|
+
| `rowHeader` | `string` | | Required. The key of the column that heads each row. |
|
|
26
|
+
| `defaultSort` | `DataTableSort` | | Starts the table sorted. Uncontrolled: the table keeps track of the sort itself after that. |
|
|
27
|
+
| `sort` | `DataTableSort` | | Controlled sort, meaning your code holds the sort and passes it in. Set it together with onSortChange. A header click then only reports the next sort. |
|
|
28
|
+
| `onSortChange` | `(sort: DataTableSort) => void` | | Called with the next sort on every header click, whether sort is controlled or not. |
|
|
29
|
+
| `emptyMessage` | `ReactNode` | `'Nothing to show.'` | Shown across every column when rows is empty. |
|
|
30
|
+
| `selectable` | `boolean` | `false` | Turns on the checkbox column. |
|
|
31
|
+
| `rowLabel` | `(row: Row) => string` | | Required when selectable. Gives each row's checkbox its name. |
|
|
32
|
+
| `selected` | `ReadonlySet<string>` | | Controlled selection (your code holds it), keyed by getRowId. |
|
|
33
|
+
| `defaultSelected` | `Iterable<string>` | | The starting selection when it's uncontrolled (the table holds it). |
|
|
34
|
+
| `onSelectedChange` | `(selected: Set<string>) => void` | | Called with the next selection on every change. |
|
|
35
|
+
| `className` | `string` | | Goes on the table element. |
|
|
36
|
+
| `ref` | `Ref<HTMLTableElement>` | | Points at the table 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 SortDirection = 'ascending' | 'descending'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
export type DataTableSort = { key: string; direction: SortDirection }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
export type DataTableColumn<Row> = {
|
|
52
|
+
key: string
|
|
53
|
+
header: string
|
|
54
|
+
cell: (row: Row) => ReactNode
|
|
55
|
+
/** Present means sortable: the value rows are ordered by. */
|
|
56
|
+
sortValue?: (row: Row) => string | number
|
|
57
|
+
/** Right-aligned, tabular figures. */
|
|
58
|
+
numeric?: boolean
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
/** Pure and stable: equal rows keep their order in both directions. Strings compare with numeric
|
|
64
|
+
* collation, so "2" sorts before "10". */
|
|
65
|
+
export function sortRows<Row>(
|
|
66
|
+
rows: readonly Row[],
|
|
67
|
+
sortValue: ((row: Row) => string | number) | undefined,
|
|
68
|
+
direction: SortDirection
|
|
69
|
+
): Row[]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Examples
|
|
73
|
+
|
|
74
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
75
|
+
|
|
76
|
+
### Sortable
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
type Status = 'In transit' | 'Delivered' | 'Held'
|
|
80
|
+
|
|
81
|
+
type Shipment = { ref: string; destination: string; weight: number; items: number; status: Status }
|
|
82
|
+
|
|
83
|
+
const SHIPMENTS: Shipment[] = [
|
|
84
|
+
{ ref: 'SHP-201', destination: 'Cluj depot', weight: 8.2, items: 5, status: 'In transit' },
|
|
85
|
+
{ ref: 'SHP-202', destination: 'Arad depot', weight: 14.0, items: 12, status: 'Delivered' },
|
|
86
|
+
{ ref: 'SHP-203', destination: 'Brasov depot', weight: 3.5, items: 2, status: 'Held' },
|
|
87
|
+
{ ref: 'SHP-204', destination: 'Sibiu depot', weight: 22.1, items: 9, status: 'In transit' },
|
|
88
|
+
{ ref: 'SHP-205', destination: 'Oradea depot', weight: 6.0, items: 1, status: 'Delivered' },
|
|
89
|
+
{ ref: 'SHP-206', destination: 'Timisoara depot', weight: 18.4, items: 15, status: 'Held' },
|
|
90
|
+
{ ref: 'SHP-207', destination: 'Iasi depot', weight: 9.9, items: 4, status: 'In transit' },
|
|
91
|
+
{ ref: 'SHP-208', destination: 'Constanta depot', weight: 27.3, items: 20, status: 'Delivered' },
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
const STATUS_TONE: Record<Status, 'info' | 'ok' | 'warning'> = {
|
|
95
|
+
'In transit': 'info',
|
|
96
|
+
Delivered: 'ok',
|
|
97
|
+
Held: 'warning',
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const COLUMNS: DataTableColumn<Shipment>[] = [
|
|
101
|
+
{ key: 'ref', header: 'Reference', cell: s => s.ref, sortValue: s => s.ref },
|
|
102
|
+
{ key: 'destination', header: 'Destination', cell: s => s.destination },
|
|
103
|
+
{ key: 'weight', header: 'Weight (kg)', cell: s => s.weight.toFixed(1), sortValue: s => s.weight, numeric: true },
|
|
104
|
+
{ key: 'items', header: 'Items', cell: s => String(s.items), sortValue: s => s.items, numeric: true },
|
|
105
|
+
{
|
|
106
|
+
key: 'status',
|
|
107
|
+
header: 'Status',
|
|
108
|
+
cell: s => (
|
|
109
|
+
<Badge emphasis="solid" tone={STATUS_TONE[s.status]}>
|
|
110
|
+
{s.status}
|
|
111
|
+
</Badge>
|
|
112
|
+
),
|
|
113
|
+
},
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
<DataTable
|
|
117
|
+
caption="Shipments, sortable"
|
|
118
|
+
columns={COLUMNS}
|
|
119
|
+
rows={SHIPMENTS}
|
|
120
|
+
getRowId={s => s.ref}
|
|
121
|
+
rowHeader="ref"
|
|
122
|
+
defaultSort={{ key: 'ref', direction: 'ascending' }}
|
|
123
|
+
/>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Controlled sort: the page, not the table, keeps track of the sort order
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
type Status = 'In transit' | 'Delivered' | 'Held'
|
|
130
|
+
|
|
131
|
+
type Shipment = { ref: string; destination: string; weight: number; items: number; status: Status }
|
|
132
|
+
|
|
133
|
+
const SHIPMENTS: Shipment[] = [
|
|
134
|
+
{ ref: 'SHP-201', destination: 'Cluj depot', weight: 8.2, items: 5, status: 'In transit' },
|
|
135
|
+
{ ref: 'SHP-202', destination: 'Arad depot', weight: 14.0, items: 12, status: 'Delivered' },
|
|
136
|
+
{ ref: 'SHP-203', destination: 'Brasov depot', weight: 3.5, items: 2, status: 'Held' },
|
|
137
|
+
{ ref: 'SHP-204', destination: 'Sibiu depot', weight: 22.1, items: 9, status: 'In transit' },
|
|
138
|
+
{ ref: 'SHP-205', destination: 'Oradea depot', weight: 6.0, items: 1, status: 'Delivered' },
|
|
139
|
+
{ ref: 'SHP-206', destination: 'Timisoara depot', weight: 18.4, items: 15, status: 'Held' },
|
|
140
|
+
{ ref: 'SHP-207', destination: 'Iasi depot', weight: 9.9, items: 4, status: 'In transit' },
|
|
141
|
+
{ ref: 'SHP-208', destination: 'Constanta depot', weight: 27.3, items: 20, status: 'Delivered' },
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
const STATUS_TONE: Record<Status, 'info' | 'ok' | 'warning'> = {
|
|
145
|
+
'In transit': 'info',
|
|
146
|
+
Delivered: 'ok',
|
|
147
|
+
Held: 'warning',
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const COLUMNS: DataTableColumn<Shipment>[] = [
|
|
151
|
+
{ key: 'ref', header: 'Reference', cell: s => s.ref, sortValue: s => s.ref },
|
|
152
|
+
{ key: 'destination', header: 'Destination', cell: s => s.destination },
|
|
153
|
+
{ key: 'weight', header: 'Weight (kg)', cell: s => s.weight.toFixed(1), sortValue: s => s.weight, numeric: true },
|
|
154
|
+
{ key: 'items', header: 'Items', cell: s => String(s.items), sortValue: s => s.items, numeric: true },
|
|
155
|
+
{
|
|
156
|
+
key: 'status',
|
|
157
|
+
header: 'Status',
|
|
158
|
+
cell: s => (
|
|
159
|
+
<Badge emphasis="solid" tone={STATUS_TONE[s.status]}>
|
|
160
|
+
{s.status}
|
|
161
|
+
</Badge>
|
|
162
|
+
),
|
|
163
|
+
},
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
function ControlledSortExample() {
|
|
167
|
+
const [sort, setSort] = useState<DataTableSort>({ key: 'weight', direction: 'descending' })
|
|
168
|
+
return (
|
|
169
|
+
<DataTable
|
|
170
|
+
caption="Shipments, controlled sort"
|
|
171
|
+
columns={COLUMNS}
|
|
172
|
+
rows={SHIPMENTS}
|
|
173
|
+
getRowId={s => s.ref}
|
|
174
|
+
rowHeader="ref"
|
|
175
|
+
sort={sort}
|
|
176
|
+
onSortChange={setSort}
|
|
177
|
+
/>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
<ControlledSortExample />
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### With row selection and an action that applies to every selected row
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
type Status = 'In transit' | 'Delivered' | 'Held'
|
|
188
|
+
|
|
189
|
+
type Shipment = { ref: string; destination: string; weight: number; items: number; status: Status }
|
|
190
|
+
|
|
191
|
+
const SHIPMENTS: Shipment[] = [
|
|
192
|
+
{ ref: 'SHP-201', destination: 'Cluj depot', weight: 8.2, items: 5, status: 'In transit' },
|
|
193
|
+
{ ref: 'SHP-202', destination: 'Arad depot', weight: 14.0, items: 12, status: 'Delivered' },
|
|
194
|
+
{ ref: 'SHP-203', destination: 'Brasov depot', weight: 3.5, items: 2, status: 'Held' },
|
|
195
|
+
{ ref: 'SHP-204', destination: 'Sibiu depot', weight: 22.1, items: 9, status: 'In transit' },
|
|
196
|
+
{ ref: 'SHP-205', destination: 'Oradea depot', weight: 6.0, items: 1, status: 'Delivered' },
|
|
197
|
+
{ ref: 'SHP-206', destination: 'Timisoara depot', weight: 18.4, items: 15, status: 'Held' },
|
|
198
|
+
{ ref: 'SHP-207', destination: 'Iasi depot', weight: 9.9, items: 4, status: 'In transit' },
|
|
199
|
+
{ ref: 'SHP-208', destination: 'Constanta depot', weight: 27.3, items: 20, status: 'Delivered' },
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
const STATUS_TONE: Record<Status, 'info' | 'ok' | 'warning'> = {
|
|
203
|
+
'In transit': 'info',
|
|
204
|
+
Delivered: 'ok',
|
|
205
|
+
Held: 'warning',
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const COLUMNS: DataTableColumn<Shipment>[] = [
|
|
209
|
+
{ key: 'ref', header: 'Reference', cell: s => s.ref, sortValue: s => s.ref },
|
|
210
|
+
{ key: 'destination', header: 'Destination', cell: s => s.destination },
|
|
211
|
+
{ key: 'weight', header: 'Weight (kg)', cell: s => s.weight.toFixed(1), sortValue: s => s.weight, numeric: true },
|
|
212
|
+
{ key: 'items', header: 'Items', cell: s => String(s.items), sortValue: s => s.items, numeric: true },
|
|
213
|
+
{
|
|
214
|
+
key: 'status',
|
|
215
|
+
header: 'Status',
|
|
216
|
+
cell: s => (
|
|
217
|
+
<Badge emphasis="solid" tone={STATUS_TONE[s.status]}>
|
|
218
|
+
{s.status}
|
|
219
|
+
</Badge>
|
|
220
|
+
),
|
|
221
|
+
},
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
function BulkExample() {
|
|
225
|
+
const [rows, setRows] = useState(SHIPMENTS)
|
|
226
|
+
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
227
|
+
const count = selected.size
|
|
228
|
+
|
|
229
|
+
const archive = () => {
|
|
230
|
+
if (count === 0) return
|
|
231
|
+
setRows(current => current.filter(row => !selected.has(row.ref)))
|
|
232
|
+
setSelected(new Set())
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div className="flex flex-col gap-4 w-full">
|
|
237
|
+
<Button variant="secondary" aria-disabled={count === 0 || undefined} onClick={archive} className="self-start">
|
|
238
|
+
{count > 0 ? `Archive ${count} shipment${count === 1 ? '' : 's'}` : 'Archive shipments'}
|
|
239
|
+
</Button>
|
|
240
|
+
<DataTable
|
|
241
|
+
caption="Shipments, selectable"
|
|
242
|
+
columns={COLUMNS}
|
|
243
|
+
rows={rows}
|
|
244
|
+
getRowId={s => s.ref}
|
|
245
|
+
rowHeader="ref"
|
|
246
|
+
selectable
|
|
247
|
+
rowLabel={s => s.ref}
|
|
248
|
+
selected={selected}
|
|
249
|
+
onSelectedChange={setSelected}
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
<BulkExample />
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### With pagination and selection
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
type Status = 'In transit' | 'Delivered' | 'Held'
|
|
262
|
+
|
|
263
|
+
type Shipment = { ref: string; destination: string; weight: number; items: number; status: Status }
|
|
264
|
+
|
|
265
|
+
const SHIPMENTS: Shipment[] = [
|
|
266
|
+
{ ref: 'SHP-201', destination: 'Cluj depot', weight: 8.2, items: 5, status: 'In transit' },
|
|
267
|
+
{ ref: 'SHP-202', destination: 'Arad depot', weight: 14.0, items: 12, status: 'Delivered' },
|
|
268
|
+
{ ref: 'SHP-203', destination: 'Brasov depot', weight: 3.5, items: 2, status: 'Held' },
|
|
269
|
+
{ ref: 'SHP-204', destination: 'Sibiu depot', weight: 22.1, items: 9, status: 'In transit' },
|
|
270
|
+
{ ref: 'SHP-205', destination: 'Oradea depot', weight: 6.0, items: 1, status: 'Delivered' },
|
|
271
|
+
{ ref: 'SHP-206', destination: 'Timisoara depot', weight: 18.4, items: 15, status: 'Held' },
|
|
272
|
+
{ ref: 'SHP-207', destination: 'Iasi depot', weight: 9.9, items: 4, status: 'In transit' },
|
|
273
|
+
{ ref: 'SHP-208', destination: 'Constanta depot', weight: 27.3, items: 20, status: 'Delivered' },
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
const STATUS_TONE: Record<Status, 'info' | 'ok' | 'warning'> = {
|
|
277
|
+
'In transit': 'info',
|
|
278
|
+
Delivered: 'ok',
|
|
279
|
+
Held: 'warning',
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const COLUMNS: DataTableColumn<Shipment>[] = [
|
|
283
|
+
{ key: 'ref', header: 'Reference', cell: s => s.ref, sortValue: s => s.ref },
|
|
284
|
+
{ key: 'destination', header: 'Destination', cell: s => s.destination },
|
|
285
|
+
{ key: 'weight', header: 'Weight (kg)', cell: s => s.weight.toFixed(1), sortValue: s => s.weight, numeric: true },
|
|
286
|
+
{ key: 'items', header: 'Items', cell: s => String(s.items), sortValue: s => s.items, numeric: true },
|
|
287
|
+
{
|
|
288
|
+
key: 'status',
|
|
289
|
+
header: 'Status',
|
|
290
|
+
cell: s => (
|
|
291
|
+
<Badge emphasis="solid" tone={STATUS_TONE[s.status]}>
|
|
292
|
+
{s.status}
|
|
293
|
+
</Badge>
|
|
294
|
+
),
|
|
295
|
+
},
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
function PagedExample() {
|
|
299
|
+
const [perPage, setPerPage] = useState(4)
|
|
300
|
+
const [page, setPage] = useState(1)
|
|
301
|
+
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
302
|
+
const start = (page - 1) * perPage
|
|
303
|
+
const visible = SHIPMENTS.slice(start, start + perPage)
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<div className="flex flex-col gap-3 w-full">
|
|
307
|
+
<DataTable
|
|
308
|
+
caption="Shipments, paged"
|
|
309
|
+
columns={COLUMNS}
|
|
310
|
+
rows={visible}
|
|
311
|
+
getRowId={s => s.ref}
|
|
312
|
+
rowHeader="ref"
|
|
313
|
+
selectable
|
|
314
|
+
rowLabel={s => s.ref}
|
|
315
|
+
selected={selected}
|
|
316
|
+
onSelectedChange={setSelected}
|
|
317
|
+
/>
|
|
318
|
+
<Pagination
|
|
319
|
+
page={page}
|
|
320
|
+
pageCount={Math.ceil(SHIPMENTS.length / perPage)}
|
|
321
|
+
onPageChange={setPage}
|
|
322
|
+
totalItems={SHIPMENTS.length}
|
|
323
|
+
pageSize={perPage}
|
|
324
|
+
onPageSizeChange={size => {
|
|
325
|
+
setPerPage(size)
|
|
326
|
+
setPage(1)
|
|
327
|
+
}}
|
|
328
|
+
pageSizeOptions={[4, 8, 12]}
|
|
329
|
+
label="Pagination, shipments"
|
|
330
|
+
/>
|
|
331
|
+
</div>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
<PagedExample />
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Empty
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
type Status = 'In transit' | 'Delivered' | 'Held'
|
|
342
|
+
|
|
343
|
+
type Shipment = { ref: string; destination: string; weight: number; items: number; status: Status }
|
|
344
|
+
|
|
345
|
+
const STATUS_TONE: Record<Status, 'info' | 'ok' | 'warning'> = {
|
|
346
|
+
'In transit': 'info',
|
|
347
|
+
Delivered: 'ok',
|
|
348
|
+
Held: 'warning',
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const COLUMNS: DataTableColumn<Shipment>[] = [
|
|
352
|
+
{ key: 'ref', header: 'Reference', cell: s => s.ref, sortValue: s => s.ref },
|
|
353
|
+
{ key: 'destination', header: 'Destination', cell: s => s.destination },
|
|
354
|
+
{ key: 'weight', header: 'Weight (kg)', cell: s => s.weight.toFixed(1), sortValue: s => s.weight, numeric: true },
|
|
355
|
+
{ key: 'items', header: 'Items', cell: s => String(s.items), sortValue: s => s.items, numeric: true },
|
|
356
|
+
{
|
|
357
|
+
key: 'status',
|
|
358
|
+
header: 'Status',
|
|
359
|
+
cell: s => (
|
|
360
|
+
<Badge emphasis="solid" tone={STATUS_TONE[s.status]}>
|
|
361
|
+
{s.status}
|
|
362
|
+
</Badge>
|
|
363
|
+
),
|
|
364
|
+
},
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
<DataTable caption="Returns" columns={COLUMNS} rows={[]} getRowId={s => s.ref} rowHeader="ref" emptyMessage="No returns yet." />
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Keyboard
|
|
371
|
+
|
|
372
|
+
| Keys | Result |
|
|
373
|
+
|---|---|
|
|
374
|
+
| `Tab` | Moves to the scroll region, then to each sort button and checkbox in order. |
|
|
375
|
+
| `Enter` | Sorts by that column, or flips its direction. |
|
|
376
|
+
| `Space` | Sorts, or ticks a checkbox. |
|
|
377
|
+
| `←` `→` | Scroll the region sideways while it has focus. |
|
|
378
|
+
|
|
379
|
+
## Do
|
|
380
|
+
|
|
381
|
+
- Right-align numbers and mark their column as numeric.
|
|
382
|
+
- Say how many rows there are near the table, and put Pagination below it.
|
|
383
|
+
- Keep a bulk action's button a tab stop (a place the Tab key lands) while nothing is selected, and mark it aria-disabled so screen readers say it's unavailable.
|
|
384
|
+
- You can page or filter the rows freely. Select-all acts on the rows shown and keeps what was selected elsewhere, and the count says how many selected rows are out of view.
|
|
385
|
+
|
|
386
|
+
## Don't
|
|
387
|
+
|
|
388
|
+
- Don't make every column sortable. Sort the ones people compare.
|
|
389
|
+
- Don't tint selected or hovered rows. The checkbox and the count already say it.
|
|
390
|
+
- Don't put a form in every cell. That's a different component.
|
|
391
|
+
|
|
392
|
+
## Anatomy
|
|
393
|
+
|
|
394
|
+
- **Caption**: Names the table and its scroll region. You can hide it visually and it still works for screen readers.
|
|
395
|
+
- **Scroll region**: It can take keyboard focus, so a wide table scrolls from the keyboard.
|
|
396
|
+
- **Column header**: A sort button when the column sorts. The sorted column carries aria-sort, an ARIA (Accessible Rich Internet Applications) attribute that tells screen readers which way it's sorted.
|
|
397
|
+
- **Row header**: The cell from the rowHeader column. It names its row.
|
|
398
|
+
- **Selection**: A checkbox per row, one to select all, and a count above the table. Selected rows don't get a tint.
|
|
399
|
+
- **Empty**: One cell that spans every column.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# DescriptionList
|
|
2
|
+
|
|
3
|
+
> Pairs of a term and its value, all about one thing, in one to three columns. The columns drop away as the space they get narrows. It's a real dl (the HTML description list element): a label in a monospaced font over each value, and a thin line above each pair.
|
|
4
|
+
|
|
5
|
+
- **Best for:** The fixed fields of one record: serial, firmware, owner, dates.
|
|
6
|
+
- **Not for:** Many records: use DataTable; one headline number: use Stat.
|
|
7
|
+
- Molecule. Source: `src/components/molecules/description-list/DescriptionList.tsx`
|
|
8
|
+
- Rendered docs page: `/components/description-list`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { DescriptionList } from 'src/components/molecules/description-list/DescriptionList'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| `items` | `readonly DescriptionListItem[]` | | Required. The pairs, in the order people read them. |
|
|
21
|
+
| `columns` | `1 \| 2 \| 3` | `1` | How many columns at full width. It drops to one when the container is narrower than 28rem. |
|
|
22
|
+
| `className` | `string` | | On the container. |
|
|
23
|
+
| `ref` | `Ref<HTMLDivElement>` | | Forwarded to the container. |
|
|
24
|
+
| `term (DescriptionListItem)` | `string` | | Required. The field name: "Serial". |
|
|
25
|
+
| `description (DescriptionListItem)` | `ReactNode` | | Required. Its value. |
|
|
26
|
+
|
|
27
|
+
## Types and helpers
|
|
28
|
+
|
|
29
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
export type DescriptionListItem = {
|
|
33
|
+
/** The name of the field: "Serial", "Last seen". Shown as a mono label. */
|
|
34
|
+
term: string
|
|
35
|
+
/** Its value: text, a Badge, a Link. */
|
|
36
|
+
description: ReactNode
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Examples
|
|
41
|
+
|
|
42
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
43
|
+
|
|
44
|
+
### One column
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
<div className="w-full max-w-sm">
|
|
48
|
+
<DescriptionList
|
|
49
|
+
items={[
|
|
50
|
+
{ term: 'Serial', description: 'HS-4471-B' },
|
|
51
|
+
{ term: 'Status', description: <Badge tone="ok">Online</Badge> },
|
|
52
|
+
{ term: 'Firmware', description: '2.8.1' },
|
|
53
|
+
{ term: 'Last seen', description: '15 Oct, 09:58' },
|
|
54
|
+
]}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Three columns, fewer as the container narrows
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<div className="w-full">
|
|
63
|
+
<DescriptionList
|
|
64
|
+
columns={3}
|
|
65
|
+
items={[
|
|
66
|
+
{ term: 'Order', description: 'ORD-2291' },
|
|
67
|
+
{ term: 'Placed', description: '12 Oct 2026' },
|
|
68
|
+
{ term: 'Ships', description: '16 Oct 2026' },
|
|
69
|
+
{ term: 'Depot', description: 'Cluj, bay 4' },
|
|
70
|
+
{ term: 'Carrier', description: 'Regular courier' },
|
|
71
|
+
{ term: 'Weight', description: '22.1 kg' },
|
|
72
|
+
]}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Keyboard
|
|
78
|
+
|
|
79
|
+
| Keys | Result |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `Tab` | Moves to any link or button inside a description. |
|
|
82
|
+
|
|
83
|
+
## Do
|
|
84
|
+
|
|
85
|
+
- Order pairs the way someone reads the record: identity first, then state, then dates.
|
|
86
|
+
- Say "None" or "Not set" for an empty value rather than leaving the description blank.
|
|
87
|
+
- Use two or three columns only for short values; long text reads better in one.
|
|
88
|
+
|
|
89
|
+
## Don't
|
|
90
|
+
|
|
91
|
+
- Don't use it for a list of records. That's what DataTable is for.
|
|
92
|
+
- Don't put a form field in a description. A record people can edit is a form.
|
|
93
|
+
- Don't repeat a term inside one list.
|
|
94
|
+
|
|
95
|
+
## Anatomy
|
|
96
|
+
|
|
97
|
+
- **Container**: A div with @container (a container query), so the columns follow the width of this box, not of the screen.
|
|
98
|
+
- **List**: A dl laid out as a grid: one column, two from 28rem wide, three from 42rem (as far as columns allows). A rem is the base text size, usually 16 pixels.
|
|
99
|
+
- **Pair**: A div holding one dt (the term) and one dd (its value), with a thin line above.
|
|
100
|
+
- **Term**: The dt, styled as a label: monospaced, uppercase, 11px (pixels).
|
|
101
|
+
- **Description**: The dd, in body text. It holds text, a Badge or a Link.
|