division-ds 0.6.0 → 0.7.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 +1 -1
- package/package.json +1 -1
- package/public/docs/components/badge.md +39 -3
- package/public/docs/components/button.md +18 -1
- package/public/docs/components/kanban-board.md +110 -0
- package/public/docs/components/link.md +4 -1
- package/public/docs/components/multi-select.md +37 -0
- package/public/docs/components/select.md +12 -0
- package/public/docs/components/terminal.md +3 -0
- package/public/manifest/components.json +103 -9
- package/public/manifest/tokens.json +210 -0
package/AGENTS.md
CHANGED
|
@@ -51,7 +51,7 @@ installs these rules into that project's `AGENTS.md`, `GEMINI.md` and Cursor rul
|
|
|
51
51
|
`.claude/skills/` (Claude Code).
|
|
52
52
|
|
|
53
53
|
An agent that supports MCP can have the docs and the same checks as tools instead:
|
|
54
|
-
`claude mcp add division-ds -- npx -y division-ds@0.
|
|
54
|
+
`claude mcp add division-ds -- npx -y division-ds@0.7.1 mcp` (`list_components`, `get_component`,
|
|
55
55
|
`search`, `get_tokens`, `validate_code`, `check_contrast`, `get_add_command`,
|
|
56
56
|
`get_review_checklist`).
|
|
57
57
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "division-ds",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Checks a project that uses Division DS components (raw colours, invented tokens, touching buttons, unknown props), and serves the docs and the same checks to agents over MCP.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Victor Ilioiu",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> A small chip that shows a status. It comes in six tones and two emphases, and it always carries a word. A colour on its own isn't a status anyone can read.
|
|
4
4
|
|
|
5
|
-
- **Best for:** You need to label a status with a word and a tone, like an order state or a sync result.
|
|
5
|
+
- **Best for:** You need to label a status with a word and a tone, like an order state or a sync result. Or a label someone made, like a project tag: then pass color instead of tone.
|
|
6
6
|
- **Not for:** A filter or selection someone can remove. Use Tag for that.
|
|
7
7
|
- Atom. Source: `src/components/atoms/badge/Badge.tsx`
|
|
8
8
|
- Rendered docs page: `/components/badge`
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
## Import
|
|
11
11
|
|
|
12
12
|
```tsx
|
|
13
|
-
import { Badge, badgeVariants } from 'src/components/atoms/badge/Badge'
|
|
13
|
+
import { Badge, badgeVariants, CATEGORY_COLORS } from 'src/components/atoms/badge/Badge'
|
|
14
14
|
```
|
|
15
15
|
|
|
16
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. An install has no index barrels: import each component from its own file, never from `components/molecules`.
|
|
@@ -19,7 +19,8 @@ Paths are from the Division DS repository root. Installed with shadcn they start
|
|
|
19
19
|
|
|
20
20
|
| Prop | Type | Default | Description |
|
|
21
21
|
|---|---|---|---|
|
|
22
|
-
| `tone` | `'neutral' \| 'signal' \| 'ok' \| 'warning' \| 'hostile' \| 'info'` | `'neutral'` | The status the badge shows. |
|
|
22
|
+
| `tone` | `'neutral' \| 'signal' \| 'ok' \| 'warning' \| 'hostile' \| 'info'` | `'neutral'` | The status the badge shows. Leave it out when you pass color. |
|
|
23
|
+
| `color` | `'gray' \| 'brown' \| 'orange' \| 'yellow' \| 'green' \| 'teal' \| 'blue' \| 'purple' \| 'pink' \| 'red'` | | A category colour for a label people make themselves, like a project tag or a topic. It replaces tone: the types reject passing both. A category colour means 'these belong together', never good or bad. The list is exported as CATEGORY_COLORS, so an app can give each new label one at random. Works with both emphases and both sizes. |
|
|
23
24
|
| `emphasis` | `'solid' \| 'subtle'` | `'subtle'` | Solid uses the invariant fill (the same in light and dark themes) with its matching ink. Subtle is the outline badge: a 1px border and text in the tone’s theme-adaptive text token, with no fill. There's no separate outline emphasis, because it would be this same look under a second name. |
|
|
24
25
|
| `size` | `'sm' \| 'md'` | `'sm'` | sm (small) is the chip for everyday interface: rows, cards, tables. md (medium) takes the small control height (32px) and the 14px text step. Use it for a badge next to display-size type, like a price. |
|
|
25
26
|
| `dot` | `boolean` | `false` | Adds a marker next to the text. |
|
|
@@ -36,6 +37,18 @@ Exported beside the component, from the same file. Import a type with `import ty
|
|
|
36
37
|
export const badgeVariants = cva( …
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
```ts
|
|
41
|
+
/** Categorical colours for labels people make (a project tag, a topic), in the order a picker
|
|
42
|
+
* shows them. Not statuses: a category colour says "same group", never good or bad. Exported so
|
|
43
|
+
* an app can give each new label one, for example
|
|
44
|
+
* `CATEGORY_COLORS[Math.floor(Math.random() * CATEGORY_COLORS.length)]`. */
|
|
45
|
+
export const CATEGORY_COLORS = ['gray', 'brown', 'orange', 'yellow', 'green', 'teal', 'blue', 'purple', 'pink', 'red'] as const
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
export type CategoryColor = (typeof CATEGORY_COLORS)[number]
|
|
50
|
+
```
|
|
51
|
+
|
|
39
52
|
## Examples
|
|
40
53
|
|
|
41
54
|
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
@@ -64,6 +77,26 @@ const TONES = ['neutral', 'signal', 'ok', 'warning', 'hostile', 'info'] as const
|
|
|
64
77
|
))}
|
|
65
78
|
```
|
|
66
79
|
|
|
80
|
+
### Category colours, subtle: labels people make, not statuses
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
{CATEGORY_COLORS.map(color => (
|
|
84
|
+
<Badge key={color} color={color}>
|
|
85
|
+
{color}
|
|
86
|
+
</Badge>
|
|
87
|
+
))}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Category colours, solid
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
{CATEGORY_COLORS.map(color => (
|
|
94
|
+
<Badge key={color} color={color} emphasis="solid">
|
|
95
|
+
{color}
|
|
96
|
+
</Badge>
|
|
97
|
+
))}
|
|
98
|
+
```
|
|
99
|
+
|
|
67
100
|
### With a dot
|
|
68
101
|
|
|
69
102
|
```tsx
|
|
@@ -158,12 +191,15 @@ Not focusable. No keyboard behaviour of its own.
|
|
|
158
191
|
- Go easy on solid. One solid badge in a row reads as the exception. Six read as noise.
|
|
159
192
|
- Keep the tone mapping the same across the product. ok is never ‘in progress’ on one screen and ‘done’ on another.
|
|
160
193
|
- Use size md when the badge sits next to a headline figure, so the two read as one statement.
|
|
194
|
+
- Use tone for a status the system knows (paid, failed, syncing) and color for a label a person made (Design, Q3, Home). A colour on its own is not a status.
|
|
195
|
+
- Store the colour with the label, so it stays the same on every screen. Pick one from CATEGORY_COLORS when the label is created.
|
|
161
196
|
|
|
162
197
|
## Don't
|
|
163
198
|
|
|
164
199
|
- Don't build a badge that's colour only, even with a tooltip.
|
|
165
200
|
- Don't put a badge inside a button. It muddies what the clickable target is.
|
|
166
201
|
- Don't use hostile for something that's only pending.
|
|
202
|
+
- Don't use a category colour to show a status. A red project label is not an error, and an error is tone="hostile", so it means the same thing on every screen.
|
|
167
203
|
- Don't bevel it (give it a cut corner). At 22px a badge is chrome, the frame around content, not an object with its own footprint. Kbd follows the same rule for the same reason.
|
|
168
204
|
- Don't resize a badge with height or text classes. The two sizes are the prop. A third size is a change to the system.
|
|
169
205
|
- Don't use size md in a table row or on a card. It's as tall as a small button, so it starts to look like one.
|
|
@@ -20,6 +20,7 @@ Paths are from the Division DS repository root. Installed with shadcn they start
|
|
|
20
20
|
| Prop | Type | Default | Description |
|
|
21
21
|
|---|---|---|---|
|
|
22
22
|
| `variant` | `'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'link'` | `'primary'` | Which fill/border treatment to use. link looks like a Link (signal text, underline, no fill, no bevel, no fixed height) but it's still a button. Use it for an action inside a sentence, not for going to another page. |
|
|
23
|
+
| `tone` | `'signal' \| 'neutral'` | `'signal'` | Only the link variant reads it. signal is the signal-coloured, underlined link. neutral is primary text with no underline until hover or keyboard focus: use it for a clickable title on a kanban card or a list row, where a column of signal links would be too loud. A link-variant label wraps onto more lines rather than overflowing its column. |
|
|
23
24
|
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg'` | `'md'` | 24 / 32 / 40 / 48px (pixels). xs is exactly the 24px minimum target, the smallest area people can reliably click or tap. It isn't a token step (there is no xs control-height token) and it's meant for dense toolbars and table rows. Size has no effect on the link variant, which takes the size of the text around it. |
|
|
24
25
|
| `fullWidth` | `boolean` | `false` | Stretches the wrapper as well as the button. |
|
|
25
26
|
| `loading` | `boolean` | `false` | Sets aria-busy and aria-disabled, blocks the click and the form submit, and shows the spinner. The button stays focusable. |
|
|
@@ -100,6 +101,21 @@ Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
|
100
101
|
</p>
|
|
101
102
|
```
|
|
102
103
|
|
|
104
|
+
### Neutral link: a kanban card title
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
<div className="w-64 flex items-start gap-2 p-3 border border-[var(--color-border-hairline)] bg-[var(--color-surface-raised)]">
|
|
108
|
+
<div className="min-w-0 flex-1 text-sm">
|
|
109
|
+
<Button variant="link" tone="neutral">
|
|
110
|
+
Replace the hop-up bucking on the second-line support rifles before Saturday
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
<Button variant="ghost" size="xs">
|
|
114
|
+
Move
|
|
115
|
+
</Button>
|
|
116
|
+
</div>
|
|
117
|
+
```
|
|
118
|
+
|
|
103
119
|
### Full width
|
|
104
120
|
|
|
105
121
|
```tsx
|
|
@@ -147,6 +163,7 @@ Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
|
147
163
|
- Use loading on a submit button. It keeps focus while the form works, so a keyboard user is never thrown back to the top of the page.
|
|
148
164
|
- Pass icons through iconStart and iconEnd, not as children. That way they stay hidden from assistive tech and get sized with the button.
|
|
149
165
|
- Use the link variant for an action that sits in running text or next to a heading (‘Reset filters’). Use Link for anything that goes somewhere.
|
|
166
|
+
- Use variant="link" tone="neutral" for a card or row title that opens something on this page, such as the detail panel of a kanban card.
|
|
150
167
|
|
|
151
168
|
## Don't
|
|
152
169
|
|
|
@@ -200,5 +217,5 @@ A busy button stays focusable and tells people it's busy through loading. Native
|
|
|
200
217
|
- **Focus wrapper**: A span that isn't clipped, carrying .bevel-focus-wrap. It draws the ring on :has(:focus-visible), so a mouse click doesn't show it, plus the reticle brackets (corner marks like a targeting sight) that close in on hover. It's always there and never optional.
|
|
201
218
|
- **Button**: The clipped, bevelled control. It never carries its own outline.
|
|
202
219
|
- **Spinner**: Decoration only, shown while loading. The hidden label is what screen readers announce.
|
|
203
|
-
- **Label**: Oxanium semibold, uppercase: the display type role. The link variant switches to the body role, in the signal text colour, underlined.
|
|
220
|
+
- **Label**: Oxanium semibold, uppercase: the display type role. The link variant switches to the body role, in the signal text colour, underlined (or primary text, underlined on hover and focus, with tone="neutral"), and wraps instead of staying on one line.
|
|
204
221
|
- **Icons**: Optional, before and after the label. Each sits in an aria-hidden slot sized to the button (14px at xs and sm, 16px at md, 20px at lg). aria-hidden is an ARIA attribute (Accessible Rich Internet Applications, the labels that tell screen readers what things are) that hides the icon from screen readers. While loading, the spinner takes the leading slot.
|
|
@@ -31,6 +31,7 @@ Paths are from the Division DS repository root. Installed with shadcn they start
|
|
|
31
31
|
| `getItemLabel` | `(item: T) => string` | | Required. The card in plain words. The Move control, its menu and every screen reader announcement use it. |
|
|
32
32
|
| `renderCard` | `(item: T) => ReactNode` | | Required. The card's body. It can hold one link or button. The board draws the frame. |
|
|
33
33
|
| `renderCardCorner` | `(item: T) => ReactNode` | | A small mark in the card's bottom-right corner, under the Move control, about 2rem square, such as the owner's avatar. Wrap it in aria-hidden (so screen readers skip it) when the card already prints the same name. |
|
|
34
|
+
| `renderCardMenu` | `(item: T, moveTo: (columnId: string) => void, columnId: string) => ReactNode` | | Replaces the Move menu's default items (the card's name and a Move to item for each other column) with your own DropdownMenu parts, such as View, Edit or Delete. They go inside the board's menu. Call moveTo(columnId) to move the card the way the default menu does: to the end of that column, with onMove, the announcement and focus back on the Move control. columnId is the card's current column. |
|
|
34
35
|
| `onMove` | `(itemId: string, toColumnId: string, toIndex: number) => void` | | Required. Called once per move. toIndex is the card's position in the destination column after the move. A move from the menu puts it last. |
|
|
35
36
|
| `emptyColumnMessage` | `string` | `'Nothing here'` | Shown in a column with no cards. |
|
|
36
37
|
| `columnHeadingLevel` | `2 \| 3 \| 4` | `3` | The heading level of the column titles. Pick one that sits under the page's own heading outline. |
|
|
@@ -236,6 +237,115 @@ function WorkOrders() {
|
|
|
236
237
|
<WorkOrders />
|
|
237
238
|
```
|
|
238
239
|
|
|
240
|
+
### Your own card menu, with renderCardMenu
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
type Order = { id: string; title: string; bay: string; status: 'open' | 'parts' | 'done' }
|
|
244
|
+
|
|
245
|
+
const ORDER_COLUMNS: KanbanColumn[] = [
|
|
246
|
+
{ id: 'open', title: 'Open' },
|
|
247
|
+
{ id: 'parts', title: 'Waiting for parts' },
|
|
248
|
+
{ id: 'done', title: 'Done' },
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
const ORDERS: Order[] = [
|
|
252
|
+
{ id: 'wo-31', title: 'Replace dock door seal', bay: 'Bay 2', status: 'open' },
|
|
253
|
+
{ id: 'wo-32', title: 'Recalibrate floor scale', bay: 'Bay 5', status: 'open' },
|
|
254
|
+
{ id: 'wo-29', title: 'Service pallet jack', bay: 'Bay 1', status: 'done' },
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
const orderAccessors = {
|
|
258
|
+
getItemId: (order: Order) => order.id,
|
|
259
|
+
getColumnId: (order: Order) => order.status,
|
|
260
|
+
setColumnId: (order: Order, status: string) => ({ ...order, status: status as Order['status'] }),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const ORDER_LABELS = ['Urgent', 'Safety', 'Contractor'] as const
|
|
264
|
+
|
|
265
|
+
function WorkOrdersWithMenu() {
|
|
266
|
+
const [orders, setOrders] = useState(ORDERS)
|
|
267
|
+
const [labels, setLabels] = useState<Record<string, string[]>>({ 'wo-31': ['Safety'] })
|
|
268
|
+
const [note, setNote] = useState('')
|
|
269
|
+
const toggleLabel = (id: string, label: string, on: boolean) =>
|
|
270
|
+
setLabels(current => ({
|
|
271
|
+
...current,
|
|
272
|
+
[id]: on ? [...(current[id] ?? []), label] : (current[id] ?? []).filter(other => other !== label),
|
|
273
|
+
}))
|
|
274
|
+
return (
|
|
275
|
+
<div className="w-full min-w-0 flex flex-col gap-3">
|
|
276
|
+
<KanbanBoard
|
|
277
|
+
label="Depot work orders, with a card menu"
|
|
278
|
+
columns={ORDER_COLUMNS}
|
|
279
|
+
items={orders}
|
|
280
|
+
getItemId={orderAccessors.getItemId}
|
|
281
|
+
getColumnId={orderAccessors.getColumnId}
|
|
282
|
+
getItemLabel={order => order.title}
|
|
283
|
+
renderCard={order => (
|
|
284
|
+
<div className="flex flex-col gap-1">
|
|
285
|
+
<p className="font-semibold">{order.title}</p>
|
|
286
|
+
<p className="font-mono text-xs uppercase tracking-[0.12em] text-[var(--color-text-secondary)]">
|
|
287
|
+
{[order.bay, ...(labels[order.id] ?? [])].join(' · ')}
|
|
288
|
+
</p>
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
renderCardMenu={(order, moveTo, columnId) => (
|
|
292
|
+
<>
|
|
293
|
+
<DropdownMenuItem icon={<Eye />} onSelect={() => setNote(`Opened ${order.title}.`)}>
|
|
294
|
+
View
|
|
295
|
+
</DropdownMenuItem>
|
|
296
|
+
<DropdownMenuItem icon={<Pencil />} onSelect={() => setNote(`Editing ${order.title}.`)}>
|
|
297
|
+
Edit
|
|
298
|
+
</DropdownMenuItem>
|
|
299
|
+
<DropdownMenuSub>
|
|
300
|
+
<DropdownMenuSubTrigger icon={<Tag />}>Labels</DropdownMenuSubTrigger>
|
|
301
|
+
<DropdownMenuSubContent>
|
|
302
|
+
{ORDER_LABELS.map(label => (
|
|
303
|
+
<DropdownMenuCheckboxItem
|
|
304
|
+
key={label}
|
|
305
|
+
checked={(labels[order.id] ?? []).includes(label)}
|
|
306
|
+
onCheckedChange={on => toggleLabel(order.id, label, on === true)}
|
|
307
|
+
onSelect={event => event.preventDefault()}
|
|
308
|
+
>
|
|
309
|
+
{label}
|
|
310
|
+
</DropdownMenuCheckboxItem>
|
|
311
|
+
))}
|
|
312
|
+
</DropdownMenuSubContent>
|
|
313
|
+
</DropdownMenuSub>
|
|
314
|
+
<DropdownMenuSub>
|
|
315
|
+
<DropdownMenuSubTrigger icon={<ArrowRightLeft />}>Move to</DropdownMenuSubTrigger>
|
|
316
|
+
<DropdownMenuSubContent>
|
|
317
|
+
{ORDER_COLUMNS.filter(column => column.id !== columnId).map(column => (
|
|
318
|
+
<DropdownMenuItem key={column.id} onSelect={() => moveTo(column.id)}>
|
|
319
|
+
{column.title}
|
|
320
|
+
</DropdownMenuItem>
|
|
321
|
+
))}
|
|
322
|
+
</DropdownMenuSubContent>
|
|
323
|
+
</DropdownMenuSub>
|
|
324
|
+
<DropdownMenuSeparator />
|
|
325
|
+
<DropdownMenuItem
|
|
326
|
+
tone="hostile"
|
|
327
|
+
icon={<Trash2 />}
|
|
328
|
+
onSelect={() => {
|
|
329
|
+
setOrders(current => current.filter(other => other.id !== order.id))
|
|
330
|
+
setNote(`Deleted ${order.title}.`)
|
|
331
|
+
}}
|
|
332
|
+
>
|
|
333
|
+
Delete
|
|
334
|
+
</DropdownMenuItem>
|
|
335
|
+
</>
|
|
336
|
+
)}
|
|
337
|
+
onMove={(id, status, index) => setOrders(current => applyKanbanMove(current, id, status, index, orderAccessors))}
|
|
338
|
+
emptyColumnMessage="No orders waiting"
|
|
339
|
+
columnHeadingLevel={4}
|
|
340
|
+
/>
|
|
341
|
+
<p className={docsProse}>{note}</p>
|
|
342
|
+
</div>
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
<WorkOrdersWithMenu />
|
|
347
|
+
```
|
|
348
|
+
|
|
239
349
|
## Keyboard
|
|
240
350
|
|
|
241
351
|
| Keys | Result |
|
|
@@ -19,7 +19,7 @@ Paths are from the Division DS repository root. Installed with shadcn they start
|
|
|
19
19
|
|
|
20
20
|
| Prop | Type | Default | Description |
|
|
21
21
|
|---|---|---|---|
|
|
22
|
-
| `tone` | `'signal' \| 'quiet'` | `'signal'` | Which colour treatment to use. |
|
|
22
|
+
| `tone` | `'signal' \| 'quiet' \| 'neutral'` | `'signal'` | Which colour treatment to use. neutral is primary text with no underline until hover or keyboard focus, for a clickable title in a list or on a card, where it is clear from position that the title is a link. Keep signal for links inside running text. |
|
|
23
23
|
| `external` | `boolean` | `false` | Opens the link in a new tab. It adds target="_blank", the rel hardening (attributes that stop the new page reaching back into yours), a trailing icon and the hidden "(opens in a new tab)" warning. Using it together with target or rel is a type error, so it can't fail silently. external already sets those values, so those props are removed from the type instead of being quietly overwritten while the page runs. |
|
|
24
24
|
| `href` | `string` | | Where the link goes. |
|
|
25
25
|
| `className` | `string` | | Merged with the component classes. If two clash, the later Tailwind utility wins (Tailwind is the CSS framework this system styles with). |
|
|
@@ -59,6 +59,9 @@ Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
|
59
59
|
<Link href="/kits" tone="quiet">
|
|
60
60
|
Field kits
|
|
61
61
|
</Link>
|
|
62
|
+
<Link href="/kits" tone="neutral">
|
|
63
|
+
Field kits
|
|
64
|
+
</Link>
|
|
62
65
|
```
|
|
63
66
|
|
|
64
67
|
### External
|
|
@@ -30,6 +30,9 @@ Paths are from the Division DS repository root. Installed with shadcn they start
|
|
|
30
30
|
| `onValueChange` | `(value: string[]) => void` | | Runs with the whole new array every time an option is added or removed. |
|
|
31
31
|
| `placeholder` | `string` | `'Select…'` | Shown while nothing is chosen. |
|
|
32
32
|
| `emptyMessage` | `string` | `'No matches'` | Shown in the panel when the typed text matches no option. |
|
|
33
|
+
| `creatable` | `boolean` | `false` | Adds one last row, `Create "<text>"`, when the typed text matches no option (ignoring case). It is never shown for empty text or for text that is already an option. It is an ordinary option in the list: the arrows reach it, Enter or a click chooses it, and screen readers read its label. |
|
|
34
|
+
| `onCreate` | `(text: string) => void` | | Runs with the typed text, trimmed, when the create row is chosen. The text field is then cleared. The component does not add anything itself: add the new option to options and its value to value here. |
|
|
35
|
+
| `createLabel` | `(text: string) => string` | `(text) => `Create "${text}"`` | The create row's label, for other wording or another language. |
|
|
33
36
|
| `inputSize` | `'sm' \| 'md' \| 'lg'` | `'md'` | Matches Input and Select: a minimum height of 32, 40 or 48px (pixels), with text at size xs, sm or base. It also sets the size of the typed text and of the option rows in the list (28, 32 or 40px). The tags come in one size, so sm fits a single row of them snugly, and md and lg add room around it. The field grows past the minimum as tags wrap. |
|
|
34
37
|
| `invalid` | `boolean` | `false` | Sets aria-invalid (which tells screen readers the value has a problem) and the invalid border. FormField sets it from error. |
|
|
35
38
|
| `disabled` | `boolean` | `false` | Turns the field off. The chosen tags stay visible, without their remove buttons. |
|
|
@@ -94,6 +97,39 @@ function ControlledExample() {
|
|
|
94
97
|
<ControlledExample />
|
|
95
98
|
```
|
|
96
99
|
|
|
100
|
+
### Creatable: pick or create labels
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
const LABELS = [
|
|
104
|
+
{ value: 'bug', label: 'Bug' },
|
|
105
|
+
{ value: 'design', label: 'Design' },
|
|
106
|
+
{ value: 'docs', label: 'Docs' },
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
function CreatableExample() {
|
|
110
|
+
const [options, setOptions] = useState(LABELS)
|
|
111
|
+
const [labels, setLabels] = useState<string[]>(['design'])
|
|
112
|
+
return (
|
|
113
|
+
<div className="w-full max-w-md">
|
|
114
|
+
<FormField label="Labels" help="Type a label that is not in the list, then press Enter to create it.">
|
|
115
|
+
<MultiSelect
|
|
116
|
+
options={options}
|
|
117
|
+
value={labels}
|
|
118
|
+
onValueChange={setLabels}
|
|
119
|
+
creatable
|
|
120
|
+
onCreate={text => {
|
|
121
|
+
setOptions(current => [...current, { value: text, label: text }])
|
|
122
|
+
setLabels(current => [...current, text])
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
</FormField>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
<CreatableExample />
|
|
131
|
+
```
|
|
132
|
+
|
|
97
133
|
### Invalid
|
|
98
134
|
|
|
99
135
|
```tsx
|
|
@@ -161,6 +197,7 @@ const LOADOUT = [
|
|
|
161
197
|
|---|---|
|
|
162
198
|
| `↓` `↑` | Opens the list, then moves the highlight, skipping rows that are turned off. |
|
|
163
199
|
| `Enter` | Adds or removes the highlighted option. The list stays open. |
|
|
200
|
+
| `Enter` | On the create row (creatable): runs onCreate with the typed text and clears the text field. |
|
|
164
201
|
| `Backspace` | When the text field is empty, removes the last tag. |
|
|
165
202
|
| `Esc` | Closes the list. Press it again to clear the typed text. |
|
|
166
203
|
| `Tab` | Moves through the tags’ remove buttons and the text field, and closes the list on the way out. |
|
|
@@ -116,6 +116,18 @@ const DELIVERY_GROUPS: SelectGroup[] = [
|
|
|
116
116
|
<Select label="Delivery" groups={DELIVERY_GROUPS} placeholder="Choose delivery" />
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
### Long list
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
const MONTH_OPTIONS = Array.from({ length: 30 }, (_, i) => {
|
|
123
|
+
const date = new Date(Date.UTC(2026, 10 - i, 1))
|
|
124
|
+
const label = date.toLocaleDateString('en-GB', { month: 'long', year: 'numeric', timeZone: 'UTC' })
|
|
125
|
+
return { value: date.toISOString().slice(0, 7), label }
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
<Select label="Billing month" options={MONTH_OPTIONS} placeholder="Choose a month" />
|
|
129
|
+
```
|
|
130
|
+
|
|
119
131
|
### Sizes
|
|
120
132
|
|
|
121
133
|
```tsx
|
|
@@ -29,6 +29,7 @@ Paths are from the Division DS repository root. Installed with shadcn they start
|
|
|
29
29
|
| `exitCode` | `number` | | With status exited: shown beside the word, and a non-zero code turns it hostile. |
|
|
30
30
|
| `statusMessage` | `string` | | Detail beside the status, and announced with it: the reason for an error, the attempt while reconnecting. |
|
|
31
31
|
| `readOnly` | `boolean` | `false` | Blocks input and never fires onData. Selecting and copying keep working, and Tab and Escape leave the terminal as they do anywhere else. |
|
|
32
|
+
| `shiftEnter` | `'newline' \| 'default'` | `'default'` | What Shift+Enter sends. xterm sends the same carriage return as Enter, so a chat program inside submits. newline sends ESC then carriage return instead, which Claude Code and similar programs read as a new line. Not sent while readOnly or composing with an input method. |
|
|
32
33
|
| `announceOutput` | `boolean` | `false` | Reads output aloud through xterm’s live region, up to 20 lines at a time. For a single session a screen reader user is working in. Leave it off when several terminals share a page. |
|
|
33
34
|
| `onData` | `(data: string) => void` | | What the person typed or pasted, plus the replies a program asks the terminal for. Send it to the process. |
|
|
34
35
|
| `onResize` | `(size: { cols: number; rows: number }) => void` | | The new grid size, debounced by 100ms and only when cols or rows changed. It fires once after mounting. |
|
|
@@ -329,6 +330,7 @@ function SampleTerminal(props: Omit<TerminalProps, 'ref'>) {
|
|
|
329
330
|
| `Ctrl` `Shift` `L` | Moves focus out of the terminal to the next tab stop. Tab and Escape belong to the program, so this is the way out. |
|
|
330
331
|
| `Ctrl` `C` | With text selected, copies it and clears the selection. With nothing selected, goes to the program as an interrupt. |
|
|
331
332
|
| `Ctrl` `Shift` `C` | Copies the selection and keeps it. |
|
|
333
|
+
| `Shift` `Enter` | Sends Enter to the program, unless shiftEnter is newline, when it sends ESC then Enter: a new line in chat programs such as Claude Code. |
|
|
332
334
|
| `Ctrl` `V` | Pastes. Ctrl+Shift+V does the same. A program that wants a literal Ctrl+V does not get it. |
|
|
333
335
|
|
|
334
336
|
## Do
|
|
@@ -336,6 +338,7 @@ function SampleTerminal(props: Omit<TerminalProps, 'ref'>) {
|
|
|
336
338
|
- Give each terminal a label that says which session it is, so a page of them can be told apart.
|
|
337
339
|
- Set readOnly when nobody can usefully type: a finished process, a session someone else owns.
|
|
338
340
|
- Say why in statusMessage when status is error.
|
|
341
|
+
- Set shiftEnter="newline" when the session runs a chat program, such as Claude Code, where Shift+Enter should start a new line.
|
|
339
342
|
- Use write’s callback to pause the source when output arrives faster than the page can draw it.
|
|
340
343
|
|
|
341
344
|
## Don't
|
|
@@ -266,7 +266,7 @@
|
|
|
266
266
|
"tier": "atoms",
|
|
267
267
|
"slug": "badge",
|
|
268
268
|
"summary": "A small chip that shows a status. It comes in six tones and two emphases, and it always carries a word. A colour on its own isn't a status anyone can read.",
|
|
269
|
-
"useWhen": "You need to label a status with a word and a tone, like an order state or a sync result.",
|
|
269
|
+
"useWhen": "You need to label a status with a word and a tone, like an order state or a sync result. Or a label someone made, like a project tag: then pass color instead of tone.",
|
|
270
270
|
"notFor": "A filter or selection someone can remove. Use Tag for that.",
|
|
271
271
|
"source": "src/components/atoms/badge/Badge.tsx",
|
|
272
272
|
"docs": "/docs/components/badge.md",
|
|
@@ -274,7 +274,8 @@
|
|
|
274
274
|
{
|
|
275
275
|
"names": [
|
|
276
276
|
"Badge",
|
|
277
|
-
"badgeVariants"
|
|
277
|
+
"badgeVariants",
|
|
278
|
+
"CATEGORY_COLORS"
|
|
278
279
|
],
|
|
279
280
|
"from": "src/components/atoms/badge/Badge"
|
|
280
281
|
}
|
|
@@ -284,7 +285,12 @@
|
|
|
284
285
|
"name": "tone",
|
|
285
286
|
"type": "'neutral' | 'signal' | 'ok' | 'warning' | 'hostile' | 'info'",
|
|
286
287
|
"default": "'neutral'",
|
|
287
|
-
"description": "The status the badge shows."
|
|
288
|
+
"description": "The status the badge shows. Leave it out when you pass color."
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"name": "color",
|
|
292
|
+
"type": "'gray' | 'brown' | 'orange' | 'yellow' | 'green' | 'teal' | 'blue' | 'purple' | 'pink' | 'red'",
|
|
293
|
+
"description": "A category colour for a label people make themselves, like a project tag or a topic. It replaces tone: the types reject passing both. A category colour means 'these belong together', never good or bad. The list is exported as CATEGORY_COLORS, so an app can give each new label one at random. Works with both emphases and both sizes."
|
|
288
294
|
},
|
|
289
295
|
{
|
|
290
296
|
"name": "emphasis",
|
|
@@ -331,6 +337,18 @@
|
|
|
331
337
|
"kind": "const",
|
|
332
338
|
"code": "export const badgeVariants = cva( …",
|
|
333
339
|
"from": "src/components/atoms/badge/Badge"
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"name": "CATEGORY_COLORS",
|
|
343
|
+
"kind": "const",
|
|
344
|
+
"code": "/** Categorical colours for labels people make (a project tag, a topic), in the order a picker\n * shows them. Not statuses: a category colour says \"same group\", never good or bad. Exported so\n * an app can give each new label one, for example\n * `CATEGORY_COLORS[Math.floor(Math.random() * CATEGORY_COLORS.length)]`. */\nexport const CATEGORY_COLORS = ['gray', 'brown', 'orange', 'yellow', 'green', 'teal', 'blue', 'purple', 'pink', 'red'] as const",
|
|
345
|
+
"from": "src/components/atoms/badge/Badge"
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"name": "CategoryColor",
|
|
349
|
+
"kind": "type",
|
|
350
|
+
"code": "export type CategoryColor = (typeof CATEGORY_COLORS)[number]",
|
|
351
|
+
"from": "src/components/atoms/badge/Badge"
|
|
334
352
|
}
|
|
335
353
|
],
|
|
336
354
|
"keyboard": [],
|
|
@@ -338,12 +356,15 @@
|
|
|
338
356
|
"Give every badge a word.",
|
|
339
357
|
"Go easy on solid. One solid badge in a row reads as the exception. Six read as noise.",
|
|
340
358
|
"Keep the tone mapping the same across the product. ok is never ‘in progress’ on one screen and ‘done’ on another.",
|
|
341
|
-
"Use size md when the badge sits next to a headline figure, so the two read as one statement."
|
|
359
|
+
"Use size md when the badge sits next to a headline figure, so the two read as one statement.",
|
|
360
|
+
"Use tone for a status the system knows (paid, failed, syncing) and color for a label a person made (Design, Q3, Home). A colour on its own is not a status.",
|
|
361
|
+
"Store the colour with the label, so it stays the same on every screen. Pick one from CATEGORY_COLORS when the label is created."
|
|
342
362
|
],
|
|
343
363
|
"donts": [
|
|
344
364
|
"Don't build a badge that's colour only, even with a tooltip.",
|
|
345
365
|
"Don't put a badge inside a button. It muddies what the clickable target is.",
|
|
346
366
|
"Don't use hostile for something that's only pending.",
|
|
367
|
+
"Don't use a category colour to show a status. A red project label is not an error, and an error is tone=\"hostile\", so it means the same thing on every screen.",
|
|
347
368
|
"Don't bevel it (give it a cut corner). At 22px a badge is chrome, the frame around content, not an object with its own footprint. Kbd follows the same rule for the same reason.",
|
|
348
369
|
"Don't resize a badge with height or text classes. The two sizes are the prop. A third size is a change to the system.",
|
|
349
370
|
"Don't use size md in a table row or on a card. It's as tall as a small button, so it starts to look like one.",
|
|
@@ -377,6 +398,14 @@
|
|
|
377
398
|
"caption": "Tones, solid",
|
|
378
399
|
"source": "const TONES = ['neutral', 'signal', 'ok', 'warning', 'hostile', 'info'] as const\n\n{TONES.map(tone => (\n <Badge key={tone} tone={tone} emphasis=\"solid\">\n {tone}\n </Badge>\n))}"
|
|
379
400
|
},
|
|
401
|
+
{
|
|
402
|
+
"caption": "Category colours, subtle: labels people make, not statuses",
|
|
403
|
+
"source": "{CATEGORY_COLORS.map(color => (\n <Badge key={color} color={color}>\n {color}\n </Badge>\n))}"
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
"caption": "Category colours, solid",
|
|
407
|
+
"source": "{CATEGORY_COLORS.map(color => (\n <Badge key={color} color={color} emphasis=\"solid\">\n {color}\n </Badge>\n))}"
|
|
408
|
+
},
|
|
380
409
|
{
|
|
381
410
|
"caption": "With a dot",
|
|
382
411
|
"source": "<Badge tone=\"ok\" dot>\n Nominal\n</Badge>\n<Badge tone=\"warning\" dot>\n Degraded\n</Badge>\n<Badge tone=\"hostile\" dot>\n Offline\n</Badge>"
|
|
@@ -426,6 +455,12 @@
|
|
|
426
455
|
"default": "'primary'",
|
|
427
456
|
"description": "Which fill/border treatment to use. link looks like a Link (signal text, underline, no fill, no bevel, no fixed height) but it's still a button. Use it for an action inside a sentence, not for going to another page."
|
|
428
457
|
},
|
|
458
|
+
{
|
|
459
|
+
"name": "tone",
|
|
460
|
+
"type": "'signal' | 'neutral'",
|
|
461
|
+
"default": "'signal'",
|
|
462
|
+
"description": "Only the link variant reads it. signal is the signal-coloured, underlined link. neutral is primary text with no underline until hover or keyboard focus: use it for a clickable title on a kanban card or a list row, where a column of signal links would be too loud. A link-variant label wraps onto more lines rather than overflowing its column."
|
|
463
|
+
},
|
|
429
464
|
{
|
|
430
465
|
"name": "size",
|
|
431
466
|
"type": "'xs' | 'sm' | 'md' | 'lg'",
|
|
@@ -528,7 +563,8 @@
|
|
|
528
563
|
"Use loading instead of disabling the button and swapping the label yourself.",
|
|
529
564
|
"Use loading on a submit button. It keeps focus while the form works, so a keyboard user is never thrown back to the top of the page.",
|
|
530
565
|
"Pass icons through iconStart and iconEnd, not as children. That way they stay hidden from assistive tech and get sized with the button.",
|
|
531
|
-
"Use the link variant for an action that sits in running text or next to a heading (‘Reset filters’). Use Link for anything that goes somewhere."
|
|
566
|
+
"Use the link variant for an action that sits in running text or next to a heading (‘Reset filters’). Use Link for anything that goes somewhere.",
|
|
567
|
+
"Use variant=\"link\" tone=\"neutral\" for a card or row title that opens something on this page, such as the detail panel of a kanban card."
|
|
532
568
|
],
|
|
533
569
|
"donts": [
|
|
534
570
|
"Don't add bevel-* or an outline class yourself. You'll clip the ring off.",
|
|
@@ -567,7 +603,7 @@
|
|
|
567
603
|
},
|
|
568
604
|
{
|
|
569
605
|
"part": "Label",
|
|
570
|
-
"description": "Oxanium semibold, uppercase: the display type role. The link variant switches to the body role, in the signal text colour, underlined."
|
|
606
|
+
"description": "Oxanium semibold, uppercase: the display type role. The link variant switches to the body role, in the signal text colour, underlined (or primary text, underlined on hover and focus, with tone=\"neutral\"), and wraps instead of staying on one line."
|
|
571
607
|
},
|
|
572
608
|
{
|
|
573
609
|
"part": "Icons",
|
|
@@ -595,6 +631,10 @@
|
|
|
595
631
|
"caption": "Link variant",
|
|
596
632
|
"source": "<p className=\"text-sm text-[var(--color-text-secondary)] max-w-md\">\n No replicas match these filters.{' '}\n <Button variant=\"link\">\n Reset filters\n </Button>{' '}\n to see the full catalogue.\n</p>"
|
|
597
633
|
},
|
|
634
|
+
{
|
|
635
|
+
"caption": "Neutral link: a kanban card title",
|
|
636
|
+
"source": "<div className=\"w-64 flex items-start gap-2 p-3 border border-[var(--color-border-hairline)] bg-[var(--color-surface-raised)]\">\n <div className=\"min-w-0 flex-1 text-sm\">\n <Button variant=\"link\" tone=\"neutral\">\n Replace the hop-up bucking on the second-line support rifles before Saturday\n </Button>\n </div>\n <Button variant=\"ghost\" size=\"xs\">\n Move\n </Button>\n</div>"
|
|
637
|
+
},
|
|
598
638
|
{
|
|
599
639
|
"caption": "Full width",
|
|
600
640
|
"source": "<div className=\"w-80\">\n <Button fullWidth>Dispatch team</Button>\n</div>"
|
|
@@ -1327,9 +1367,9 @@
|
|
|
1327
1367
|
"props": [
|
|
1328
1368
|
{
|
|
1329
1369
|
"name": "tone",
|
|
1330
|
-
"type": "'signal' | 'quiet'",
|
|
1370
|
+
"type": "'signal' | 'quiet' | 'neutral'",
|
|
1331
1371
|
"default": "'signal'",
|
|
1332
|
-
"description": "Which colour treatment to use."
|
|
1372
|
+
"description": "Which colour treatment to use. neutral is primary text with no underline until hover or keyboard focus, for a clickable title in a list or on a card, where it is clear from position that the title is a link. Keep signal for links inside running text."
|
|
1333
1373
|
},
|
|
1334
1374
|
{
|
|
1335
1375
|
"name": "external",
|
|
@@ -1417,7 +1457,7 @@
|
|
|
1417
1457
|
},
|
|
1418
1458
|
{
|
|
1419
1459
|
"caption": "Tones",
|
|
1420
|
-
"source": "<Link href=\"/kits\" tone=\"signal\">\n Field kits\n</Link>\n<Link href=\"/kits\" tone=\"quiet\">\n Field kits\n</Link>"
|
|
1460
|
+
"source": "<Link href=\"/kits\" tone=\"signal\">\n Field kits\n</Link>\n<Link href=\"/kits\" tone=\"quiet\">\n Field kits\n</Link>\n<Link href=\"/kits\" tone=\"neutral\">\n Field kits\n</Link>"
|
|
1421
1461
|
},
|
|
1422
1462
|
{
|
|
1423
1463
|
"caption": "External",
|
|
@@ -2333,6 +2373,10 @@
|
|
|
2333
2373
|
"caption": "Groups with headings and icons",
|
|
2334
2374
|
"source": "const DELIVERY_GROUPS: SelectGroup[] = [\n {\n label: 'Courier',\n options: [\n { value: 'standard', label: 'Standard', icon: <Truck /> },\n { value: 'priority', label: 'Priority', icon: <Zap /> },\n ],\n },\n {\n label: 'Collect',\n options: [\n { value: 'depot', label: 'Brasov depot', icon: <Package /> },\n { value: 'field', label: 'At the field', icon: <Crosshair /> },\n ],\n },\n]\n\n<Select label=\"Delivery\" groups={DELIVERY_GROUPS} placeholder=\"Choose delivery\" />"
|
|
2335
2375
|
},
|
|
2376
|
+
{
|
|
2377
|
+
"caption": "Long list",
|
|
2378
|
+
"source": "const MONTH_OPTIONS = Array.from({ length: 30 }, (_, i) => {\n const date = new Date(Date.UTC(2026, 10 - i, 1))\n const label = date.toLocaleDateString('en-GB', { month: 'long', year: 'numeric', timeZone: 'UTC' })\n return { value: date.toISOString().slice(0, 7), label }\n})\n\n<Select label=\"Billing month\" options={MONTH_OPTIONS} placeholder=\"Choose a month\" />"
|
|
2379
|
+
},
|
|
2336
2380
|
{
|
|
2337
2381
|
"caption": "Sizes",
|
|
2338
2382
|
"source": "const TERRAIN_OPTIONS = [\n { value: 'woodland', label: 'Woodland' },\n { value: 'urban', label: 'Urban' },\n { value: 'desert', label: 'Desert' },\n { value: 'indoor', label: 'Indoor' },\n]\n\n<Select label=\"Terrain (small)\" inputSize=\"sm\" options={TERRAIN_OPTIONS} placeholder=\"Terrain\" />\n<Select label=\"Terrain (medium)\" inputSize=\"md\" options={TERRAIN_OPTIONS} placeholder=\"Terrain\" />\n<Select label=\"Terrain (large)\" inputSize=\"lg\" options={TERRAIN_OPTIONS} placeholder=\"Terrain\" />"
|
|
@@ -6283,6 +6327,23 @@
|
|
|
6283
6327
|
"default": "'No matches'",
|
|
6284
6328
|
"description": "Shown in the panel when the typed text matches no option."
|
|
6285
6329
|
},
|
|
6330
|
+
{
|
|
6331
|
+
"name": "creatable",
|
|
6332
|
+
"type": "boolean",
|
|
6333
|
+
"default": "false",
|
|
6334
|
+
"description": "Adds one last row, `Create \"<text>\"`, when the typed text matches no option (ignoring case). It is never shown for empty text or for text that is already an option. It is an ordinary option in the list: the arrows reach it, Enter or a click chooses it, and screen readers read its label."
|
|
6335
|
+
},
|
|
6336
|
+
{
|
|
6337
|
+
"name": "onCreate",
|
|
6338
|
+
"type": "(text: string) => void",
|
|
6339
|
+
"description": "Runs with the typed text, trimmed, when the create row is chosen. The text field is then cleared. The component does not add anything itself: add the new option to options and its value to value here."
|
|
6340
|
+
},
|
|
6341
|
+
{
|
|
6342
|
+
"name": "createLabel",
|
|
6343
|
+
"type": "(text: string) => string",
|
|
6344
|
+
"default": "(text) => `Create \"${text}\"`",
|
|
6345
|
+
"description": "The create row's label, for other wording or another language."
|
|
6346
|
+
},
|
|
6286
6347
|
{
|
|
6287
6348
|
"name": "inputSize",
|
|
6288
6349
|
"type": "'sm' | 'md' | 'lg'",
|
|
@@ -6347,6 +6408,12 @@
|
|
|
6347
6408
|
],
|
|
6348
6409
|
"does": "Adds or removes the highlighted option. The list stays open."
|
|
6349
6410
|
},
|
|
6411
|
+
{
|
|
6412
|
+
"keys": [
|
|
6413
|
+
"Enter"
|
|
6414
|
+
],
|
|
6415
|
+
"does": "On the create row (creatable): runs onCreate with the typed text and clears the text field."
|
|
6416
|
+
},
|
|
6350
6417
|
{
|
|
6351
6418
|
"keys": [
|
|
6352
6419
|
"Backspace"
|
|
@@ -6411,6 +6478,10 @@
|
|
|
6411
6478
|
"caption": "Controlled: the page holds the chosen options",
|
|
6412
6479
|
"source": "const LOADOUT = [\n { value: 'mags', label: 'Spare magazines' },\n { value: 'goggles', label: 'Full-seal goggles' },\n { value: 'mesh', label: 'Mesh face guard' },\n { value: 'radio', label: 'Team radio', disabled: true },\n { value: 'gloves', label: 'Gloves' },\n { value: 'loader', label: 'Speed loader' },\n { value: 'tracer', label: 'Tracer unit' },\n { value: 'pouch', label: 'Dump pouch' },\n]\n\nfunction ControlledExample() {\n const [kit, setKit] = useState<string[]>(['mags', 'goggles', 'gloves'])\n return (\n <div className=\"flex flex-col gap-2 w-full max-w-md\">\n <FormField label=\"Loadout\" help=\"Remove a tag and watch the others close the gap.\">\n <MultiSelect options={LOADOUT} value={kit} onValueChange={setKit} />\n </FormField>\n <span className=\"font-mono text-xs text-[var(--color-text-secondary)]\">value: [{kit.join(', ')}]</span>\n </div>\n )\n}\n\n<ControlledExample />"
|
|
6413
6480
|
},
|
|
6481
|
+
{
|
|
6482
|
+
"caption": "Creatable: pick or create labels",
|
|
6483
|
+
"source": "const LABELS = [\n { value: 'bug', label: 'Bug' },\n { value: 'design', label: 'Design' },\n { value: 'docs', label: 'Docs' },\n]\n\nfunction CreatableExample() {\n const [options, setOptions] = useState(LABELS)\n const [labels, setLabels] = useState<string[]>(['design'])\n return (\n <div className=\"w-full max-w-md\">\n <FormField label=\"Labels\" help=\"Type a label that is not in the list, then press Enter to create it.\">\n <MultiSelect\n options={options}\n value={labels}\n onValueChange={setLabels}\n creatable\n onCreate={text => {\n setOptions(current => [...current, { value: text, label: text }])\n setLabels(current => [...current, text])\n }}\n />\n </FormField>\n </div>\n )\n}\n\n<CreatableExample />"
|
|
6484
|
+
},
|
|
6414
6485
|
{
|
|
6415
6486
|
"caption": "Invalid",
|
|
6416
6487
|
"source": "const LOADOUT = [\n { value: 'mags', label: 'Spare magazines' },\n { value: 'goggles', label: 'Full-seal goggles' },\n { value: 'mesh', label: 'Mesh face guard' },\n { value: 'radio', label: 'Team radio', disabled: true },\n { value: 'gloves', label: 'Gloves' },\n { value: 'loader', label: 'Speed loader' },\n { value: 'tracer', label: 'Tracer unit' },\n { value: 'pouch', label: 'Dump pouch' },\n]\n\n<div className=\"w-full max-w-md\">\n <FormField label=\"Loadout\" error=\"Pick eye protection.\">\n <MultiSelect options={LOADOUT} defaultValue={['mags']} />\n </FormField>\n</div>"
|
|
@@ -11091,6 +11162,11 @@
|
|
|
11091
11162
|
"type": "(item: T) => ReactNode",
|
|
11092
11163
|
"description": "A small mark in the card's bottom-right corner, under the Move control, about 2rem square, such as the owner's avatar. Wrap it in aria-hidden (so screen readers skip it) when the card already prints the same name."
|
|
11093
11164
|
},
|
|
11165
|
+
{
|
|
11166
|
+
"name": "renderCardMenu",
|
|
11167
|
+
"type": "(item: T, moveTo: (columnId: string) => void, columnId: string) => ReactNode",
|
|
11168
|
+
"description": "Replaces the Move menu's default items (the card's name and a Move to item for each other column) with your own DropdownMenu parts, such as View, Edit or Delete. They go inside the board's menu. Call moveTo(columnId) to move the card the way the default menu does: to the end of that column, with onMove, the announcement and focus back on the Move control. columnId is the card's current column."
|
|
11169
|
+
},
|
|
11094
11170
|
{
|
|
11095
11171
|
"name": "onMove",
|
|
11096
11172
|
"type": "(itemId: string, toColumnId: string, toIndex: number) => void",
|
|
@@ -11271,6 +11347,10 @@
|
|
|
11271
11347
|
{
|
|
11272
11348
|
"caption": "Work orders, with an empty column",
|
|
11273
11349
|
"source": "type Order = { id: string; title: string; bay: string; status: 'open' | 'parts' | 'done' }\n\nconst ORDER_COLUMNS: KanbanColumn[] = [\n { id: 'open', title: 'Open' },\n { id: 'parts', title: 'Waiting for parts' },\n { id: 'done', title: 'Done' },\n]\n\nconst ORDERS: Order[] = [\n { id: 'wo-31', title: 'Replace dock door seal', bay: 'Bay 2', status: 'open' },\n { id: 'wo-32', title: 'Recalibrate floor scale', bay: 'Bay 5', status: 'open' },\n { id: 'wo-29', title: 'Service pallet jack', bay: 'Bay 1', status: 'done' },\n]\n\nconst orderAccessors = {\n getItemId: (order: Order) => order.id,\n getColumnId: (order: Order) => order.status,\n setColumnId: (order: Order, status: string) => ({ ...order, status: status as Order['status'] }),\n}\n\nfunction WorkOrders() {\n const [orders, setOrders] = useState(ORDERS)\n return (\n <div className=\"w-full min-w-0\">\n <KanbanBoard\n label=\"Depot work orders\"\n columns={ORDER_COLUMNS}\n items={orders}\n getItemId={orderAccessors.getItemId}\n getColumnId={orderAccessors.getColumnId}\n getItemLabel={order => order.title}\n renderCard={order => (\n <div className=\"flex flex-col gap-1\">\n <p className=\"font-semibold\">{order.title}</p>\n <p className=\"font-mono text-xs uppercase tracking-[0.12em] text-[var(--color-text-secondary)]\">{order.bay}</p>\n </div>\n )}\n onMove={(id, status, index) => setOrders(current => applyKanbanMove(current, id, status, index, orderAccessors))}\n emptyColumnMessage=\"No orders waiting\"\n columnHeadingLevel={4}\n />\n </div>\n )\n}\n\n<WorkOrders />"
|
|
11350
|
+
},
|
|
11351
|
+
{
|
|
11352
|
+
"caption": "Your own card menu, with renderCardMenu",
|
|
11353
|
+
"source": "type Order = { id: string; title: string; bay: string; status: 'open' | 'parts' | 'done' }\n\nconst ORDER_COLUMNS: KanbanColumn[] = [\n { id: 'open', title: 'Open' },\n { id: 'parts', title: 'Waiting for parts' },\n { id: 'done', title: 'Done' },\n]\n\nconst ORDERS: Order[] = [\n { id: 'wo-31', title: 'Replace dock door seal', bay: 'Bay 2', status: 'open' },\n { id: 'wo-32', title: 'Recalibrate floor scale', bay: 'Bay 5', status: 'open' },\n { id: 'wo-29', title: 'Service pallet jack', bay: 'Bay 1', status: 'done' },\n]\n\nconst orderAccessors = {\n getItemId: (order: Order) => order.id,\n getColumnId: (order: Order) => order.status,\n setColumnId: (order: Order, status: string) => ({ ...order, status: status as Order['status'] }),\n}\n\nconst ORDER_LABELS = ['Urgent', 'Safety', 'Contractor'] as const\n\nfunction WorkOrdersWithMenu() {\n const [orders, setOrders] = useState(ORDERS)\n const [labels, setLabels] = useState<Record<string, string[]>>({ 'wo-31': ['Safety'] })\n const [note, setNote] = useState('')\n const toggleLabel = (id: string, label: string, on: boolean) =>\n setLabels(current => ({\n ...current,\n [id]: on ? [...(current[id] ?? []), label] : (current[id] ?? []).filter(other => other !== label),\n }))\n return (\n <div className=\"w-full min-w-0 flex flex-col gap-3\">\n <KanbanBoard\n label=\"Depot work orders, with a card menu\"\n columns={ORDER_COLUMNS}\n items={orders}\n getItemId={orderAccessors.getItemId}\n getColumnId={orderAccessors.getColumnId}\n getItemLabel={order => order.title}\n renderCard={order => (\n <div className=\"flex flex-col gap-1\">\n <p className=\"font-semibold\">{order.title}</p>\n <p className=\"font-mono text-xs uppercase tracking-[0.12em] text-[var(--color-text-secondary)]\">\n {[order.bay, ...(labels[order.id] ?? [])].join(' · ')}\n </p>\n </div>\n )}\n renderCardMenu={(order, moveTo, columnId) => (\n <>\n <DropdownMenuItem icon={<Eye />} onSelect={() => setNote(`Opened ${order.title}.`)}>\n View\n </DropdownMenuItem>\n <DropdownMenuItem icon={<Pencil />} onSelect={() => setNote(`Editing ${order.title}.`)}>\n Edit\n </DropdownMenuItem>\n <DropdownMenuSub>\n <DropdownMenuSubTrigger icon={<Tag />}>Labels</DropdownMenuSubTrigger>\n <DropdownMenuSubContent>\n {ORDER_LABELS.map(label => (\n <DropdownMenuCheckboxItem\n key={label}\n checked={(labels[order.id] ?? []).includes(label)}\n onCheckedChange={on => toggleLabel(order.id, label, on === true)}\n onSelect={event => event.preventDefault()}\n >\n {label}\n </DropdownMenuCheckboxItem>\n ))}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n <DropdownMenuSub>\n <DropdownMenuSubTrigger icon={<ArrowRightLeft />}>Move to</DropdownMenuSubTrigger>\n <DropdownMenuSubContent>\n {ORDER_COLUMNS.filter(column => column.id !== columnId).map(column => (\n <DropdownMenuItem key={column.id} onSelect={() => moveTo(column.id)}>\n {column.title}\n </DropdownMenuItem>\n ))}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n tone=\"hostile\"\n icon={<Trash2 />}\n onSelect={() => {\n setOrders(current => current.filter(other => other.id !== order.id))\n setNote(`Deleted ${order.title}.`)\n }}\n >\n Delete\n </DropdownMenuItem>\n </>\n )}\n onMove={(id, status, index) => setOrders(current => applyKanbanMove(current, id, status, index, orderAccessors))}\n emptyColumnMessage=\"No orders waiting\"\n columnHeadingLevel={4}\n />\n <p className={docsProse}>{note}</p>\n </div>\n )\n}\n\n<WorkOrdersWithMenu />"
|
|
11274
11354
|
}
|
|
11275
11355
|
],
|
|
11276
11356
|
"closed": true
|
|
@@ -12103,6 +12183,12 @@
|
|
|
12103
12183
|
"default": "false",
|
|
12104
12184
|
"description": "Blocks input and never fires onData. Selecting and copying keep working, and Tab and Escape leave the terminal as they do anywhere else."
|
|
12105
12185
|
},
|
|
12186
|
+
{
|
|
12187
|
+
"name": "shiftEnter",
|
|
12188
|
+
"type": "'newline' | 'default'",
|
|
12189
|
+
"default": "'default'",
|
|
12190
|
+
"description": "What Shift+Enter sends. xterm sends the same carriage return as Enter, so a chat program inside submits. newline sends ESC then carriage return instead, which Claude Code and similar programs read as a new line. Not sent while readOnly or composing with an input method."
|
|
12191
|
+
},
|
|
12106
12192
|
{
|
|
12107
12193
|
"name": "announceOutput",
|
|
12108
12194
|
"type": "boolean",
|
|
@@ -12200,6 +12286,13 @@
|
|
|
12200
12286
|
],
|
|
12201
12287
|
"does": "Copies the selection and keeps it."
|
|
12202
12288
|
},
|
|
12289
|
+
{
|
|
12290
|
+
"keys": [
|
|
12291
|
+
"Shift",
|
|
12292
|
+
"Enter"
|
|
12293
|
+
],
|
|
12294
|
+
"does": "Sends Enter to the program, unless shiftEnter is newline, when it sends ESC then Enter: a new line in chat programs such as Claude Code."
|
|
12295
|
+
},
|
|
12203
12296
|
{
|
|
12204
12297
|
"keys": [
|
|
12205
12298
|
"Ctrl",
|
|
@@ -12212,6 +12305,7 @@
|
|
|
12212
12305
|
"Give each terminal a label that says which session it is, so a page of them can be told apart.",
|
|
12213
12306
|
"Set readOnly when nobody can usefully type: a finished process, a session someone else owns.",
|
|
12214
12307
|
"Say why in statusMessage when status is error.",
|
|
12308
|
+
"Set shiftEnter=\"newline\" when the session runs a chat program, such as Claude Code, where Shift+Enter should start a new line.",
|
|
12215
12309
|
"Use write’s callback to pause the source when output arrives faster than the page can draw it."
|
|
12216
12310
|
],
|
|
12217
12311
|
"donts": [
|
|
@@ -755,6 +755,216 @@
|
|
|
755
755
|
"light": "10.76:1 on accent.warning.fill"
|
|
756
756
|
}
|
|
757
757
|
},
|
|
758
|
+
{
|
|
759
|
+
"name": "--color-category-gray-text",
|
|
760
|
+
"layer": "semantic",
|
|
761
|
+
"value": {
|
|
762
|
+
"dark": "#a3a9b3",
|
|
763
|
+
"light": "#5c636e"
|
|
764
|
+
},
|
|
765
|
+
"use": "Subtle Badge color=\"gray\": text and border. A label, not a status."
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
"name": "--color-category-gray-fill",
|
|
769
|
+
"layer": "semantic",
|
|
770
|
+
"value": "#9aa1ab",
|
|
771
|
+
"use": "Solid Badge color=\"gray\": the fill."
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
"name": "--color-category-gray-ink",
|
|
775
|
+
"layer": "semantic",
|
|
776
|
+
"value": "#0a0602",
|
|
777
|
+
"use": "Text on category.gray.fill."
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
"name": "--color-category-brown-text",
|
|
781
|
+
"layer": "semantic",
|
|
782
|
+
"value": {
|
|
783
|
+
"dark": "#c9a383",
|
|
784
|
+
"light": "#7a5232"
|
|
785
|
+
},
|
|
786
|
+
"use": "Subtle Badge color=\"brown\": text and border. A label, not a status."
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
"name": "--color-category-brown-fill",
|
|
790
|
+
"layer": "semantic",
|
|
791
|
+
"value": "#b98a64",
|
|
792
|
+
"use": "Solid Badge color=\"brown\": the fill."
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
"name": "--color-category-brown-ink",
|
|
796
|
+
"layer": "semantic",
|
|
797
|
+
"value": "#0a0602",
|
|
798
|
+
"use": "Text on category.brown.fill."
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
"name": "--color-category-orange-text",
|
|
802
|
+
"layer": "semantic",
|
|
803
|
+
"value": {
|
|
804
|
+
"dark": "#f0a060",
|
|
805
|
+
"light": "#a34a00"
|
|
806
|
+
},
|
|
807
|
+
"use": "Subtle Badge color=\"orange\": text and border. A label, not a status."
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
"name": "--color-category-orange-fill",
|
|
811
|
+
"layer": "semantic",
|
|
812
|
+
"value": "#f0913a",
|
|
813
|
+
"use": "Solid Badge color=\"orange\": the fill."
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
"name": "--color-category-orange-ink",
|
|
817
|
+
"layer": "semantic",
|
|
818
|
+
"value": "#0a0602",
|
|
819
|
+
"use": "Text on category.orange.fill."
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
"name": "--color-category-yellow-text",
|
|
823
|
+
"layer": "semantic",
|
|
824
|
+
"value": {
|
|
825
|
+
"dark": "#e6c65a",
|
|
826
|
+
"light": "#7a5c00"
|
|
827
|
+
},
|
|
828
|
+
"use": "Subtle Badge color=\"yellow\": text and border. A label, not a status."
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
"name": "--color-category-yellow-fill",
|
|
832
|
+
"layer": "semantic",
|
|
833
|
+
"value": "#e8c341",
|
|
834
|
+
"use": "Solid Badge color=\"yellow\": the fill."
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
"name": "--color-category-yellow-ink",
|
|
838
|
+
"layer": "semantic",
|
|
839
|
+
"value": "#0a0602",
|
|
840
|
+
"use": "Text on category.yellow.fill."
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
"name": "--color-category-green-text",
|
|
844
|
+
"layer": "semantic",
|
|
845
|
+
"value": {
|
|
846
|
+
"dark": "#6fcf8f",
|
|
847
|
+
"light": "#1e6b3a"
|
|
848
|
+
},
|
|
849
|
+
"use": "Subtle Badge color=\"green\": text and border. A label, not a status."
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
"name": "--color-category-green-fill",
|
|
853
|
+
"layer": "semantic",
|
|
854
|
+
"value": "#4fbf7a",
|
|
855
|
+
"use": "Solid Badge color=\"green\": the fill."
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
"name": "--color-category-green-ink",
|
|
859
|
+
"layer": "semantic",
|
|
860
|
+
"value": "#0a0602",
|
|
861
|
+
"use": "Text on category.green.fill."
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
"name": "--color-category-teal-text",
|
|
865
|
+
"layer": "semantic",
|
|
866
|
+
"value": {
|
|
867
|
+
"dark": "#5ccfc4",
|
|
868
|
+
"light": "#0d6662"
|
|
869
|
+
},
|
|
870
|
+
"use": "Subtle Badge color=\"teal\": text and border. A label, not a status."
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
"name": "--color-category-teal-fill",
|
|
874
|
+
"layer": "semantic",
|
|
875
|
+
"value": "#3fbfb2",
|
|
876
|
+
"use": "Solid Badge color=\"teal\": the fill."
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
"name": "--color-category-teal-ink",
|
|
880
|
+
"layer": "semantic",
|
|
881
|
+
"value": "#0a0602",
|
|
882
|
+
"use": "Text on category.teal.fill."
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
"name": "--color-category-blue-text",
|
|
886
|
+
"layer": "semantic",
|
|
887
|
+
"value": {
|
|
888
|
+
"dark": "#7fb2ff",
|
|
889
|
+
"light": "#1f5fbf"
|
|
890
|
+
},
|
|
891
|
+
"use": "Subtle Badge color=\"blue\": text and border. A label, not a status."
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
"name": "--color-category-blue-fill",
|
|
895
|
+
"layer": "semantic",
|
|
896
|
+
"value": "#6fa3f5",
|
|
897
|
+
"use": "Solid Badge color=\"blue\": the fill."
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
"name": "--color-category-blue-ink",
|
|
901
|
+
"layer": "semantic",
|
|
902
|
+
"value": "#0a0602",
|
|
903
|
+
"use": "Text on category.blue.fill."
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
"name": "--color-category-purple-text",
|
|
907
|
+
"layer": "semantic",
|
|
908
|
+
"value": {
|
|
909
|
+
"dark": "#b59cf5",
|
|
910
|
+
"light": "#6a3fbf"
|
|
911
|
+
},
|
|
912
|
+
"use": "Subtle Badge color=\"purple\": text and border. A label, not a status."
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
"name": "--color-category-purple-fill",
|
|
916
|
+
"layer": "semantic",
|
|
917
|
+
"value": "#a88af0",
|
|
918
|
+
"use": "Solid Badge color=\"purple\": the fill."
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
"name": "--color-category-purple-ink",
|
|
922
|
+
"layer": "semantic",
|
|
923
|
+
"value": "#0a0602",
|
|
924
|
+
"use": "Text on category.purple.fill."
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
"name": "--color-category-pink-text",
|
|
928
|
+
"layer": "semantic",
|
|
929
|
+
"value": {
|
|
930
|
+
"dark": "#f08fc4",
|
|
931
|
+
"light": "#a3306f"
|
|
932
|
+
},
|
|
933
|
+
"use": "Subtle Badge color=\"pink\": text and border. A label, not a status."
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
"name": "--color-category-pink-fill",
|
|
937
|
+
"layer": "semantic",
|
|
938
|
+
"value": "#ec84bb",
|
|
939
|
+
"use": "Solid Badge color=\"pink\": the fill."
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
"name": "--color-category-pink-ink",
|
|
943
|
+
"layer": "semantic",
|
|
944
|
+
"value": "#0a0602",
|
|
945
|
+
"use": "Text on category.pink.fill."
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
"name": "--color-category-red-text",
|
|
949
|
+
"layer": "semantic",
|
|
950
|
+
"value": {
|
|
951
|
+
"dark": "#f58a8a",
|
|
952
|
+
"light": "#b42828"
|
|
953
|
+
},
|
|
954
|
+
"use": "Subtle Badge color=\"red\": text and border. A label, not a status."
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
"name": "--color-category-red-fill",
|
|
958
|
+
"layer": "semantic",
|
|
959
|
+
"value": "#f07a7a",
|
|
960
|
+
"use": "Solid Badge color=\"red\": the fill."
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
"name": "--color-category-red-ink",
|
|
964
|
+
"layer": "semantic",
|
|
965
|
+
"value": "#0a0602",
|
|
966
|
+
"use": "Text on category.red.fill."
|
|
967
|
+
},
|
|
758
968
|
{
|
|
759
969
|
"name": "--color-numeral-pad",
|
|
760
970
|
"layer": "semantic",
|