division-ds 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/public/docs/components/badge.md +30 -1
- package/public/docs/components/chart-panel.md +1 -1
- package/public/docs/components/progress.md +23 -2
- package/public/docs/components/select.md +115 -69
- package/public/manifest/components.json +67 -17
- package/public/manifest/tokens.json +126 -0
- package/scripts/lib/search-score.mjs +47 -24
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.10.0 mcp` (`list_components`, `get_component`,
|
|
55
55
|
`search`, `get_tokens`, `validate_code`, `check_contrast`, `get_add_command`,
|
|
56
56
|
`get_review_checklist`, and two that report back: `request_change` and `report_drift`, which open
|
|
57
57
|
an issue on the public repository instead of patching the installed copy).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "division-ds",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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",
|
|
@@ -20,7 +20,8 @@ Paths are from the Division DS repository root. Installed with shadcn they start
|
|
|
20
20
|
| Prop | Type | Default | Description |
|
|
21
21
|
|---|---|---|---|
|
|
22
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
|
+
| `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, or passing it with severity. 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. |
|
|
24
|
+
| `severity` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | | A step on the ordered severity scale, from 1 (lowest) to 6, for a measure with more levels than the four status tones, like air quality from Good to Hazardous. The fill goes from green to maroon. It replaces tone and color: the types reject passing two. The steps are exported as SEVERITY_STEPS. The badge still needs the level's word: the step's colour is never the message on its own. Works with both emphases and both sizes. |
|
|
24
25
|
| `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. |
|
|
25
26
|
| `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. |
|
|
26
27
|
| `dot` | `boolean` | `false` | Adds a marker next to the text. |
|
|
@@ -97,6 +98,30 @@ const TONES = ['neutral', 'signal', 'ok', 'warning', 'hostile', 'info'] as const
|
|
|
97
98
|
))}
|
|
98
99
|
```
|
|
99
100
|
|
|
101
|
+
### Severity, solid: the six air quality levels, each with its word
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
const AQI_LEVELS = ['Good', 'Moderate', 'Unhealthy for sensitive groups', 'Unhealthy', 'Very unhealthy', 'Hazardous'] as const
|
|
105
|
+
|
|
106
|
+
{SEVERITY_STEPS.map(step => (
|
|
107
|
+
<Badge key={step} severity={step} emphasis="solid">
|
|
108
|
+
{AQI_LEVELS[step - 1]}
|
|
109
|
+
</Badge>
|
|
110
|
+
))}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Severity, subtle
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
const AQI_LEVELS = ['Good', 'Moderate', 'Unhealthy for sensitive groups', 'Unhealthy', 'Very unhealthy', 'Hazardous'] as const
|
|
117
|
+
|
|
118
|
+
{SEVERITY_STEPS.map(step => (
|
|
119
|
+
<Badge key={step} severity={step}>
|
|
120
|
+
{AQI_LEVELS[step - 1]}
|
|
121
|
+
</Badge>
|
|
122
|
+
))}
|
|
123
|
+
```
|
|
124
|
+
|
|
100
125
|
### With a dot
|
|
101
126
|
|
|
102
127
|
```tsx
|
|
@@ -192,6 +217,8 @@ Not focusable. No keyboard behaviour of its own.
|
|
|
192
217
|
- Keep the tone mapping the same across the product. ok is never ‘in progress’ on one screen and ‘done’ on another.
|
|
193
218
|
- Use size md when the badge sits next to a headline figure, so the two read as one statement.
|
|
194
219
|
- 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.
|
|
220
|
+
- Use severity when a measure has more than four ordered levels, like air quality, so each level gets its own step and no two share a colour. Use the same step for the same level on every screen.
|
|
221
|
+
- Prefer solid for a severity scale: the fills darken step by step in both themes, so the order reads at a glance. In the dark theme the subtle text steps are ordered by hue, not lightness.
|
|
195
222
|
- Store the colour with the label, so it stays the same on every screen. Pick one from CATEGORY_COLORS when the label is created.
|
|
196
223
|
|
|
197
224
|
## Don't
|
|
@@ -200,6 +227,8 @@ Not focusable. No keyboard behaviour of its own.
|
|
|
200
227
|
- Don't put a badge inside a button. It muddies what the clickable target is.
|
|
201
228
|
- Don't use hostile for something that's only pending.
|
|
202
229
|
- 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.
|
|
230
|
+
- Don't squeeze a six-level scale into the four status tones. Two levels end up sharing a colour, and only the word tells them apart. That's what severity is for.
|
|
231
|
+
- Don't use severity for a status that isn't on a scale. A failed payment is tone="hostile", not severity={5}.
|
|
203
232
|
- 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.
|
|
204
233
|
- 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.
|
|
205
234
|
- 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.
|
|
@@ -34,7 +34,7 @@ Paths are from the Division DS repository root. Installed with shadcn they start
|
|
|
34
34
|
| `centerLabel` | `string` | | Donut only. The caption under the total in the centre of the ring. |
|
|
35
35
|
| `formatValue` | `(value: number) => string` | `Intl.NumberFormat('en-IE')` | Formats numbers in the tooltip and the table. It formats the y-axis too, unless you give formatAxisValue. |
|
|
36
36
|
| `formatAxisValue` | `(value: number) => string` | `formatValue` | Formats only the y-axis tick labels. Use a shorter, compact formatter when formatValue's output is too wide for the axis. SpendChart's compact euro formatter is one example. |
|
|
37
|
-
| `axisWidth` | `number` | `
|
|
37
|
+
| `axisWidth` | `number` | `automatic` | An override for the width the y-axis reserves, in px (pixels). By default the axis sizes itself to its widest formatted tick label, never narrower than 48, so a currency symbol is never cut off. Set it only to line up the axes of several panels. |
|
|
38
38
|
| `bands` | `{ from: string \| number; to: string \| number; label?: string }[]` | `[]` | Line and bar only. Shades stretches of the x-axis, such as night hours on a 24-hour chart. from and to are x-axis values (values of xKey), both included. When from comes after to in the data, the band wraps past the end: 22:00 to 06:00 shades 22:00 to the last point and the first point to 06:00. A band naming a value that isn't in the data is skipped. Every band is listed under the chart with its label, so screen readers hear it too. |
|
|
39
39
|
| `limits` | `{ value: number; label?: string }[]` | `[]` | Line and bar only, not at size sm. Draws a dashed horizontal line at a y value, such as an alert limit of 35 µg/m³. The y-axis grows to include it, so a limit above every data point stays in view. Every limit is listed under the chart with its label and its value formatted by formatValue, so screen readers hear it too. |
|
|
40
40
|
| `headingLevel` | `2 \| 3 \| 4` | `3` | Pick the level that fits the page's heading outline. |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> A progress bar that's either determinate (it knows how far along the task is) or indeterminate (it doesn't). The readout is real text in the DOM (the Document Object Model, the live page structure the browser builds) from the very first render. That's motion token M3's rule, and it means a screen reader, the software that reads the page aloud for blind and low-vision people, never hears a number roll.
|
|
4
4
|
|
|
5
|
-
- **Best for:** Showing how far a measurable task has got
|
|
5
|
+
- **Best for:** Showing how far a measurable task has got, or how much of a limit is used: set allowOverMax so a budget at 112% reads as over, not as 100%.
|
|
6
6
|
- **Not for:** A wait with no measurable work. Use Spinner.
|
|
7
7
|
- Atom. Source: `src/components/atoms/progress/Progress.tsx`
|
|
8
8
|
- Rendered docs page: `/components/progress`
|
|
@@ -20,10 +20,12 @@ Paths are from the Division DS repository root. Installed with shadcn they start
|
|
|
20
20
|
| Prop | Type | Default | Description |
|
|
21
21
|
|---|---|---|---|
|
|
22
22
|
| `label` | `string` | | Required. The bar’s accessible name. |
|
|
23
|
-
| `value` | `number \| null` | | null means indeterminate. The value is clamped to [0, max], so an out-of-range value shows as complete instead of overflowing the track. |
|
|
23
|
+
| `value` | `number \| null` | | null means indeterminate. The value is clamped to [0, max], so an out-of-range value shows as complete instead of overflowing the track. Set allowOverMax to show a value above max as it is. |
|
|
24
24
|
| `max` | `number` | `100` | The value at which the bar is complete. |
|
|
25
25
|
| `showValue` | `boolean` | `false` | Shows the percentage (or "In progress") as a readout. |
|
|
26
26
|
| `tone` | `'signal' \| 'ok' \| 'warning' \| 'hostile'` | `'signal'` | The fill colour. signal paints accent.signal.indicator to match Slider. On the track in light theme that’s a 2.80:1 contrast ratio, below the 3:1 a bar needs when it’s the only sign of progress. So set showValue there. |
|
|
27
|
+
| `allowOverMax` | `boolean` | `false` | Shows a value above max as it is instead of clamping it, for a budget at 112%. The track rescales to span the value, a marker shows where max sits, the part past it paints hostile, and the readout shows the real value (112%) in hostile. aria-valuenow is the real value, and aria-valuetext reads "112%, 12% over". |
|
|
28
|
+
| `shape` | `'bar' \| 'ring'` | `'bar'` | ring draws a circular indicator with the readout in its centre and the label beneath, for a countdown or session timer. It takes the same value and max, It is not a live region, so a ticking timer stays quiet; announce milestones from your own status message. allowOverMax does not apply to it: the value is clamped. |
|
|
27
29
|
| `className` | `string` | | Merged with the outer wrapper’s classes. Later Tailwind utilities (Tailwind is the CSS framework the system is styled with) win. |
|
|
28
30
|
| `ref` | `Ref<HTMLDivElement>` | | Passed through to the Radix Progress root, which is the bar itself. Radix is the library of accessible building blocks this component sits on. |
|
|
29
31
|
| `...rest` | `Omit<HTMLAttributes<HTMLDivElement>, 'children'>` | | Every native div prop except children, spread onto the outer wrapper, so a molecule can attach a data-testid there. children is typed away because this component draws its own label row and track, and a children prop would be thrown away without any warning. |
|
|
@@ -75,6 +77,24 @@ Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
|
75
77
|
</div>
|
|
76
78
|
```
|
|
77
79
|
|
|
80
|
+
### Over the limit
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
<div className="flex flex-col gap-4 w-full max-w-sm">
|
|
84
|
+
<Progress label="Groceries" value={64} tone="ok" allowOverMax showValue />
|
|
85
|
+
<Progress label="Eating out" value={112} tone="ok" allowOverMax showValue />
|
|
86
|
+
</div>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Ring
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<div className="flex gap-6">
|
|
93
|
+
<Progress shape="ring" label="Focus session" value={18} max={25} showValue />
|
|
94
|
+
<Progress shape="ring" label="Break" value={2} max={5} tone="ok" showValue />
|
|
95
|
+
</div>
|
|
96
|
+
```
|
|
97
|
+
|
|
78
98
|
## Keyboard
|
|
79
99
|
|
|
80
100
|
Not focusable. No keyboard behaviour of its own.
|
|
@@ -90,6 +110,7 @@ Not focusable. No keyboard behaviour of its own.
|
|
|
90
110
|
- Don't animate the number up from zero and call that animated value the readout. The real value belongs in the DOM straight away.
|
|
91
111
|
- Don't use Progress for a wait with no measurable work. That's a Spinner.
|
|
92
112
|
- Don't put the padded readout into the accessible name. That's exactly why the pad is a separate aria-hidden span, and label is what Radix announces.
|
|
113
|
+
- Don't leave allowOverMax off on a spend-against-limit bar. Clamped, an overspent category reads as exactly on budget.
|
|
93
114
|
- Don't rely on the tone alone to say something failed. Say it in text.
|
|
94
115
|
|
|
95
116
|
## Anatomy
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> A single choice from a list, shown in a portalled listbox (a pop-up list rendered at the top level of the page, so nothing around it can clip it). Options come in as an array instead of as children, because the trigger, the panel and the look of each row are decisions the system makes once, not per use.
|
|
4
4
|
|
|
5
|
-
- **Best for:** One choice from a list of about five options or more.
|
|
5
|
+
- **Best for:** One choice from a list of about five options or more: a country, a time zone, a currency.
|
|
6
6
|
- **Not for:** Several values: use MultiSelect. Fewer than about five options: use Radio.
|
|
7
7
|
- Atom. Source: `src/components/atoms/select/Select.tsx`
|
|
8
8
|
- Rendered docs page: `/components/select`
|
|
@@ -35,6 +35,7 @@ Paths are from the Division DS repository root. Installed with shadcn they start
|
|
|
35
35
|
| `name` | `string` | | The form field name, for native form submission. |
|
|
36
36
|
| `inputSize` | `'sm' \| 'md' \| 'lg'` | `'md'` | Matches Input’s sizes so the two line up. |
|
|
37
37
|
| `invalid` | `boolean` | `false` | Sets aria-invalid, which tells assistive tech the field has an error, and shows the invalid border. |
|
|
38
|
+
| `iconInTrigger` | `boolean` | `false` | Repeats the chosen row’s icon in the trigger, before its label, still aria-hidden. For icons people recognise the option by, such as a country’s flag; leave it off for decorative ones. |
|
|
38
39
|
| `className` | `string` | | Merged with the trigger’s classes. Later Tailwind utilities (Tailwind is the CSS framework the system uses) win. |
|
|
39
40
|
| `ref` | `Ref<HTMLButtonElement>` | | Passed through to the trigger button. |
|
|
40
41
|
| `id` | `string` | | The trigger’s id. FormField sets it. |
|
|
@@ -51,17 +52,17 @@ export type SelectOption = { value: string; label: string; disabled?: boolean }
|
|
|
51
52
|
```
|
|
52
53
|
|
|
53
54
|
```ts
|
|
54
|
-
/** A Select row: a `SelectOption` plus an optional leading icon. Kept as its own type rather than
|
|
55
|
-
* an `icon` field on `SelectOption`, because MultiSelect takes `SelectOption[]` too and renders
|
|
56
|
-
* no icons: widening the shared type would let an icon compile there and vanish. */
|
|
57
|
-
export type SelectOptionWithIcon = SelectOption & {
|
|
58
|
-
/** Leading glyph, aria-hidden: the label names the row.
|
|
59
|
-
icon?: ReactNode
|
|
55
|
+
/** A Select row: a `SelectOption` plus an optional leading icon. Kept as its own type rather than
|
|
56
|
+
* an `icon` field on `SelectOption`, because MultiSelect takes `SelectOption[]` too and renders
|
|
57
|
+
* no icons: widening the shared type would let an icon compile there and vanish. */
|
|
58
|
+
export type SelectOptionWithIcon = SelectOption & {
|
|
59
|
+
/** Leading glyph, aria-hidden: the label names the row. Shown in the trigger only with `iconInTrigger`. */
|
|
60
|
+
icon?: ReactNode
|
|
60
61
|
}
|
|
61
62
|
```
|
|
62
63
|
|
|
63
64
|
```ts
|
|
64
|
-
/** A titled run of rows. The heading is not selectable; it names the group for screen readers. */
|
|
65
|
+
/** A titled run of rows. The heading is not selectable; it names the group for screen readers. */
|
|
65
66
|
export type SelectGroup = { label: string; options: SelectOptionWithIcon[] }
|
|
66
67
|
```
|
|
67
68
|
|
|
@@ -72,10 +73,10 @@ Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
|
72
73
|
### Placeholder
|
|
73
74
|
|
|
74
75
|
```tsx
|
|
75
|
-
const SPEED_OPTIONS = [
|
|
76
|
-
{ value: 'standard', label: 'Standard' },
|
|
77
|
-
{ value: 'priority', label: 'Priority' },
|
|
78
|
-
{ value: 'overnight', label: 'Overnight', disabled: true },
|
|
76
|
+
const SPEED_OPTIONS = [
|
|
77
|
+
{ value: 'standard', label: 'Standard' },
|
|
78
|
+
{ value: 'priority', label: 'Priority' },
|
|
79
|
+
{ value: 'overnight', label: 'Overnight', disabled: true },
|
|
79
80
|
]
|
|
80
81
|
|
|
81
82
|
<Select label="Dispatch speed" options={SPEED_OPTIONS} placeholder="Choose a speed" />
|
|
@@ -84,10 +85,10 @@ const SPEED_OPTIONS = [
|
|
|
84
85
|
### With a value
|
|
85
86
|
|
|
86
87
|
```tsx
|
|
87
|
-
const SPEED_OPTIONS = [
|
|
88
|
-
{ value: 'standard', label: 'Standard' },
|
|
89
|
-
{ value: 'priority', label: 'Priority' },
|
|
90
|
-
{ value: 'overnight', label: 'Overnight', disabled: true },
|
|
88
|
+
const SPEED_OPTIONS = [
|
|
89
|
+
{ value: 'standard', label: 'Standard' },
|
|
90
|
+
{ value: 'priority', label: 'Priority' },
|
|
91
|
+
{ value: 'overnight', label: 'Overnight', disabled: true },
|
|
91
92
|
]
|
|
92
93
|
|
|
93
94
|
<Select label="Dispatch speed" options={SPEED_OPTIONS} defaultValue="priority" />
|
|
@@ -96,33 +97,76 @@ const SPEED_OPTIONS = [
|
|
|
96
97
|
### Groups with headings and icons
|
|
97
98
|
|
|
98
99
|
```tsx
|
|
99
|
-
const DELIVERY_GROUPS: SelectGroup[] = [
|
|
100
|
-
{
|
|
101
|
-
label: 'Courier',
|
|
102
|
-
options: [
|
|
103
|
-
{ value: 'standard', label: 'Standard', icon: <Truck /> },
|
|
104
|
-
{ value: 'priority', label: 'Priority', icon: <Zap /> },
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
label: 'Collect',
|
|
109
|
-
options: [
|
|
110
|
-
{ value: 'depot', label: 'Brasov depot', icon: <Package /> },
|
|
111
|
-
{ value: 'field', label: 'At the field', icon: <Crosshair /> },
|
|
112
|
-
],
|
|
113
|
-
},
|
|
100
|
+
const DELIVERY_GROUPS: SelectGroup[] = [
|
|
101
|
+
{
|
|
102
|
+
label: 'Courier',
|
|
103
|
+
options: [
|
|
104
|
+
{ value: 'standard', label: 'Standard', icon: <Truck /> },
|
|
105
|
+
{ value: 'priority', label: 'Priority', icon: <Zap /> },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
label: 'Collect',
|
|
110
|
+
options: [
|
|
111
|
+
{ value: 'depot', label: 'Brasov depot', icon: <Package /> },
|
|
112
|
+
{ value: 'field', label: 'At the field', icon: <Crosshair /> },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
114
115
|
]
|
|
115
116
|
|
|
116
117
|
<Select label="Delivery" groups={DELIVERY_GROUPS} placeholder="Choose delivery" />
|
|
117
118
|
```
|
|
118
119
|
|
|
120
|
+
### Countries
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
type Stripes = { dir: 'v' | 'h'; colors: string[] }
|
|
124
|
+
|
|
125
|
+
const FLAGS: Record<string, Stripes> = {
|
|
126
|
+
at: { dir: 'h', colors: ['#c8102e', '#ffffff', '#c8102e'] }, // token-discipline: allow, Austria's flag colours, not interface colour
|
|
127
|
+
be: { dir: 'v', colors: ['#000000', '#fdda24', '#ef3340'] }, // token-discipline: allow, Belgium's flag colours, not interface colour
|
|
128
|
+
de: { dir: 'h', colors: ['#000000', '#dd0000', '#ffce00'] }, // token-discipline: allow, Germany's flag colours, not interface colour
|
|
129
|
+
es: { dir: 'h', colors: ['#aa151b', '#f1bf00', '#f1bf00', '#aa151b'] }, // token-discipline: allow, Spain's flag colours, not interface colour
|
|
130
|
+
fr: { dir: 'v', colors: ['#0055a4', '#ffffff', '#ef4135'] }, // token-discipline: allow, France's flag colours, not interface colour
|
|
131
|
+
ie: { dir: 'v', colors: ['#169b62', '#ffffff', '#ff883e'] }, // token-discipline: allow, Ireland's flag colours, not interface colour
|
|
132
|
+
it: { dir: 'v', colors: ['#009246', '#ffffff', '#ce2b37'] }, // token-discipline: allow, Italy's flag colours, not interface colour
|
|
133
|
+
nl: { dir: 'h', colors: ['#ae1c28', '#ffffff', '#21468b'] }, // token-discipline: allow, the Netherlands' flag colours, not interface colour
|
|
134
|
+
pl: { dir: 'h', colors: ['#ffffff', '#dc143c'] }, // token-discipline: allow, Poland's flag colours, not interface colour
|
|
135
|
+
ro: { dir: 'v', colors: ['#002b7f', '#fcd116', '#ce1126'] }, // token-discipline: allow, Romania's flag colours, not interface colour
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function Flag({ code }: { code: string }) {
|
|
139
|
+
const { dir, colors } = FLAGS[code]
|
|
140
|
+
const n = colors.length
|
|
141
|
+
return (
|
|
142
|
+
<svg viewBox="0 0 20 14" style={{ width: 20, height: 14 }} className="block">
|
|
143
|
+
{colors.map((fill, i) =>
|
|
144
|
+
dir === 'v' ? (
|
|
145
|
+
<rect key={i} x={(20 / n) * i} y={0} width={20 / n} height={14} fill={fill} />
|
|
146
|
+
) : (
|
|
147
|
+
<rect key={i} x={0} y={(14 / n) * i} width={20} height={14 / n} fill={fill} />
|
|
148
|
+
)
|
|
149
|
+
)}
|
|
150
|
+
<rect x={0.5} y={0.5} width={19} height={13} fill="none" stroke="var(--color-border-hairline-strong)" />
|
|
151
|
+
</svg>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const COUNTRY_OPTIONS = [
|
|
156
|
+
['at', 'Austria'], ['be', 'Belgium'], ['fr', 'France'], ['de', 'Germany'], ['ie', 'Ireland'],
|
|
157
|
+
['it', 'Italy'], ['nl', 'Netherlands'], ['pl', 'Poland'], ['ro', 'Romania'], ['es', 'Spain'],
|
|
158
|
+
].map(([value, label]) => ({ value, label, icon: <Flag code={value} /> }))
|
|
159
|
+
|
|
160
|
+
<Select label="Country" options={COUNTRY_OPTIONS} iconInTrigger defaultValue="ro" />
|
|
161
|
+
```
|
|
162
|
+
|
|
119
163
|
### Long list
|
|
120
164
|
|
|
121
165
|
```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 }
|
|
166
|
+
const MONTH_OPTIONS = Array.from({ length: 30 }, (_, i) => {
|
|
167
|
+
const date = new Date(Date.UTC(2026, 10 - i, 1))
|
|
168
|
+
const label = date.toLocaleDateString('en-GB', { month: 'long', year: 'numeric', timeZone: 'UTC' })
|
|
169
|
+
return { value: date.toISOString().slice(0, 7), label }
|
|
126
170
|
})
|
|
127
171
|
|
|
128
172
|
<Select label="Billing month" options={MONTH_OPTIONS} placeholder="Choose a month" />
|
|
@@ -131,25 +175,25 @@ const MONTH_OPTIONS = Array.from({ length: 30 }, (_, i) => {
|
|
|
131
175
|
### Sizes
|
|
132
176
|
|
|
133
177
|
```tsx
|
|
134
|
-
const TERRAIN_OPTIONS = [
|
|
135
|
-
{ value: 'woodland', label: 'Woodland' },
|
|
136
|
-
{ value: 'urban', label: 'Urban' },
|
|
137
|
-
{ value: 'desert', label: 'Desert' },
|
|
138
|
-
{ value: 'indoor', label: 'Indoor' },
|
|
178
|
+
const TERRAIN_OPTIONS = [
|
|
179
|
+
{ value: 'woodland', label: 'Woodland' },
|
|
180
|
+
{ value: 'urban', label: 'Urban' },
|
|
181
|
+
{ value: 'desert', label: 'Desert' },
|
|
182
|
+
{ value: 'indoor', label: 'Indoor' },
|
|
139
183
|
]
|
|
140
184
|
|
|
141
|
-
<Select label="Terrain (small)" inputSize="sm" options={TERRAIN_OPTIONS} placeholder="Terrain" />
|
|
142
|
-
<Select label="Terrain (medium)" inputSize="md" options={TERRAIN_OPTIONS} placeholder="Terrain" />
|
|
185
|
+
<Select label="Terrain (small)" inputSize="sm" options={TERRAIN_OPTIONS} placeholder="Terrain" />
|
|
186
|
+
<Select label="Terrain (medium)" inputSize="md" options={TERRAIN_OPTIONS} placeholder="Terrain" />
|
|
143
187
|
<Select label="Terrain (large)" inputSize="lg" options={TERRAIN_OPTIONS} placeholder="Terrain" />
|
|
144
188
|
```
|
|
145
189
|
|
|
146
190
|
### Disabled control
|
|
147
191
|
|
|
148
192
|
```tsx
|
|
149
|
-
const SPEED_OPTIONS = [
|
|
150
|
-
{ value: 'standard', label: 'Standard' },
|
|
151
|
-
{ value: 'priority', label: 'Priority' },
|
|
152
|
-
{ value: 'overnight', label: 'Overnight', disabled: true },
|
|
193
|
+
const SPEED_OPTIONS = [
|
|
194
|
+
{ value: 'standard', label: 'Standard' },
|
|
195
|
+
{ value: 'priority', label: 'Priority' },
|
|
196
|
+
{ value: 'overnight', label: 'Overnight', disabled: true },
|
|
153
197
|
]
|
|
154
198
|
|
|
155
199
|
<Select label="Dispatch speed, unavailable" options={SPEED_OPTIONS} defaultValue="standard" disabled />
|
|
@@ -158,10 +202,10 @@ const SPEED_OPTIONS = [
|
|
|
158
202
|
### Invalid
|
|
159
203
|
|
|
160
204
|
```tsx
|
|
161
|
-
const SPEED_OPTIONS = [
|
|
162
|
-
{ value: 'standard', label: 'Standard' },
|
|
163
|
-
{ value: 'priority', label: 'Priority' },
|
|
164
|
-
{ value: 'overnight', label: 'Overnight', disabled: true },
|
|
205
|
+
const SPEED_OPTIONS = [
|
|
206
|
+
{ value: 'standard', label: 'Standard' },
|
|
207
|
+
{ value: 'priority', label: 'Priority' },
|
|
208
|
+
{ value: 'overnight', label: 'Overnight', disabled: true },
|
|
165
209
|
]
|
|
166
210
|
|
|
167
211
|
<Select label="Dispatch speed" options={SPEED_OPTIONS} invalid placeholder="Choose a speed" />
|
|
@@ -170,26 +214,26 @@ const SPEED_OPTIONS = [
|
|
|
170
214
|
### Beside an Input
|
|
171
215
|
|
|
172
216
|
```tsx
|
|
173
|
-
const TERRAIN_OPTIONS = [
|
|
174
|
-
{ value: 'woodland', label: 'Woodland' },
|
|
175
|
-
{ value: 'urban', label: 'Urban' },
|
|
176
|
-
{ value: 'desert', label: 'Desert' },
|
|
177
|
-
{ value: 'indoor', label: 'Indoor' },
|
|
217
|
+
const TERRAIN_OPTIONS = [
|
|
218
|
+
{ value: 'woodland', label: 'Woodland' },
|
|
219
|
+
{ value: 'urban', label: 'Urban' },
|
|
220
|
+
{ value: 'desert', label: 'Desert' },
|
|
221
|
+
{ value: 'indoor', label: 'Indoor' },
|
|
178
222
|
]
|
|
179
223
|
|
|
180
|
-
<label className="flex flex-col gap-1 text-sm">
|
|
181
|
-
Call sign
|
|
182
|
-
<Input placeholder="Anvil-3" />
|
|
183
|
-
</label>
|
|
184
|
-
{/* Deliberately NOT a <label>. A <label> associates with a labelable element — input,
|
|
185
|
-
select, textarea, button among them — but Radix's trigger is a <button>, and a
|
|
186
|
-
<label> wrapping a button is inert: it neither names it nor forwards a click. The
|
|
187
|
-
Select's accessible name comes from its required `label` prop, and the visible text
|
|
188
|
-
here matches it exactly (SC 2.5.3, label in name). Wrapping it in a <label> would
|
|
189
|
-
have looked correct and done nothing. */}
|
|
190
|
-
<div className="flex flex-col gap-1 text-sm">
|
|
191
|
-
<span>Terrain</span>
|
|
192
|
-
<Select label="Terrain" options={TERRAIN_OPTIONS} placeholder="Choose terrain" />
|
|
224
|
+
<label className="flex flex-col gap-1 text-sm">
|
|
225
|
+
Call sign
|
|
226
|
+
<Input placeholder="Anvil-3" />
|
|
227
|
+
</label>
|
|
228
|
+
{/* Deliberately NOT a <label>. A <label> associates with a labelable element — input,
|
|
229
|
+
select, textarea, button among them — but Radix's trigger is a <button>, and a
|
|
230
|
+
<label> wrapping a button is inert: it neither names it nor forwards a click. The
|
|
231
|
+
Select's accessible name comes from its required `label` prop, and the visible text
|
|
232
|
+
here matches it exactly (SC 2.5.3, label in name). Wrapping it in a <label> would
|
|
233
|
+
have looked correct and done nothing. */}
|
|
234
|
+
<div className="flex flex-col gap-1 text-sm">
|
|
235
|
+
<span>Terrain</span>
|
|
236
|
+
<Select label="Terrain" options={TERRAIN_OPTIONS} placeholder="Choose terrain" />
|
|
193
237
|
</div>
|
|
194
238
|
```
|
|
195
239
|
|
|
@@ -213,6 +257,7 @@ const TERRAIN_OPTIONS = [
|
|
|
213
257
|
- Below about five options, use radios instead.
|
|
214
258
|
- Group rows under headings once the list mixes different kinds of thing, and keep each group to a handful.
|
|
215
259
|
- Give every row an icon, or none of them.
|
|
260
|
+
- Set iconInTrigger when the icon is how people recognise the option, such as a flag in a country list.
|
|
216
261
|
- Wrap it in a FormField for a visible label, help and error. FormField hands it the label.
|
|
217
262
|
|
|
218
263
|
## Don't
|
|
@@ -220,6 +265,7 @@ const TERRAIN_OPTIONS = [
|
|
|
220
265
|
- Don't build a searchable select out of this. That's the command palette's job.
|
|
221
266
|
- Don't use it for several values. That's MultiSelect, which shows each choice as a removable tag.
|
|
222
267
|
- Don't put more than a couple of dozen options in it without search.
|
|
268
|
+
- Don't use emoji flags. Windows draws them as letter pairs such as RO; pass a small image or SVG as the icon instead.
|
|
223
269
|
- Don't make a group of one. A heading over a single row is just noise.
|
|
224
270
|
- Don't style the trigger differently from Input. A form with mismatched control heights looks broken.
|
|
225
271
|
- Don't bevel the trigger. It shares Input's frame and draws its own focus ring (the outline that shows which control the keyboard is on), and a cut corner would clip it. The listbox carries the crosshair instead: the shape lives on the surface, not on the control.
|
|
@@ -232,5 +278,5 @@ const TERRAIN_OPTIONS = [
|
|
|
232
278
|
- **Content**: Portalled and positioned by Radix, the library of accessible building blocks this sits on. It slides down out of the trigger as it opens (M7; Radix’s Select closes at once, with no exit animation). It’s the one atom that carries the corner crosshair, because it’s a surface framing other content, and that’s what the crosshair marks. It’s also the shape that can’t be cut, since clip-path would clip the crosshair’s arms and the last row’s selection fill along with them.
|
|
233
279
|
- **Item**: Highlighted rows use accent.signal.indicator with accent.signal.indicator-ink. That’s the fill-selection grammar, the system’s rule for how a selection looks.
|
|
234
280
|
- **ItemIndicator**: A tick on the selected row, so selection isn’t shown by the highlight alone.
|
|
235
|
-
- **Item icon**: Optional. Sits before the label and is aria-hidden, an ARIA (Accessible Rich Internet Applications) attribute that hides it from assistive tech. It follows the row colour, so it turns ink on the highlight. It isn’t shown in the trigger.
|
|
281
|
+
- **Item icon**: Optional. Sits before the label and is aria-hidden, an ARIA (Accessible Rich Internet Applications) attribute that hides it from assistive tech. It follows the row colour, so it turns ink on the highlight. It isn’t shown in the trigger unless iconInTrigger is set.
|
|
236
282
|
- **Group label**: Used with groups: a mono (monospaced font) heading over its rows that names the group for screen readers. It can’t be selected. A hairline separates the groups.
|
|
@@ -290,7 +290,12 @@
|
|
|
290
290
|
{
|
|
291
291
|
"name": "color",
|
|
292
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."
|
|
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, or passing it with severity. 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."
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"name": "severity",
|
|
297
|
+
"type": "1 | 2 | 3 | 4 | 5 | 6",
|
|
298
|
+
"description": "A step on the ordered severity scale, from 1 (lowest) to 6, for a measure with more levels than the four status tones, like air quality from Good to Hazardous. The fill goes from green to maroon. It replaces tone and color: the types reject passing two. The steps are exported as SEVERITY_STEPS. The badge still needs the level's word: the step's colour is never the message on its own. Works with both emphases and both sizes."
|
|
294
299
|
},
|
|
295
300
|
{
|
|
296
301
|
"name": "emphasis",
|
|
@@ -358,6 +363,8 @@
|
|
|
358
363
|
"Keep the tone mapping the same across the product. ok is never ‘in progress’ on one screen and ‘done’ on another.",
|
|
359
364
|
"Use size md when the badge sits next to a headline figure, so the two read as one statement.",
|
|
360
365
|
"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.",
|
|
366
|
+
"Use severity when a measure has more than four ordered levels, like air quality, so each level gets its own step and no two share a colour. Use the same step for the same level on every screen.",
|
|
367
|
+
"Prefer solid for a severity scale: the fills darken step by step in both themes, so the order reads at a glance. In the dark theme the subtle text steps are ordered by hue, not lightness.",
|
|
361
368
|
"Store the colour with the label, so it stays the same on every screen. Pick one from CATEGORY_COLORS when the label is created."
|
|
362
369
|
],
|
|
363
370
|
"donts": [
|
|
@@ -365,6 +372,8 @@
|
|
|
365
372
|
"Don't put a badge inside a button. It muddies what the clickable target is.",
|
|
366
373
|
"Don't use hostile for something that's only pending.",
|
|
367
374
|
"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.",
|
|
375
|
+
"Don't squeeze a six-level scale into the four status tones. Two levels end up sharing a colour, and only the word tells them apart. That's what severity is for.",
|
|
376
|
+
"Don't use severity for a status that isn't on a scale. A failed payment is tone=\"hostile\", not severity={5}.",
|
|
368
377
|
"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.",
|
|
369
378
|
"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.",
|
|
370
379
|
"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.",
|
|
@@ -406,6 +415,14 @@
|
|
|
406
415
|
"caption": "Category colours, solid",
|
|
407
416
|
"source": "{CATEGORY_COLORS.map(color => (\n <Badge key={color} color={color} emphasis=\"solid\">\n {color}\n </Badge>\n))}"
|
|
408
417
|
},
|
|
418
|
+
{
|
|
419
|
+
"caption": "Severity, solid: the six air quality levels, each with its word",
|
|
420
|
+
"source": "const AQI_LEVELS = ['Good', 'Moderate', 'Unhealthy for sensitive groups', 'Unhealthy', 'Very unhealthy', 'Hazardous'] as const\n\n{SEVERITY_STEPS.map(step => (\n <Badge key={step} severity={step} emphasis=\"solid\">\n {AQI_LEVELS[step - 1]}\n </Badge>\n))}"
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
"caption": "Severity, subtle",
|
|
424
|
+
"source": "const AQI_LEVELS = ['Good', 'Moderate', 'Unhealthy for sensitive groups', 'Unhealthy', 'Very unhealthy', 'Hazardous'] as const\n\n{SEVERITY_STEPS.map(step => (\n <Badge key={step} severity={step}>\n {AQI_LEVELS[step - 1]}\n </Badge>\n))}"
|
|
425
|
+
},
|
|
409
426
|
{
|
|
410
427
|
"caption": "With a dot",
|
|
411
428
|
"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>"
|
|
@@ -1719,7 +1736,7 @@
|
|
|
1719
1736
|
"tier": "atoms",
|
|
1720
1737
|
"slug": "progress",
|
|
1721
1738
|
"summary": "A progress bar that's either determinate (it knows how far along the task is) or indeterminate (it doesn't). The readout is real text in the DOM (the Document Object Model, the live page structure the browser builds) from the very first render. That's motion token M3's rule, and it means a screen reader, the software that reads the page aloud for blind and low-vision people, never hears a number roll.",
|
|
1722
|
-
"useWhen": "Showing how far a measurable task has got
|
|
1739
|
+
"useWhen": "Showing how far a measurable task has got, or how much of a limit is used: set allowOverMax so a budget at 112% reads as over, not as 100%.",
|
|
1723
1740
|
"notFor": "A wait with no measurable work. Use Spinner.",
|
|
1724
1741
|
"source": "src/components/atoms/progress/Progress.tsx",
|
|
1725
1742
|
"docs": "/docs/components/progress.md",
|
|
@@ -1740,7 +1757,7 @@
|
|
|
1740
1757
|
{
|
|
1741
1758
|
"name": "value",
|
|
1742
1759
|
"type": "number | null",
|
|
1743
|
-
"description": "null means indeterminate. The value is clamped to [0, max], so an out-of-range value shows as complete instead of overflowing the track."
|
|
1760
|
+
"description": "null means indeterminate. The value is clamped to [0, max], so an out-of-range value shows as complete instead of overflowing the track. Set allowOverMax to show a value above max as it is."
|
|
1744
1761
|
},
|
|
1745
1762
|
{
|
|
1746
1763
|
"name": "max",
|
|
@@ -1760,6 +1777,18 @@
|
|
|
1760
1777
|
"default": "'signal'",
|
|
1761
1778
|
"description": "The fill colour. signal paints accent.signal.indicator to match Slider. On the track in light theme that’s a 2.80:1 contrast ratio, below the 3:1 a bar needs when it’s the only sign of progress. So set showValue there."
|
|
1762
1779
|
},
|
|
1780
|
+
{
|
|
1781
|
+
"name": "allowOverMax",
|
|
1782
|
+
"type": "boolean",
|
|
1783
|
+
"default": "false",
|
|
1784
|
+
"description": "Shows a value above max as it is instead of clamping it, for a budget at 112%. The track rescales to span the value, a marker shows where max sits, the part past it paints hostile, and the readout shows the real value (112%) in hostile. aria-valuenow is the real value, and aria-valuetext reads \"112%, 12% over\"."
|
|
1785
|
+
},
|
|
1786
|
+
{
|
|
1787
|
+
"name": "shape",
|
|
1788
|
+
"type": "'bar' | 'ring'",
|
|
1789
|
+
"default": "'bar'",
|
|
1790
|
+
"description": "ring draws a circular indicator with the readout in its centre and the label beneath, for a countdown or session timer. It takes the same value and max, It is not a live region, so a ticking timer stays quiet; announce milestones from your own status message. allowOverMax does not apply to it: the value is clamped."
|
|
1791
|
+
},
|
|
1763
1792
|
{
|
|
1764
1793
|
"name": "className",
|
|
1765
1794
|
"type": "string",
|
|
@@ -1786,6 +1815,7 @@
|
|
|
1786
1815
|
"Don't animate the number up from zero and call that animated value the readout. The real value belongs in the DOM straight away.",
|
|
1787
1816
|
"Don't use Progress for a wait with no measurable work. That's a Spinner.",
|
|
1788
1817
|
"Don't put the padded readout into the accessible name. That's exactly why the pad is a separate aria-hidden span, and label is what Radix announces.",
|
|
1818
|
+
"Don't leave allowOverMax off on a spend-against-limit bar. Clamped, an overspent category reads as exactly on budget.",
|
|
1789
1819
|
"Don't rely on the tone alone to say something failed. Say it in text."
|
|
1790
1820
|
],
|
|
1791
1821
|
"anatomy": [
|
|
@@ -1830,6 +1860,14 @@
|
|
|
1830
1860
|
{
|
|
1831
1861
|
"caption": "Tones",
|
|
1832
1862
|
"source": "<div className=\"flex flex-col gap-4 w-full max-w-sm\">\n <Progress label=\"Signal supply run\" value={40} tone=\"signal\" showValue />\n <Progress label=\"BB reserves\" value={82} tone=\"ok\" showValue />\n <Progress label=\"Gas reserves\" value={22} tone=\"warning\" showValue />\n <Progress label=\"Battery reserves\" value={6} tone=\"hostile\" showValue />\n</div>"
|
|
1863
|
+
},
|
|
1864
|
+
{
|
|
1865
|
+
"caption": "Over the limit",
|
|
1866
|
+
"source": "<div className=\"flex flex-col gap-4 w-full max-w-sm\">\n <Progress label=\"Groceries\" value={64} tone=\"ok\" allowOverMax showValue />\n <Progress label=\"Eating out\" value={112} tone=\"ok\" allowOverMax showValue />\n</div>"
|
|
1867
|
+
},
|
|
1868
|
+
{
|
|
1869
|
+
"caption": "Ring",
|
|
1870
|
+
"source": "<div className=\"flex gap-6\">\n <Progress shape=\"ring\" label=\"Focus session\" value={18} max={25} showValue />\n <Progress shape=\"ring\" label=\"Break\" value={2} max={5} tone=\"ok\" showValue />\n</div>"
|
|
1833
1871
|
}
|
|
1834
1872
|
],
|
|
1835
1873
|
"closed": false
|
|
@@ -2135,7 +2173,7 @@
|
|
|
2135
2173
|
"tier": "atoms",
|
|
2136
2174
|
"slug": "select",
|
|
2137
2175
|
"summary": "A single choice from a list, shown in a portalled listbox (a pop-up list rendered at the top level of the page, so nothing around it can clip it). Options come in as an array instead of as children, because the trigger, the panel and the look of each row are decisions the system makes once, not per use.",
|
|
2138
|
-
"useWhen": "One choice from a list of about five options or more.",
|
|
2176
|
+
"useWhen": "One choice from a list of about five options or more: a country, a time zone, a currency.",
|
|
2139
2177
|
"notFor": "Several values: use MultiSelect. Fewer than about five options: use Radio.",
|
|
2140
2178
|
"source": "src/components/atoms/select/Select.tsx",
|
|
2141
2179
|
"docs": "/docs/components/select.md",
|
|
@@ -2214,6 +2252,12 @@
|
|
|
2214
2252
|
"default": "false",
|
|
2215
2253
|
"description": "Sets aria-invalid, which tells assistive tech the field has an error, and shows the invalid border."
|
|
2216
2254
|
},
|
|
2255
|
+
{
|
|
2256
|
+
"name": "iconInTrigger",
|
|
2257
|
+
"type": "boolean",
|
|
2258
|
+
"default": "false",
|
|
2259
|
+
"description": "Repeats the chosen row’s icon in the trigger, before its label, still aria-hidden. For icons people recognise the option by, such as a country’s flag; leave it off for decorative ones."
|
|
2260
|
+
},
|
|
2217
2261
|
{
|
|
2218
2262
|
"name": "className",
|
|
2219
2263
|
"type": "string",
|
|
@@ -2255,13 +2299,13 @@
|
|
|
2255
2299
|
{
|
|
2256
2300
|
"name": "SelectOptionWithIcon",
|
|
2257
2301
|
"kind": "type",
|
|
2258
|
-
"code": "/** A Select row: a `SelectOption` plus an optional leading icon. Kept as its own type rather than\n * an `icon` field on `SelectOption`, because MultiSelect takes `SelectOption[]` too and renders\n * no icons: widening the shared type would let an icon compile there and vanish. */\nexport type SelectOptionWithIcon = SelectOption & {\n /** Leading glyph, aria-hidden: the label names the row.
|
|
2302
|
+
"code": "/** A Select row: a `SelectOption` plus an optional leading icon. Kept as its own type rather than\r\n * an `icon` field on `SelectOption`, because MultiSelect takes `SelectOption[]` too and renders\r\n * no icons: widening the shared type would let an icon compile there and vanish. */\r\nexport type SelectOptionWithIcon = SelectOption & {\r\n /** Leading glyph, aria-hidden: the label names the row. Shown in the trigger only with `iconInTrigger`. */\r\n icon?: ReactNode\r\n}",
|
|
2259
2303
|
"from": "src/components/atoms/select/Select"
|
|
2260
2304
|
},
|
|
2261
2305
|
{
|
|
2262
2306
|
"name": "SelectGroup",
|
|
2263
2307
|
"kind": "type",
|
|
2264
|
-
"code": "/** A titled run of rows. The heading is not selectable; it names the group for screen readers. */\nexport type SelectGroup = { label: string; options: SelectOptionWithIcon[] }",
|
|
2308
|
+
"code": "/** A titled run of rows. The heading is not selectable; it names the group for screen readers. */\r\nexport type SelectGroup = { label: string; options: SelectOptionWithIcon[] }",
|
|
2265
2309
|
"from": "src/components/atoms/select/Select"
|
|
2266
2310
|
}
|
|
2267
2311
|
],
|
|
@@ -2319,12 +2363,14 @@
|
|
|
2319
2363
|
"Below about five options, use radios instead.",
|
|
2320
2364
|
"Group rows under headings once the list mixes different kinds of thing, and keep each group to a handful.",
|
|
2321
2365
|
"Give every row an icon, or none of them.",
|
|
2366
|
+
"Set iconInTrigger when the icon is how people recognise the option, such as a flag in a country list.",
|
|
2322
2367
|
"Wrap it in a FormField for a visible label, help and error. FormField hands it the label."
|
|
2323
2368
|
],
|
|
2324
2369
|
"donts": [
|
|
2325
2370
|
"Don't build a searchable select out of this. That's the command palette's job.",
|
|
2326
2371
|
"Don't use it for several values. That's MultiSelect, which shows each choice as a removable tag.",
|
|
2327
2372
|
"Don't put more than a couple of dozen options in it without search.",
|
|
2373
|
+
"Don't use emoji flags. Windows draws them as letter pairs such as RO; pass a small image or SVG as the icon instead.",
|
|
2328
2374
|
"Don't make a group of one. A heading over a single row is just noise.",
|
|
2329
2375
|
"Don't style the trigger differently from Input. A form with mismatched control heights looks broken.",
|
|
2330
2376
|
"Don't bevel the trigger. It shares Input's frame and draws its own focus ring (the outline that shows which control the keyboard is on), and a cut corner would clip it. The listbox carries the crosshair instead: the shape lives on the surface, not on the control.",
|
|
@@ -2353,7 +2399,7 @@
|
|
|
2353
2399
|
},
|
|
2354
2400
|
{
|
|
2355
2401
|
"part": "Item icon",
|
|
2356
|
-
"description": "Optional. Sits before the label and is aria-hidden, an ARIA (Accessible Rich Internet Applications) attribute that hides it from assistive tech. It follows the row colour, so it turns ink on the highlight. It isn’t shown in the trigger."
|
|
2402
|
+
"description": "Optional. Sits before the label and is aria-hidden, an ARIA (Accessible Rich Internet Applications) attribute that hides it from assistive tech. It follows the row colour, so it turns ink on the highlight. It isn’t shown in the trigger unless iconInTrigger is set."
|
|
2357
2403
|
},
|
|
2358
2404
|
{
|
|
2359
2405
|
"part": "Group label",
|
|
@@ -2363,35 +2409,39 @@
|
|
|
2363
2409
|
"examples": [
|
|
2364
2410
|
{
|
|
2365
2411
|
"caption": "Placeholder",
|
|
2366
|
-
"source": "const SPEED_OPTIONS = [\n { value: 'standard', label: 'Standard' },\n { value: 'priority', label: 'Priority' },\n { value: 'overnight', label: 'Overnight', disabled: true },\n]\n\n<Select label=\"Dispatch speed\" options={SPEED_OPTIONS} placeholder=\"Choose a speed\" />"
|
|
2412
|
+
"source": "const SPEED_OPTIONS = [\r\n { value: 'standard', label: 'Standard' },\r\n { value: 'priority', label: 'Priority' },\r\n { value: 'overnight', label: 'Overnight', disabled: true },\r\n]\n\n<Select label=\"Dispatch speed\" options={SPEED_OPTIONS} placeholder=\"Choose a speed\" />"
|
|
2367
2413
|
},
|
|
2368
2414
|
{
|
|
2369
2415
|
"caption": "With a value",
|
|
2370
|
-
"source": "const SPEED_OPTIONS = [\n { value: 'standard', label: 'Standard' },\n { value: 'priority', label: 'Priority' },\n { value: 'overnight', label: 'Overnight', disabled: true },\n]\n\n<Select label=\"Dispatch speed\" options={SPEED_OPTIONS} defaultValue=\"priority\" />"
|
|
2416
|
+
"source": "const SPEED_OPTIONS = [\r\n { value: 'standard', label: 'Standard' },\r\n { value: 'priority', label: 'Priority' },\r\n { value: 'overnight', label: 'Overnight', disabled: true },\r\n]\n\n<Select label=\"Dispatch speed\" options={SPEED_OPTIONS} defaultValue=\"priority\" />"
|
|
2371
2417
|
},
|
|
2372
2418
|
{
|
|
2373
2419
|
"caption": "Groups with headings and icons",
|
|
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\" />"
|
|
2420
|
+
"source": "const DELIVERY_GROUPS: SelectGroup[] = [\r\n {\r\n label: 'Courier',\r\n options: [\r\n { value: 'standard', label: 'Standard', icon: <Truck /> },\r\n { value: 'priority', label: 'Priority', icon: <Zap /> },\r\n ],\r\n },\r\n {\r\n label: 'Collect',\r\n options: [\r\n { value: 'depot', label: 'Brasov depot', icon: <Package /> },\r\n { value: 'field', label: 'At the field', icon: <Crosshair /> },\r\n ],\r\n },\r\n]\n\n<Select label=\"Delivery\" groups={DELIVERY_GROUPS} placeholder=\"Choose delivery\" />"
|
|
2421
|
+
},
|
|
2422
|
+
{
|
|
2423
|
+
"caption": "Countries",
|
|
2424
|
+
"source": "type Stripes = { dir: 'v' | 'h'; colors: string[] }\n\nconst FLAGS: Record<string, Stripes> = {\r\n at: { dir: 'h', colors: ['#c8102e', '#ffffff', '#c8102e'] }, // token-discipline: allow, Austria's flag colours, not interface colour\r\n be: { dir: 'v', colors: ['#000000', '#fdda24', '#ef3340'] }, // token-discipline: allow, Belgium's flag colours, not interface colour\r\n de: { dir: 'h', colors: ['#000000', '#dd0000', '#ffce00'] }, // token-discipline: allow, Germany's flag colours, not interface colour\r\n es: { dir: 'h', colors: ['#aa151b', '#f1bf00', '#f1bf00', '#aa151b'] }, // token-discipline: allow, Spain's flag colours, not interface colour\r\n fr: { dir: 'v', colors: ['#0055a4', '#ffffff', '#ef4135'] }, // token-discipline: allow, France's flag colours, not interface colour\r\n ie: { dir: 'v', colors: ['#169b62', '#ffffff', '#ff883e'] }, // token-discipline: allow, Ireland's flag colours, not interface colour\r\n it: { dir: 'v', colors: ['#009246', '#ffffff', '#ce2b37'] }, // token-discipline: allow, Italy's flag colours, not interface colour\r\n nl: { dir: 'h', colors: ['#ae1c28', '#ffffff', '#21468b'] }, // token-discipline: allow, the Netherlands' flag colours, not interface colour\r\n pl: { dir: 'h', colors: ['#ffffff', '#dc143c'] }, // token-discipline: allow, Poland's flag colours, not interface colour\r\n ro: { dir: 'v', colors: ['#002b7f', '#fcd116', '#ce1126'] }, // token-discipline: allow, Romania's flag colours, not interface colour\r\n}\n\nfunction Flag({ code }: { code: string }) {\r\n const { dir, colors } = FLAGS[code]\r\n const n = colors.length\r\n return (\r\n <svg viewBox=\"0 0 20 14\" style={{ width: 20, height: 14 }} className=\"block\">\r\n {colors.map((fill, i) =>\r\n dir === 'v' ? (\r\n <rect key={i} x={(20 / n) * i} y={0} width={20 / n} height={14} fill={fill} />\r\n ) : (\r\n <rect key={i} x={0} y={(14 / n) * i} width={20} height={14 / n} fill={fill} />\r\n )\r\n )}\r\n <rect x={0.5} y={0.5} width={19} height={13} fill=\"none\" stroke=\"var(--color-border-hairline-strong)\" />\r\n </svg>\r\n )\r\n}\n\nconst COUNTRY_OPTIONS = [\r\n ['at', 'Austria'], ['be', 'Belgium'], ['fr', 'France'], ['de', 'Germany'], ['ie', 'Ireland'],\r\n ['it', 'Italy'], ['nl', 'Netherlands'], ['pl', 'Poland'], ['ro', 'Romania'], ['es', 'Spain'],\r\n].map(([value, label]) => ({ value, label, icon: <Flag code={value} /> }))\n\n<Select label=\"Country\" options={COUNTRY_OPTIONS} iconInTrigger defaultValue=\"ro\" />"
|
|
2375
2425
|
},
|
|
2376
2426
|
{
|
|
2377
2427
|
"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\" />"
|
|
2428
|
+
"source": "const MONTH_OPTIONS = Array.from({ length: 30 }, (_, i) => {\r\n const date = new Date(Date.UTC(2026, 10 - i, 1))\r\n const label = date.toLocaleDateString('en-GB', { month: 'long', year: 'numeric', timeZone: 'UTC' })\r\n return { value: date.toISOString().slice(0, 7), label }\r\n})\n\n<Select label=\"Billing month\" options={MONTH_OPTIONS} placeholder=\"Choose a month\" />"
|
|
2379
2429
|
},
|
|
2380
2430
|
{
|
|
2381
2431
|
"caption": "Sizes",
|
|
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\" />"
|
|
2432
|
+
"source": "const TERRAIN_OPTIONS = [\r\n { value: 'woodland', label: 'Woodland' },\r\n { value: 'urban', label: 'Urban' },\r\n { value: 'desert', label: 'Desert' },\r\n { value: 'indoor', label: 'Indoor' },\r\n]\n\n<Select label=\"Terrain (small)\" inputSize=\"sm\" options={TERRAIN_OPTIONS} placeholder=\"Terrain\" />\r\n<Select label=\"Terrain (medium)\" inputSize=\"md\" options={TERRAIN_OPTIONS} placeholder=\"Terrain\" />\r\n<Select label=\"Terrain (large)\" inputSize=\"lg\" options={TERRAIN_OPTIONS} placeholder=\"Terrain\" />"
|
|
2383
2433
|
},
|
|
2384
2434
|
{
|
|
2385
2435
|
"caption": "Disabled control",
|
|
2386
|
-
"source": "const SPEED_OPTIONS = [\n { value: 'standard', label: 'Standard' },\n { value: 'priority', label: 'Priority' },\n { value: 'overnight', label: 'Overnight', disabled: true },\n]\n\n<Select label=\"Dispatch speed, unavailable\" options={SPEED_OPTIONS} defaultValue=\"standard\" disabled />"
|
|
2436
|
+
"source": "const SPEED_OPTIONS = [\r\n { value: 'standard', label: 'Standard' },\r\n { value: 'priority', label: 'Priority' },\r\n { value: 'overnight', label: 'Overnight', disabled: true },\r\n]\n\n<Select label=\"Dispatch speed, unavailable\" options={SPEED_OPTIONS} defaultValue=\"standard\" disabled />"
|
|
2387
2437
|
},
|
|
2388
2438
|
{
|
|
2389
2439
|
"caption": "Invalid",
|
|
2390
|
-
"source": "const SPEED_OPTIONS = [\n { value: 'standard', label: 'Standard' },\n { value: 'priority', label: 'Priority' },\n { value: 'overnight', label: 'Overnight', disabled: true },\n]\n\n<Select label=\"Dispatch speed\" options={SPEED_OPTIONS} invalid placeholder=\"Choose a speed\" />"
|
|
2440
|
+
"source": "const SPEED_OPTIONS = [\r\n { value: 'standard', label: 'Standard' },\r\n { value: 'priority', label: 'Priority' },\r\n { value: 'overnight', label: 'Overnight', disabled: true },\r\n]\n\n<Select label=\"Dispatch speed\" options={SPEED_OPTIONS} invalid placeholder=\"Choose a speed\" />"
|
|
2391
2441
|
},
|
|
2392
2442
|
{
|
|
2393
2443
|
"caption": "Beside an Input",
|
|
2394
|
-
"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<label className=\"flex flex-col gap-1 text-sm\">\n Call sign\n <Input placeholder=\"Anvil-3\" />\n</label>\n{/* Deliberately NOT a <label>. A <label> associates with a labelable element — input,\n select, textarea, button among them — but Radix's trigger is a <button>, and a\n <label> wrapping a button is inert: it neither names it nor forwards a click. The\n Select's accessible name comes from its required `label` prop, and the visible text\n here matches it exactly (SC 2.5.3, label in name). Wrapping it in a <label> would\n have looked correct and done nothing. */}\n<div className=\"flex flex-col gap-1 text-sm\">\n <span>Terrain</span>\n <Select label=\"Terrain\" options={TERRAIN_OPTIONS} placeholder=\"Choose terrain\" />\n</div>"
|
|
2444
|
+
"source": "const TERRAIN_OPTIONS = [\r\n { value: 'woodland', label: 'Woodland' },\r\n { value: 'urban', label: 'Urban' },\r\n { value: 'desert', label: 'Desert' },\r\n { value: 'indoor', label: 'Indoor' },\r\n]\n\n<label className=\"flex flex-col gap-1 text-sm\">\r\n Call sign\r\n <Input placeholder=\"Anvil-3\" />\r\n</label>\r\n{/* Deliberately NOT a <label>. A <label> associates with a labelable element — input,\r\n select, textarea, button among them — but Radix's trigger is a <button>, and a\r\n <label> wrapping a button is inert: it neither names it nor forwards a click. The\r\n Select's accessible name comes from its required `label` prop, and the visible text\r\n here matches it exactly (SC 2.5.3, label in name). Wrapping it in a <label> would\r\n have looked correct and done nothing. */}\r\n<div className=\"flex flex-col gap-1 text-sm\">\r\n <span>Terrain</span>\r\n <Select label=\"Terrain\" options={TERRAIN_OPTIONS} placeholder=\"Choose terrain\" />\r\n</div>"
|
|
2395
2445
|
}
|
|
2396
2446
|
],
|
|
2397
2447
|
"closed": false,
|
|
@@ -9840,8 +9890,8 @@
|
|
|
9840
9890
|
{
|
|
9841
9891
|
"name": "axisWidth",
|
|
9842
9892
|
"type": "number",
|
|
9843
|
-
"default": "
|
|
9844
|
-
"description": "
|
|
9893
|
+
"default": "automatic",
|
|
9894
|
+
"description": "An override for the width the y-axis reserves, in px (pixels). By default the axis sizes itself to its widest formatted tick label, never narrower than 48, so a currency symbol is never cut off. Set it only to line up the axes of several panels."
|
|
9845
9895
|
},
|
|
9846
9896
|
{
|
|
9847
9897
|
"name": "bands",
|
|
@@ -965,6 +965,132 @@
|
|
|
965
965
|
"value": "#0a0602",
|
|
966
966
|
"use": "Text on category.red.fill."
|
|
967
967
|
},
|
|
968
|
+
{
|
|
969
|
+
"name": "--color-severity-1-text",
|
|
970
|
+
"layer": "semantic",
|
|
971
|
+
"value": {
|
|
972
|
+
"dark": "#49d092",
|
|
973
|
+
"light": "#1d714b"
|
|
974
|
+
},
|
|
975
|
+
"use": "Subtle Badge severity=1: text and border. A step, with its word."
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
"name": "--color-severity-1-fill",
|
|
979
|
+
"layer": "semantic",
|
|
980
|
+
"value": "#2fb577",
|
|
981
|
+
"use": "Solid Badge severity=1: the fill."
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
"name": "--color-severity-1-ink",
|
|
985
|
+
"layer": "semantic",
|
|
986
|
+
"value": "#0a0602",
|
|
987
|
+
"use": "Text on severity.1.fill."
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
"name": "--color-severity-2-text",
|
|
991
|
+
"layer": "semantic",
|
|
992
|
+
"value": {
|
|
993
|
+
"dark": "#ffcb79",
|
|
994
|
+
"light": "#804e00"
|
|
995
|
+
},
|
|
996
|
+
"use": "Subtle Badge severity=2: text and border. A step, with its word."
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
"name": "--color-severity-2-fill",
|
|
1000
|
+
"layer": "semantic",
|
|
1001
|
+
"value": "#ffbb52",
|
|
1002
|
+
"use": "Solid Badge severity=2: the fill."
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
"name": "--color-severity-2-ink",
|
|
1006
|
+
"layer": "semantic",
|
|
1007
|
+
"value": "#0a0602",
|
|
1008
|
+
"use": "Text on severity.2.fill."
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
"name": "--color-severity-3-text",
|
|
1012
|
+
"layer": "semantic",
|
|
1013
|
+
"value": {
|
|
1014
|
+
"dark": "#f19300",
|
|
1015
|
+
"light": "#b13200"
|
|
1016
|
+
},
|
|
1017
|
+
"use": "Subtle Badge severity=3: text and border. A step, with its word."
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
"name": "--color-severity-3-fill",
|
|
1021
|
+
"layer": "semantic",
|
|
1022
|
+
"value": "#f19300",
|
|
1023
|
+
"use": "Solid Badge severity=3: the fill."
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
"name": "--color-severity-3-ink",
|
|
1027
|
+
"layer": "semantic",
|
|
1028
|
+
"value": "#0a0602",
|
|
1029
|
+
"use": "Text on severity.3.fill."
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
"name": "--color-severity-4-text",
|
|
1033
|
+
"layer": "semantic",
|
|
1034
|
+
"value": {
|
|
1035
|
+
"dark": "#ff5512",
|
|
1036
|
+
"light": "#ae0a0a"
|
|
1037
|
+
},
|
|
1038
|
+
"use": "Subtle Badge severity=4: text and border. A step, with its word."
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
"name": "--color-severity-4-fill",
|
|
1042
|
+
"layer": "semantic",
|
|
1043
|
+
"value": "#f33636",
|
|
1044
|
+
"use": "Solid Badge severity=4: the fill."
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
"name": "--color-severity-4-ink",
|
|
1048
|
+
"layer": "semantic",
|
|
1049
|
+
"value": "#0a0602",
|
|
1050
|
+
"use": "Text on severity.4.fill."
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
"name": "--color-severity-5-text",
|
|
1054
|
+
"layer": "semantic",
|
|
1055
|
+
"value": {
|
|
1056
|
+
"dark": "#f55b5b",
|
|
1057
|
+
"light": "#790707"
|
|
1058
|
+
},
|
|
1059
|
+
"use": "Subtle Badge severity=5: text and border. A step, with its word."
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
"name": "--color-severity-5-fill",
|
|
1063
|
+
"layer": "semantic",
|
|
1064
|
+
"value": "#ae0a0a",
|
|
1065
|
+
"use": "Solid Badge severity=5: the fill."
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
"name": "--color-severity-5-ink",
|
|
1069
|
+
"layer": "semantic",
|
|
1070
|
+
"value": "#ffffff",
|
|
1071
|
+
"use": "Text on severity.5.fill."
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
"name": "--color-severity-6-text",
|
|
1075
|
+
"layer": "semantic",
|
|
1076
|
+
"value": {
|
|
1077
|
+
"dark": "#f78181",
|
|
1078
|
+
"light": "#440404"
|
|
1079
|
+
},
|
|
1080
|
+
"use": "Subtle Badge severity=6: text and border. A step, with its word."
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
"name": "--color-severity-6-fill",
|
|
1084
|
+
"layer": "semantic",
|
|
1085
|
+
"value": "#440404",
|
|
1086
|
+
"use": "Solid Badge severity=6: the fill."
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
"name": "--color-severity-6-ink",
|
|
1090
|
+
"layer": "semantic",
|
|
1091
|
+
"value": "#ffffff",
|
|
1092
|
+
"use": "Text on severity.6.fill."
|
|
1093
|
+
},
|
|
968
1094
|
{
|
|
969
1095
|
"name": "--color-numeral-pad",
|
|
970
1096
|
"layer": "semantic",
|
|
@@ -1,24 +1,47 @@
|
|
|
1
|
-
// The one ranking for "find me the thing for X". The MCP server's `search` tool and the docs site's
|
|
2
|
-
// top-bar search both call it, so an agent and a person typing the same words get the same order.
|
|
3
|
-
// Pure and dependency-free: it runs in Node and in the browser.
|
|
4
|
-
|
|
5
|
-
export const words = text => String(text).toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
// The one ranking for "find me the thing for X". The MCP server's `search` tool and the docs site's
|
|
2
|
+
// top-bar search both call it, so an agent and a person typing the same words get the same order.
|
|
3
|
+
// Pure and dependency-free: it runs in Node and in the browser.
|
|
4
|
+
|
|
5
|
+
export const words = text => String(text).toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)
|
|
6
|
+
|
|
7
|
+
// Words that say nothing about the component. Left in, "a" and "an" prefix-match half the
|
|
8
|
+
// summaries, so "pick a country" ranked on filler (issue #2).
|
|
9
|
+
// "one" stays: it is what tells Select from MultiSelect. Dropped only when the query has another
|
|
10
|
+
// word, so a search for "the" alone still works.
|
|
11
|
+
const STOP = new Set(['a', 'an', 'the', 'of', 'to', 'for', 'from', 'in', 'on', 'with', 'and', 'or', 'some', 'i', 'me', 'my', 'user', 'users', 'let', 'lets'])
|
|
12
|
+
|
|
13
|
+
// Ways people say the same intent. A query word also matches the other words of its group, so
|
|
14
|
+
// "pick a country" and "choose an option" reach Select, whose own words are select and choice.
|
|
15
|
+
const SYNONYMS = [
|
|
16
|
+
['pick', 'choose', 'select', 'choice', 'option'],
|
|
17
|
+
]
|
|
18
|
+
const variants = term => {
|
|
19
|
+
const group = SYNONYMS.find(g => g.includes(term))
|
|
20
|
+
return group ? [term, ...group.filter(w => w !== term)] : [term]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A scorer for one query. `title` is the names, `body` the summary; `props` (descriptions) and
|
|
24
|
+
* `propNames` count once each, as hints, so a long prop table does not outrank a component or
|
|
25
|
+
* token whose own summary is about the query. 0 means no match. */
|
|
26
|
+
export function scorer(query) {
|
|
27
|
+
const all_ = words(query)
|
|
28
|
+
const kept = all_.filter(w => !STOP.has(w))
|
|
29
|
+
const terms = (kept.length ? kept : all_).map(variants)
|
|
30
|
+
return (title, body, props = '', propNames = '') => {
|
|
31
|
+
if (terms.length === 0) return 0
|
|
32
|
+
const t = words(title).join(' ')
|
|
33
|
+
const b = words(body)
|
|
34
|
+
const p = words(props)
|
|
35
|
+
const n = words(propNames)
|
|
36
|
+
const tw = t.split(' ')
|
|
37
|
+
const inTitle = vs => vs.some(v => t.includes(v))
|
|
38
|
+
// A name that is the word beats a name that contains it: Select over MultiSelect for "select".
|
|
39
|
+
const titleWord = vs => (vs.some(v => tw.includes(v)) ? 1 : 0)
|
|
40
|
+
const bodyHits = vs => b.filter(w => vs.some(v => w.startsWith(v))).length
|
|
41
|
+
const inMain = vs => inTitle(vs) || bodyHits(vs) > 0
|
|
42
|
+
// Every word of the query in the name or summary beats one word found often: "page
|
|
43
|
+
// background" is the surface token, not PageHeader, whose name matches "page" alone.
|
|
44
|
+
const all = terms.length > 1 && terms.every(inMain) ? 5 : 0
|
|
45
|
+
return all + terms.reduce((sum, vs) => sum + (inTitle(vs) ? 3 + titleWord(vs) : 0) + Math.min(bodyHits(vs), 3) + (inMain(vs) ? 0 : n.some(w => vs.some(v => w.startsWith(v))) ? 2 : p.some(w => vs.some(v => w.startsWith(v))) ? 1 : 0), 0)
|
|
46
|
+
}
|
|
47
|
+
}
|