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,147 @@
1
+ # CommandPalette
2
+
3
+ > Every action in one place. Press Ctrl+K anywhere, type a few letters, move with the arrow keys and press Enter. Focus (where keyboard input goes) returns to where it was, and then the command runs.
4
+
5
+ - **Best for:** Every action in one place behind Ctrl+K, with a visible button that opens it too.
6
+ - **Not for:** Searching page content: use Search. A few actions on one control: use DropdownMenu.
7
+ - Organism. Source: `src/components/organisms/command-palette/CommandPalette.tsx`
8
+ - Rendered docs page: `/components/command-palette`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { CommandPalette, filterCommands, groupCommands } from 'src/components/organisms/command-palette/CommandPalette'
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
+ | `commands` | `Command[]` | | Required. Each has id, label, group, keywords, shortcut, icon and onSelect. |
23
+ | `open` | `boolean` | | Open state when controlled, meaning your code decides whether it is open. |
24
+ | `defaultOpen` | `boolean` | `false` | Starting open state when uncontrolled, meaning the palette keeps track of it itself. |
25
+ | `onOpenChange` | `(open: boolean) => void` | | Called on every open and close, including Escape and a click on the scrim (the dimmed backdrop). If you refuse the close, the chosen command stays ready to run until the user does anything else in the palette. Closing it yourself later counts as accepting it. |
26
+ | `shortcut` | `boolean` | `true` | Ctrl+K or Cmd+K opens and closes the palette from anywhere. |
27
+ | `label` | `string` | `'Command palette'` | The dialog's accessible name (what a screen reader announces), hidden from view. |
28
+ | `searchLabel` | `string` | `'Search commands'` | The search field's accessible name. |
29
+ | `placeholder` | `string` | `'Type a command or search'` | Shown in the empty search field. |
30
+ | `emptyMessage` | `string` | `'No matching commands'` | Shown when nothing matches the search. |
31
+ | `ref` | `Ref<HTMLInputElement>` | | The search field, while the palette is open. |
32
+
33
+ ## Types and helpers
34
+
35
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
36
+
37
+ ```ts
38
+ export type Command = {
39
+ id: string
40
+ label: string
41
+ /** Commands with the same group are drawn together under its heading. */
42
+ group?: string
43
+ /** Extra words a search matches on. */
44
+ keywords?: string[]
45
+ /** A hint shown on the option, e.g. "N". Display only: the palette does not bind it. */
46
+ shortcut?: string
47
+ /** Drawn aria-hidden; the label carries the meaning. */
48
+ icon?: ReactNode
49
+ onSelect: () => void
50
+ }
51
+ ```
52
+
53
+ ```ts
54
+ export type CommandGroup = { group?: string; commands: Command[] }
55
+ ```
56
+
57
+ ```ts
58
+ /** Every whitespace-separated term must appear in the label, the group or a keyword, ignoring case.
59
+ * A label that starts with the first term ranks first; otherwise the given order holds. */
60
+ export function filterCommands(commands: readonly Command[], query: string): Command[]
61
+ ```
62
+
63
+ ```ts
64
+ /** Groups in the order each group first appears; order within a group is kept. */
65
+ export function groupCommands(commands: readonly Command[]): CommandGroup[]
66
+ ```
67
+
68
+ ## Examples
69
+
70
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
71
+
72
+ ### Store commands
73
+
74
+ ```tsx
75
+ function StoreCommands() {
76
+ const [open, setOpen] = useState(false)
77
+ const [last, setLast] = useState('none')
78
+ const make = (id: string, label: string, group: string, icon: ReactNode, extra: Partial<Command> = {}): Command => ({
79
+ id,
80
+ label,
81
+ group,
82
+ icon,
83
+ onSelect: () => setLast(label),
84
+ ...extra,
85
+ })
86
+ const commands = [
87
+ make('orders', 'Go to orders', 'Navigate', <Package />, { keywords: ['sales'] }),
88
+ make('shipments', 'Go to shipments', 'Navigate', <Truck />),
89
+ make('returns', 'Go to returns', 'Navigate', <Inbox />),
90
+ make('new-shipment', 'New shipment', 'Create', <Plus />, { shortcut: 'N' }),
91
+ make('new-product', 'New product', 'Create', <Box />),
92
+ make('invite', 'Invite a teammate', 'Account', <Users />, { keywords: ['team', 'people'] }),
93
+ make('settings', 'Open settings', 'Account', <Settings />),
94
+ ]
95
+ return (
96
+ <div className="flex flex-wrap items-center gap-3">
97
+ <Button variant="secondary" aria-keyshortcuts="Control+K Meta+K" onClick={() => setOpen(true)}>
98
+ Open command palette
99
+ </Button>
100
+ <span aria-hidden="true" className="inline-flex gap-1">
101
+ <Kbd>Ctrl</Kbd>
102
+ <Kbd>K</Kbd>
103
+ </span>
104
+ <p role="status" className="font-mono text-xs text-[var(--color-text-secondary)]">
105
+ Last command: {last}
106
+ </p>
107
+ <CommandPalette commands={commands} open={open} onOpenChange={setOpen} />
108
+ </div>
109
+ )
110
+ }
111
+
112
+ <StoreCommands />
113
+ ```
114
+
115
+ ## Keyboard
116
+
117
+ | Keys | Result |
118
+ |---|---|
119
+ | `Ctrl` `K` | Opens or closes it from anywhere on the page (Cmd+K on macOS). |
120
+ | `↓` | Next command, wrapping round to the first. |
121
+ | `↑` | Previous command, wrapping round to the last. |
122
+ | `Home` | First command. |
123
+ | `End` | Last command. |
124
+ | `Enter` | Runs the active command. |
125
+ | `Esc` | Closes without running anything. |
126
+
127
+ ## Do
128
+
129
+ - Name commands as verbs: 'Go to orders', 'New shipment'.
130
+ - Add keywords for the words people actually type.
131
+ - Give it a visible button that opens it too. People won't find a shortcut on its own.
132
+
133
+ ## Don't
134
+
135
+ - Don't put destructive commands in it without a dialog that asks for confirmation.
136
+ - Don't show a shortcut hint the app doesn't actually set up.
137
+ - Don't put more than one palette on a page. Each one listens for the same Ctrl+K/Cmd+K across the whole page, so a second one fights the first for the shortcut.
138
+
139
+ ## Anatomy
140
+
141
+ - **Dialog**: Named 'Command palette', with the name hidden from view. Drawn on surface.base with the corner crosshair.
142
+ - **Search field**: A combobox, a text field tied to a list of choices. Focus stays here the whole time.
143
+ - **Listbox**: Named 'Commands'. It's not there at all when nothing matches.
144
+ - **Group**: Named by its heading.
145
+ - **Option**: The active one gets the fill, and the search field points to it as its active descendant, so screen readers announce it while focus stays in the field.
146
+ - **Count**: A status line hidden from view but read by screen readers: '2 commands'.
147
+ - **Shortcut hint**: A Kbd that's hidden from assistive technology (screen readers and similar tools). It's for display only.
@@ -0,0 +1,401 @@
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
+ 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
+ | `caption` | `string` | | Required. Names the table and its scroll region. |
23
+ | `hideCaption` | `boolean` | `false` | Hides the caption visually. It still names the table and its scroll region for screen readers. |
24
+ | `columns` | `DataTableColumn<Row>[]` | | Required. Each column has key, header, cell, sortValue and numeric. |
25
+ | `rows` | `readonly Row[]` | | Required. |
26
+ | `getRowId` | `(row: Row) => string` | | Required. |
27
+ | `rowHeader` | `string` | | Required. The key of the column that heads each row. |
28
+ | `defaultSort` | `DataTableSort` | | Starts the table sorted. Uncontrolled: the table keeps track of the sort itself after that. |
29
+ | `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. |
30
+ | `onSortChange` | `(sort: DataTableSort) => void` | | Called with the next sort on every header click, whether sort is controlled or not. |
31
+ | `emptyMessage` | `ReactNode` | `'Nothing to show.'` | Shown across every column when rows is empty. |
32
+ | `selectable` | `boolean` | `false` | Turns on the checkbox column. |
33
+ | `rowLabel` | `(row: Row) => string` | | Required when selectable. Gives each row's checkbox its name. |
34
+ | `selected` | `ReadonlySet<string>` | | Controlled selection (your code holds it), keyed by getRowId. |
35
+ | `defaultSelected` | `Iterable<string>` | | The starting selection when it's uncontrolled (the table holds it). |
36
+ | `onSelectedChange` | `(selected: Set<string>) => void` | | Called with the next selection on every change. |
37
+ | `className` | `string` | | Goes on the table element. |
38
+ | `ref` | `Ref<HTMLTableElement>` | | Points at the table element. |
39
+
40
+ ## Types and helpers
41
+
42
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
43
+
44
+ ```ts
45
+ export type SortDirection = 'ascending' | 'descending'
46
+ ```
47
+
48
+ ```ts
49
+ export type DataTableSort = { key: string; direction: SortDirection }
50
+ ```
51
+
52
+ ```ts
53
+ export type DataTableColumn<Row> = {
54
+ key: string
55
+ header: string
56
+ cell: (row: Row) => ReactNode
57
+ /** Present means sortable: the value rows are ordered by. */
58
+ sortValue?: (row: Row) => string | number
59
+ /** Right-aligned, tabular figures. */
60
+ numeric?: boolean
61
+ }
62
+ ```
63
+
64
+ ```ts
65
+ /** Pure and stable: equal rows keep their order in both directions. Strings compare with numeric
66
+ * collation, so "2" sorts before "10". */
67
+ export function sortRows<Row>(
68
+ rows: readonly Row[],
69
+ sortValue: ((row: Row) => string | number) | undefined,
70
+ direction: SortDirection
71
+ ): Row[]
72
+ ```
73
+
74
+ ## Examples
75
+
76
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
77
+
78
+ ### Sortable
79
+
80
+ ```tsx
81
+ type Status = 'In transit' | 'Delivered' | 'Held'
82
+
83
+ type Shipment = { ref: string; destination: string; weight: number; items: number; status: Status }
84
+
85
+ const SHIPMENTS: Shipment[] = [
86
+ { ref: 'SHP-201', destination: 'Cluj depot', weight: 8.2, items: 5, status: 'In transit' },
87
+ { ref: 'SHP-202', destination: 'Arad depot', weight: 14.0, items: 12, status: 'Delivered' },
88
+ { ref: 'SHP-203', destination: 'Brasov depot', weight: 3.5, items: 2, status: 'Held' },
89
+ { ref: 'SHP-204', destination: 'Sibiu depot', weight: 22.1, items: 9, status: 'In transit' },
90
+ { ref: 'SHP-205', destination: 'Oradea depot', weight: 6.0, items: 1, status: 'Delivered' },
91
+ { ref: 'SHP-206', destination: 'Timisoara depot', weight: 18.4, items: 15, status: 'Held' },
92
+ { ref: 'SHP-207', destination: 'Iasi depot', weight: 9.9, items: 4, status: 'In transit' },
93
+ { ref: 'SHP-208', destination: 'Constanta depot', weight: 27.3, items: 20, status: 'Delivered' },
94
+ ]
95
+
96
+ const STATUS_TONE: Record<Status, 'info' | 'ok' | 'warning'> = {
97
+ 'In transit': 'info',
98
+ Delivered: 'ok',
99
+ Held: 'warning',
100
+ }
101
+
102
+ const COLUMNS: DataTableColumn<Shipment>[] = [
103
+ { key: 'ref', header: 'Reference', cell: s => s.ref, sortValue: s => s.ref },
104
+ { key: 'destination', header: 'Destination', cell: s => s.destination },
105
+ { key: 'weight', header: 'Weight (kg)', cell: s => s.weight.toFixed(1), sortValue: s => s.weight, numeric: true },
106
+ { key: 'items', header: 'Items', cell: s => String(s.items), sortValue: s => s.items, numeric: true },
107
+ {
108
+ key: 'status',
109
+ header: 'Status',
110
+ cell: s => (
111
+ <Badge emphasis="solid" tone={STATUS_TONE[s.status]}>
112
+ {s.status}
113
+ </Badge>
114
+ ),
115
+ },
116
+ ]
117
+
118
+ <DataTable
119
+ caption="Shipments, sortable"
120
+ columns={COLUMNS}
121
+ rows={SHIPMENTS}
122
+ getRowId={s => s.ref}
123
+ rowHeader="ref"
124
+ defaultSort={{ key: 'ref', direction: 'ascending' }}
125
+ />
126
+ ```
127
+
128
+ ### Controlled sort: the page, not the table, keeps track of the sort order
129
+
130
+ ```tsx
131
+ type Status = 'In transit' | 'Delivered' | 'Held'
132
+
133
+ type Shipment = { ref: string; destination: string; weight: number; items: number; status: Status }
134
+
135
+ const SHIPMENTS: Shipment[] = [
136
+ { ref: 'SHP-201', destination: 'Cluj depot', weight: 8.2, items: 5, status: 'In transit' },
137
+ { ref: 'SHP-202', destination: 'Arad depot', weight: 14.0, items: 12, status: 'Delivered' },
138
+ { ref: 'SHP-203', destination: 'Brasov depot', weight: 3.5, items: 2, status: 'Held' },
139
+ { ref: 'SHP-204', destination: 'Sibiu depot', weight: 22.1, items: 9, status: 'In transit' },
140
+ { ref: 'SHP-205', destination: 'Oradea depot', weight: 6.0, items: 1, status: 'Delivered' },
141
+ { ref: 'SHP-206', destination: 'Timisoara depot', weight: 18.4, items: 15, status: 'Held' },
142
+ { ref: 'SHP-207', destination: 'Iasi depot', weight: 9.9, items: 4, status: 'In transit' },
143
+ { ref: 'SHP-208', destination: 'Constanta depot', weight: 27.3, items: 20, status: 'Delivered' },
144
+ ]
145
+
146
+ const STATUS_TONE: Record<Status, 'info' | 'ok' | 'warning'> = {
147
+ 'In transit': 'info',
148
+ Delivered: 'ok',
149
+ Held: 'warning',
150
+ }
151
+
152
+ const COLUMNS: DataTableColumn<Shipment>[] = [
153
+ { key: 'ref', header: 'Reference', cell: s => s.ref, sortValue: s => s.ref },
154
+ { key: 'destination', header: 'Destination', cell: s => s.destination },
155
+ { key: 'weight', header: 'Weight (kg)', cell: s => s.weight.toFixed(1), sortValue: s => s.weight, numeric: true },
156
+ { key: 'items', header: 'Items', cell: s => String(s.items), sortValue: s => s.items, numeric: true },
157
+ {
158
+ key: 'status',
159
+ header: 'Status',
160
+ cell: s => (
161
+ <Badge emphasis="solid" tone={STATUS_TONE[s.status]}>
162
+ {s.status}
163
+ </Badge>
164
+ ),
165
+ },
166
+ ]
167
+
168
+ function ControlledSortExample() {
169
+ const [sort, setSort] = useState<DataTableSort>({ key: 'weight', direction: 'descending' })
170
+ return (
171
+ <DataTable
172
+ caption="Shipments, controlled sort"
173
+ columns={COLUMNS}
174
+ rows={SHIPMENTS}
175
+ getRowId={s => s.ref}
176
+ rowHeader="ref"
177
+ sort={sort}
178
+ onSortChange={setSort}
179
+ />
180
+ )
181
+ }
182
+
183
+ <ControlledSortExample />
184
+ ```
185
+
186
+ ### With row selection and an action that applies to every selected row
187
+
188
+ ```tsx
189
+ type Status = 'In transit' | 'Delivered' | 'Held'
190
+
191
+ type Shipment = { ref: string; destination: string; weight: number; items: number; status: Status }
192
+
193
+ const SHIPMENTS: Shipment[] = [
194
+ { ref: 'SHP-201', destination: 'Cluj depot', weight: 8.2, items: 5, status: 'In transit' },
195
+ { ref: 'SHP-202', destination: 'Arad depot', weight: 14.0, items: 12, status: 'Delivered' },
196
+ { ref: 'SHP-203', destination: 'Brasov depot', weight: 3.5, items: 2, status: 'Held' },
197
+ { ref: 'SHP-204', destination: 'Sibiu depot', weight: 22.1, items: 9, status: 'In transit' },
198
+ { ref: 'SHP-205', destination: 'Oradea depot', weight: 6.0, items: 1, status: 'Delivered' },
199
+ { ref: 'SHP-206', destination: 'Timisoara depot', weight: 18.4, items: 15, status: 'Held' },
200
+ { ref: 'SHP-207', destination: 'Iasi depot', weight: 9.9, items: 4, status: 'In transit' },
201
+ { ref: 'SHP-208', destination: 'Constanta depot', weight: 27.3, items: 20, status: 'Delivered' },
202
+ ]
203
+
204
+ const STATUS_TONE: Record<Status, 'info' | 'ok' | 'warning'> = {
205
+ 'In transit': 'info',
206
+ Delivered: 'ok',
207
+ Held: 'warning',
208
+ }
209
+
210
+ const COLUMNS: DataTableColumn<Shipment>[] = [
211
+ { key: 'ref', header: 'Reference', cell: s => s.ref, sortValue: s => s.ref },
212
+ { key: 'destination', header: 'Destination', cell: s => s.destination },
213
+ { key: 'weight', header: 'Weight (kg)', cell: s => s.weight.toFixed(1), sortValue: s => s.weight, numeric: true },
214
+ { key: 'items', header: 'Items', cell: s => String(s.items), sortValue: s => s.items, numeric: true },
215
+ {
216
+ key: 'status',
217
+ header: 'Status',
218
+ cell: s => (
219
+ <Badge emphasis="solid" tone={STATUS_TONE[s.status]}>
220
+ {s.status}
221
+ </Badge>
222
+ ),
223
+ },
224
+ ]
225
+
226
+ function BulkExample() {
227
+ const [rows, setRows] = useState(SHIPMENTS)
228
+ const [selected, setSelected] = useState<Set<string>>(new Set())
229
+ const count = selected.size
230
+
231
+ const archive = () => {
232
+ if (count === 0) return
233
+ setRows(current => current.filter(row => !selected.has(row.ref)))
234
+ setSelected(new Set())
235
+ }
236
+
237
+ return (
238
+ <div className="flex flex-col gap-4 w-full">
239
+ <Button variant="secondary" aria-disabled={count === 0 || undefined} onClick={archive} className="self-start">
240
+ {count > 0 ? `Archive ${count} shipment${count === 1 ? '' : 's'}` : 'Archive shipments'}
241
+ </Button>
242
+ <DataTable
243
+ caption="Shipments, selectable"
244
+ columns={COLUMNS}
245
+ rows={rows}
246
+ getRowId={s => s.ref}
247
+ rowHeader="ref"
248
+ selectable
249
+ rowLabel={s => s.ref}
250
+ selected={selected}
251
+ onSelectedChange={setSelected}
252
+ />
253
+ </div>
254
+ )
255
+ }
256
+
257
+ <BulkExample />
258
+ ```
259
+
260
+ ### With pagination and selection
261
+
262
+ ```tsx
263
+ type Status = 'In transit' | 'Delivered' | 'Held'
264
+
265
+ type Shipment = { ref: string; destination: string; weight: number; items: number; status: Status }
266
+
267
+ const SHIPMENTS: Shipment[] = [
268
+ { ref: 'SHP-201', destination: 'Cluj depot', weight: 8.2, items: 5, status: 'In transit' },
269
+ { ref: 'SHP-202', destination: 'Arad depot', weight: 14.0, items: 12, status: 'Delivered' },
270
+ { ref: 'SHP-203', destination: 'Brasov depot', weight: 3.5, items: 2, status: 'Held' },
271
+ { ref: 'SHP-204', destination: 'Sibiu depot', weight: 22.1, items: 9, status: 'In transit' },
272
+ { ref: 'SHP-205', destination: 'Oradea depot', weight: 6.0, items: 1, status: 'Delivered' },
273
+ { ref: 'SHP-206', destination: 'Timisoara depot', weight: 18.4, items: 15, status: 'Held' },
274
+ { ref: 'SHP-207', destination: 'Iasi depot', weight: 9.9, items: 4, status: 'In transit' },
275
+ { ref: 'SHP-208', destination: 'Constanta depot', weight: 27.3, items: 20, status: 'Delivered' },
276
+ ]
277
+
278
+ const STATUS_TONE: Record<Status, 'info' | 'ok' | 'warning'> = {
279
+ 'In transit': 'info',
280
+ Delivered: 'ok',
281
+ Held: 'warning',
282
+ }
283
+
284
+ const COLUMNS: DataTableColumn<Shipment>[] = [
285
+ { key: 'ref', header: 'Reference', cell: s => s.ref, sortValue: s => s.ref },
286
+ { key: 'destination', header: 'Destination', cell: s => s.destination },
287
+ { key: 'weight', header: 'Weight (kg)', cell: s => s.weight.toFixed(1), sortValue: s => s.weight, numeric: true },
288
+ { key: 'items', header: 'Items', cell: s => String(s.items), sortValue: s => s.items, numeric: true },
289
+ {
290
+ key: 'status',
291
+ header: 'Status',
292
+ cell: s => (
293
+ <Badge emphasis="solid" tone={STATUS_TONE[s.status]}>
294
+ {s.status}
295
+ </Badge>
296
+ ),
297
+ },
298
+ ]
299
+
300
+ function PagedExample() {
301
+ const [perPage, setPerPage] = useState(4)
302
+ const [page, setPage] = useState(1)
303
+ const [selected, setSelected] = useState<Set<string>>(new Set())
304
+ const start = (page - 1) * perPage
305
+ const visible = SHIPMENTS.slice(start, start + perPage)
306
+
307
+ return (
308
+ <div className="flex flex-col gap-3 w-full">
309
+ <DataTable
310
+ caption="Shipments, paged"
311
+ columns={COLUMNS}
312
+ rows={visible}
313
+ getRowId={s => s.ref}
314
+ rowHeader="ref"
315
+ selectable
316
+ rowLabel={s => s.ref}
317
+ selected={selected}
318
+ onSelectedChange={setSelected}
319
+ />
320
+ <Pagination
321
+ page={page}
322
+ pageCount={Math.ceil(SHIPMENTS.length / perPage)}
323
+ onPageChange={setPage}
324
+ totalItems={SHIPMENTS.length}
325
+ pageSize={perPage}
326
+ onPageSizeChange={size => {
327
+ setPerPage(size)
328
+ setPage(1)
329
+ }}
330
+ pageSizeOptions={[4, 8, 12]}
331
+ label="Pagination, shipments"
332
+ />
333
+ </div>
334
+ )
335
+ }
336
+
337
+ <PagedExample />
338
+ ```
339
+
340
+ ### Empty
341
+
342
+ ```tsx
343
+ type Status = 'In transit' | 'Delivered' | 'Held'
344
+
345
+ type Shipment = { ref: string; destination: string; weight: number; items: number; status: Status }
346
+
347
+ const STATUS_TONE: Record<Status, 'info' | 'ok' | 'warning'> = {
348
+ 'In transit': 'info',
349
+ Delivered: 'ok',
350
+ Held: 'warning',
351
+ }
352
+
353
+ const COLUMNS: DataTableColumn<Shipment>[] = [
354
+ { key: 'ref', header: 'Reference', cell: s => s.ref, sortValue: s => s.ref },
355
+ { key: 'destination', header: 'Destination', cell: s => s.destination },
356
+ { key: 'weight', header: 'Weight (kg)', cell: s => s.weight.toFixed(1), sortValue: s => s.weight, numeric: true },
357
+ { key: 'items', header: 'Items', cell: s => String(s.items), sortValue: s => s.items, numeric: true },
358
+ {
359
+ key: 'status',
360
+ header: 'Status',
361
+ cell: s => (
362
+ <Badge emphasis="solid" tone={STATUS_TONE[s.status]}>
363
+ {s.status}
364
+ </Badge>
365
+ ),
366
+ },
367
+ ]
368
+
369
+ <DataTable caption="Returns" columns={COLUMNS} rows={[]} getRowId={s => s.ref} rowHeader="ref" emptyMessage="No returns yet." />
370
+ ```
371
+
372
+ ## Keyboard
373
+
374
+ | Keys | Result |
375
+ |---|---|
376
+ | `Tab` | Moves to the scroll region, then to each sort button and checkbox in order. |
377
+ | `Enter` | Sorts by that column, or flips its direction. |
378
+ | `Space` | Sorts, or ticks a checkbox. |
379
+ | `←` `→` | Scroll the region sideways while it has focus. |
380
+
381
+ ## Do
382
+
383
+ - Right-align numbers and mark their column as numeric.
384
+ - Say how many rows there are near the table, and put Pagination below it.
385
+ - 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.
386
+ - 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.
387
+
388
+ ## Don't
389
+
390
+ - Don't make every column sortable. Sort the ones people compare.
391
+ - Don't tint selected or hovered rows. The checkbox and the count already say it.
392
+ - Don't put a form in every cell. That's a different component.
393
+
394
+ ## Anatomy
395
+
396
+ - **Caption**: Names the table and its scroll region. You can hide it visually and it still works for screen readers.
397
+ - **Scroll region**: It can take keyboard focus, so a wide table scrolls from the keyboard.
398
+ - **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.
399
+ - **Row header**: The cell from the rowHeader column. It names its row.
400
+ - **Selection**: A checkbox per row, one to select all, and a count above the table. Selected rows don't get a tint.
401
+ - **Empty**: One cell that spans every column.
@@ -0,0 +1,103 @@
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
+ 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
+ | `items` | `readonly DescriptionListItem[]` | | Required. The pairs, in the order people read them. |
23
+ | `columns` | `1 \| 2 \| 3` | `1` | How many columns at full width. It drops to one when the container is narrower than 28rem. |
24
+ | `className` | `string` | | On the container. |
25
+ | `ref` | `Ref<HTMLDivElement>` | | Forwarded to the container. |
26
+ | `term (DescriptionListItem)` | `string` | | Required. The field name: "Serial". |
27
+ | `description (DescriptionListItem)` | `ReactNode` | | Required. Its value. |
28
+
29
+ ## Types and helpers
30
+
31
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
32
+
33
+ ```ts
34
+ export type DescriptionListItem = {
35
+ /** The name of the field: "Serial", "Last seen". Shown as a mono label. */
36
+ term: string
37
+ /** Its value: text, a Badge, a Link. */
38
+ description: ReactNode
39
+ }
40
+ ```
41
+
42
+ ## Examples
43
+
44
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
45
+
46
+ ### One column
47
+
48
+ ```tsx
49
+ <div className="w-full max-w-sm">
50
+ <DescriptionList
51
+ items={[
52
+ { term: 'Serial', description: 'HS-4471-B' },
53
+ { term: 'Status', description: <Badge tone="ok">Online</Badge> },
54
+ { term: 'Firmware', description: '2.8.1' },
55
+ { term: 'Last seen', description: '15 Oct, 09:58' },
56
+ ]}
57
+ />
58
+ </div>
59
+ ```
60
+
61
+ ### Three columns, fewer as the container narrows
62
+
63
+ ```tsx
64
+ <div className="w-full">
65
+ <DescriptionList
66
+ columns={3}
67
+ items={[
68
+ { term: 'Order', description: 'ORD-2291' },
69
+ { term: 'Placed', description: '12 Oct 2026' },
70
+ { term: 'Ships', description: '16 Oct 2026' },
71
+ { term: 'Depot', description: 'Cluj, bay 4' },
72
+ { term: 'Carrier', description: 'Regular courier' },
73
+ { term: 'Weight', description: '22.1 kg' },
74
+ ]}
75
+ />
76
+ </div>
77
+ ```
78
+
79
+ ## Keyboard
80
+
81
+ | Keys | Result |
82
+ |---|---|
83
+ | `Tab` | Moves to any link or button inside a description. |
84
+
85
+ ## Do
86
+
87
+ - Order pairs the way someone reads the record: identity first, then state, then dates.
88
+ - Say "None" or "Not set" for an empty value rather than leaving the description blank.
89
+ - Use two or three columns only for short values; long text reads better in one.
90
+
91
+ ## Don't
92
+
93
+ - Don't use it for a list of records. That's what DataTable is for.
94
+ - Don't put a form field in a description. A record people can edit is a form.
95
+ - Don't repeat a term inside one list.
96
+
97
+ ## Anatomy
98
+
99
+ - **Container**: A div with @container (a container query), so the columns follow the width of this box, not of the screen.
100
+ - **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.
101
+ - **Pair**: A div holding one dt (the term) and one dd (its value), with a thin line above.
102
+ - **Term**: The dt, styled as a label: monospaced, uppercase, 11px (pixels).
103
+ - **Description**: The dd, in body text. It holds text, a Badge or a Link.