@jonmilner/ui 0.7.0 → 0.8.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/dist/Badge/Badge.css.mjs +8 -3
- package/dist/Badge/index.d.mts +4 -2
- package/dist/Badge/index.mjs +27 -21
- package/dist/MultiSelect/MultiSelect.css +1 -1
- package/dist/MultiSelect/client.d.mts +1 -0
- package/dist/MultiSelect/client.mjs +5 -4
- package/dist/MultiSelect/index.d.mts +2 -1
- package/dist/MultiSelect/index.mjs +12 -10
- package/dist/SearchField/SearchField.css +1 -1
- package/dist/SearchField/SearchField.css.mjs +2 -2
- package/dist/SearchField/client.mjs +38 -37
- package/dist/Tabs/Tabs.css +1 -1
- package/dist/Tag/Tag.css.mjs +14 -4
- package/dist/Tag/index.d.mts +3 -1
- package/dist/Tag/index.mjs +46 -42
- package/dist/base.css +1 -1
- package/dist/brands/default.mjs +35 -9
- package/dist/brands/forced-colors.mjs +14 -0
- package/dist/contrast/contrast.mjs +36 -30
- package/dist/docs/Badge.md +13 -4
- package/dist/docs/Icon.md +1 -1
- package/dist/docs/MultiSelect.md +4 -2
- package/dist/docs/Tag.md +12 -3
- package/dist/docs/llms.txt +1 -1
- package/dist/docs/manifest.json +81 -8
- package/dist/docs/migrations/0.8.0.md +69 -0
- package/dist/docs/theming.md +5 -1
- package/dist/styles/atoms.css +1 -1
- package/dist/styles/atoms.css.d.mts +2 -2
- package/dist/styles/atoms.css.mjs +71 -49
- package/dist/styles/subtle-fills.d.mts +29 -0
- package/dist/styles/subtle-fills.mjs +29 -0
- package/dist/tokens/contract.d.mts +1 -1
- package/dist/tokens/roles.d.mts +1 -1
- package/dist/tokens/roles.mjs +1 -21
- package/package.json +1 -1
package/dist/brands/default.mjs
CHANGED
|
@@ -61,11 +61,23 @@ var s = (e, t) => Object.fromEntries(Object.entries(t).map(([t, [n, r]]) => [t,
|
|
|
61
61
|
800: [.42, .085],
|
|
62
62
|
900: [.37, .07],
|
|
63
63
|
950: [.27, .05]
|
|
64
|
-
}), h = {
|
|
64
|
+
}), h = s(295, {
|
|
65
|
+
50: [.97, .014],
|
|
66
|
+
100: [.938, .032],
|
|
67
|
+
200: [.886, .06],
|
|
68
|
+
300: [.815, .1],
|
|
69
|
+
400: [.72, .15],
|
|
70
|
+
500: [.63, .19],
|
|
71
|
+
600: [.55, .2],
|
|
72
|
+
700: [.49, .19],
|
|
73
|
+
800: [.425, .165],
|
|
74
|
+
900: [.38, .14],
|
|
75
|
+
950: [.28, .095]
|
|
76
|
+
}), g = {
|
|
65
77
|
metrics: i,
|
|
66
78
|
fallback: t,
|
|
67
79
|
generic: "sans-serif"
|
|
68
|
-
},
|
|
80
|
+
}, _ = {
|
|
69
81
|
metrics: a,
|
|
70
82
|
fallback: [
|
|
71
83
|
o,
|
|
@@ -73,7 +85,7 @@ var s = (e, t) => Object.fromEntries(Object.entries(t).map(([t, [n, r]]) => [t,
|
|
|
73
85
|
r
|
|
74
86
|
],
|
|
75
87
|
generic: "monospace"
|
|
76
|
-
},
|
|
88
|
+
}, v = {
|
|
77
89
|
name: "default",
|
|
78
90
|
color: {
|
|
79
91
|
bg: e(c, u[950]),
|
|
@@ -93,16 +105,30 @@ var s = (e, t) => Object.fromEntries(Object.entries(t).map(([t, [n, r]]) => [t,
|
|
|
93
105
|
successFg: e(c, u[950]),
|
|
94
106
|
warning: e(m[600], m[400]),
|
|
95
107
|
warningFg: e(c, u[950]),
|
|
108
|
+
done: e(h[600], h[400]),
|
|
109
|
+
doneFg: e(c, u[950]),
|
|
110
|
+
neutralSubtle: e(u[100], u[800]),
|
|
111
|
+
neutralSubtleFg: e(u[700], u[200]),
|
|
112
|
+
accentSubtle: e(d[100], d[950]),
|
|
113
|
+
accentSubtleFg: e(d[700], d[300]),
|
|
114
|
+
successSubtle: e(p[100], p[950]),
|
|
115
|
+
successSubtleFg: e(p[700], p[300]),
|
|
116
|
+
warningSubtle: e(m[100], m[950]),
|
|
117
|
+
warningSubtleFg: e(m[700], m[300]),
|
|
118
|
+
dangerSubtle: e(f[100], f[950]),
|
|
119
|
+
dangerSubtleFg: e(f[700], f[300]),
|
|
120
|
+
doneSubtle: e(h[100], h[950]),
|
|
121
|
+
doneSubtleFg: e(h[700], h[300]),
|
|
96
122
|
focus: e(d[600], d[400]),
|
|
97
123
|
shadow: e(u[950], l)
|
|
98
124
|
},
|
|
99
125
|
font: {
|
|
100
|
-
body:
|
|
101
|
-
heading:
|
|
102
|
-
display:
|
|
103
|
-
label:
|
|
104
|
-
mono:
|
|
126
|
+
body: g,
|
|
127
|
+
heading: g,
|
|
128
|
+
display: g,
|
|
129
|
+
label: g,
|
|
130
|
+
mono: _
|
|
105
131
|
}
|
|
106
132
|
};
|
|
107
133
|
//#endregion
|
|
108
|
-
export {
|
|
134
|
+
export { v as defaultBrand };
|
|
@@ -17,6 +17,20 @@ var e = {
|
|
|
17
17
|
successFg: "Canvas",
|
|
18
18
|
warning: "CanvasText",
|
|
19
19
|
warningFg: "Canvas",
|
|
20
|
+
done: "CanvasText",
|
|
21
|
+
doneFg: "Canvas",
|
|
22
|
+
neutralSubtle: "Canvas",
|
|
23
|
+
neutralSubtleFg: "CanvasText",
|
|
24
|
+
accentSubtle: "Canvas",
|
|
25
|
+
accentSubtleFg: "CanvasText",
|
|
26
|
+
successSubtle: "Canvas",
|
|
27
|
+
successSubtleFg: "CanvasText",
|
|
28
|
+
warningSubtle: "Canvas",
|
|
29
|
+
warningSubtleFg: "CanvasText",
|
|
30
|
+
dangerSubtle: "Canvas",
|
|
31
|
+
dangerSubtleFg: "CanvasText",
|
|
32
|
+
doneSubtle: "Canvas",
|
|
33
|
+
doneSubtleFg: "CanvasText",
|
|
20
34
|
focus: "Highlight",
|
|
21
35
|
shadow: "CanvasText"
|
|
22
36
|
};
|
|
@@ -1,57 +1,63 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { subtleFills as e } from "../styles/subtle-fills.mjs";
|
|
2
|
+
import { decode as t, resolveColor as n } from "./color.mjs";
|
|
2
3
|
//#region src/contrast/contrast.ts
|
|
3
|
-
var
|
|
4
|
+
var r = [
|
|
4
5
|
"bg",
|
|
5
6
|
"bgMuted",
|
|
6
7
|
"bgRaised"
|
|
7
|
-
],
|
|
8
|
+
], i = (e, t, n) => e.flatMap((e) => t.map((t) => ({
|
|
8
9
|
fg: e,
|
|
9
10
|
bg: t,
|
|
10
11
|
min: n
|
|
11
|
-
}))),
|
|
12
|
-
...
|
|
12
|
+
}))), a = Object.values(e).map(({ color: e, backgroundColor: t }) => [e, t]), o = [
|
|
13
|
+
...i([
|
|
13
14
|
"fg",
|
|
14
15
|
"fgMuted",
|
|
15
16
|
"accent",
|
|
16
17
|
"danger",
|
|
17
18
|
"success",
|
|
18
|
-
"warning"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
...
|
|
22
|
-
...
|
|
23
|
-
...
|
|
24
|
-
...
|
|
25
|
-
...
|
|
26
|
-
],
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
"warning",
|
|
20
|
+
"done"
|
|
21
|
+
], r, 4.5),
|
|
22
|
+
...i(["accentFg"], ["accent"], 4.5),
|
|
23
|
+
...i(["accentFillFg"], ["accentFill"], 4.5),
|
|
24
|
+
...i(["dangerFg"], ["danger"], 4.5),
|
|
25
|
+
...i(["successFg"], ["success"], 4.5),
|
|
26
|
+
...i(["warningFg"], ["warning"], 4.5),
|
|
27
|
+
...i(["doneFg"], ["done"], 4.5),
|
|
28
|
+
...a.flatMap(([e, t]) => i([e], [t], 4.5)),
|
|
29
|
+
...i(["borderStrong", "focus"], r, 3),
|
|
30
|
+
...i(["focus"], a.map(([, e]) => e), 3),
|
|
31
|
+
...i(a.map(([e]) => e), ["bg"], 3)
|
|
32
|
+
], s = ["light", "dark"];
|
|
33
|
+
function c({ r: e, g: n, b: r }) {
|
|
34
|
+
return .2126 * t(e) + .7152 * t(n) + .0722 * t(r);
|
|
29
35
|
}
|
|
30
|
-
function
|
|
31
|
-
return
|
|
36
|
+
function l(e, t, r = "light") {
|
|
37
|
+
return u(n(e, r), n(t, r), t);
|
|
32
38
|
}
|
|
33
|
-
function
|
|
39
|
+
function u(e, t, n) {
|
|
34
40
|
if (t.alpha < 1) throw Error(`Contrast background must be opaque: ${n}`);
|
|
35
|
-
let r = (t, n) => t * e.alpha + n * (1 - e.alpha), [i, a] = [
|
|
41
|
+
let r = (t, n) => t * e.alpha + n * (1 - e.alpha), [i, a] = [c({
|
|
36
42
|
r: r(e.r, t.r),
|
|
37
43
|
g: r(e.g, t.g),
|
|
38
44
|
b: r(e.b, t.b),
|
|
39
45
|
alpha: 1
|
|
40
|
-
}),
|
|
46
|
+
}), c(t)].toSorted((e, t) => t - e);
|
|
41
47
|
return (i + .05) / (a + .05);
|
|
42
48
|
}
|
|
43
|
-
function
|
|
44
|
-
return
|
|
45
|
-
let r = /* @__PURE__ */ new Map(),
|
|
46
|
-
return
|
|
47
|
-
let r =
|
|
48
|
-
return r <
|
|
49
|
-
...
|
|
50
|
-
mode:
|
|
49
|
+
function d(e) {
|
|
50
|
+
return s.flatMap((t) => {
|
|
51
|
+
let r = /* @__PURE__ */ new Map(), i = (i) => r.get(i) ?? r.set(i, n(e.color[i], t)).get(i);
|
|
52
|
+
return o.flatMap((n) => {
|
|
53
|
+
let r = u(i(n.fg), i(n.bg), e.color[n.bg]);
|
|
54
|
+
return r < n.min ? [{
|
|
55
|
+
...n,
|
|
56
|
+
mode: t,
|
|
51
57
|
ratio: r
|
|
52
58
|
}] : [];
|
|
53
59
|
});
|
|
54
60
|
});
|
|
55
61
|
}
|
|
56
62
|
//#endregion
|
|
57
|
-
export {
|
|
63
|
+
export { d as checkContrast, o as contrastPairs, l as contrastRatio };
|
package/dist/docs/Badge.md
CHANGED
|
@@ -21,7 +21,8 @@ A static label that states the status of, or a short fact about, the thing besid
|
|
|
21
21
|
|
|
22
22
|
## Rules
|
|
23
23
|
|
|
24
|
-
- `variant`: `neutral` (default, `fgMuted` on `bgMuted`) for most statuses and facts; `accent` (`accentFg` on `accent`) to draw the eye; `success` (`successFg` on `success`) for a
|
|
24
|
+
- `variant`: `neutral` (default, `fgMuted` on `bgMuted`) for most statuses and facts; `accent` (`accentFg` on `accent`) to draw the eye; `success` (`successFg` on `success`) for a passing or healthy state; `warning` (`warningFg` on `warning`) for one that needs attention soon; `danger` (`dangerFg` on `danger`) for a failure or a blocked state; `done` (`doneFg` on `done`, violet) for finished work, such as a merged PR or a closed issue.
|
|
25
|
+
- `subtle`: `false` (default) is the solid fill above; `true` swaps it for the variant's tint, `<variant>SubtleFg` on `<variant>Subtle` (`neutralSubtleFg` on `neutralSubtle` and so on). Use subtle where many badges sit together, such as a list or a board, so each one stays calm and the color still means one thing; keep solid for the one status that should stand out.
|
|
25
26
|
- `size`: `sm` or `med` (default), the label's `fontSize` step.
|
|
26
27
|
- The label states the state. Color is never the only cue (WCAG 1.4.1): under forced colors every variant, `success` and `warning` included, is the same outlined box, and some readers cannot tell the fills apart (green from red above all). Write "Failed" or "Passed", not a red or green badge reading "3".
|
|
27
28
|
- Keep it to one or two words; it never wraps. With no children it renders nothing.
|
|
@@ -44,6 +45,13 @@ A static label that states the status of, or a short fact about, the thing besid
|
|
|
44
45
|
</Badge>
|
|
45
46
|
```
|
|
46
47
|
|
|
48
|
+
```tsx
|
|
49
|
+
// A row of statuses in a list: subtle, so none shouts.
|
|
50
|
+
<Badge subtle variant="done">
|
|
51
|
+
Merged
|
|
52
|
+
</Badge>
|
|
53
|
+
```
|
|
54
|
+
|
|
47
55
|
## Don't
|
|
48
56
|
|
|
49
57
|
```tsx
|
|
@@ -60,8 +68,8 @@ A static label that states the status of, or a short fact about, the thing besid
|
|
|
60
68
|
|
|
61
69
|
- It is plain text in a `span`: screen readers read the label in line with what is around it, so the label must make sense there ("Quarterly report Draft").
|
|
62
70
|
- No role, no `aria-live`: a badge that changes is not announced. If a status change matters right away, announce it elsewhere (a toast).
|
|
63
|
-
- Each variant's text and fill pass AA (4.5:1) in light and dark mode; the pairs are in `contrastPairs` (ADR 0033).
|
|
64
|
-
- Under forced colors every badge is `CanvasText` on `Canvas` with a `CanvasText` outline, so the box survives and the variants look alike; the label carries the meaning. The role fills are not kept: `accent` would take the link color, on something that is not a link, and the browser's backplate would hide the filled variants' text. `success`, `warning` and `
|
|
71
|
+
- Each variant's text and fill, solid and subtle, pass AA (4.5:1) in light and dark mode; the pairs are in `contrastPairs` (ADR 0033, 0121). Each subtle text also passes 3:1 on `bg`.
|
|
72
|
+
- Under forced colors every badge is `CanvasText` on `Canvas` with a `CanvasText` outline, so the box survives and the variants, solid or subtle, look alike; the label carries the meaning. The role fills are not kept: `accent` would take the link color, on something that is not a link, and the browser's backplate would hide the filled variants' text. `success`, `warning`, `danger` and `done` map to the same system pair (ADR 0104, 0121), so only the label tells them apart.
|
|
65
73
|
|
|
66
74
|
## Props
|
|
67
75
|
|
|
@@ -72,6 +80,7 @@ Generated from the TypeScript types.
|
|
|
72
80
|
| Prop | Type | Required | Description |
|
|
73
81
|
| --- | --- | --- | --- |
|
|
74
82
|
| `size` | `"med" \| "sm"` | no | |
|
|
75
|
-
| `
|
|
83
|
+
| `subtle` | `boolean` | no | |
|
|
84
|
+
| `variant` | `"accent" \| "danger" \| "done" \| "neutral" \| "success" \| "warning"` | no | |
|
|
76
85
|
|
|
77
86
|
Also accepts the style props (see `manifest.json` `styleProps`).
|
package/dist/docs/Icon.md
CHANGED
|
@@ -117,7 +117,7 @@ Generated from the TypeScript types.
|
|
|
117
117
|
|
|
118
118
|
| Prop | Type | Required | Description |
|
|
119
119
|
| --- | --- | --- | --- |
|
|
120
|
-
| `color` | `"accent" \| "accentFg" \| "accentFillFg" \| "bg" \| "bgMuted" \| "bgRaised" \| "border" \| "borderStrong" \| "danger" \| "dangerFg" \| "fg" \| "fgMuted" \| "focus" \| "success" \| "successFg" \| "warning" \| "warningFg"` | no | |
|
|
120
|
+
| `color` | `"accent" \| "accentFg" \| "accentFillFg" \| "accentSubtleFg" \| "bg" \| "bgMuted" \| "bgRaised" \| "border" \| "borderStrong" \| "danger" \| "dangerFg" \| "dangerSubtleFg" \| "done" \| "doneFg" \| "doneSubtleFg" \| "fg" \| "fgMuted" \| "focus" \| "neutralSubtleFg" \| "success" \| "successFg" \| "successSubtleFg" \| "warning" \| "warningFg" \| "warningSubtleFg"` | no | |
|
|
121
121
|
| `label` | `string` | no | |
|
|
122
122
|
| `size` | `"2xl" \| "3xl" \| "4xl" \| "lg" \| "med" \| "sm" \| "xl" \| "xs"` | no | |
|
|
123
123
|
| `strokeWidth` | `number` | no | |
|
package/dist/docs/MultiSelect.md
CHANGED
|
@@ -25,6 +25,7 @@ A form field for picking any number of values from a list too long to show at on
|
|
|
25
25
|
- `value` with `onValueChange` controls it; `defaultValue` leaves it uncontrolled. Both are arrays of option values, and `onValueChange` gets the new array.
|
|
26
26
|
- `name` makes each selected value submit as its own `name=value` pair, as a `<select multiple>` does; read them with `formData.getAll(name)`. Nothing selected submits nothing.
|
|
27
27
|
- `placeholder` (`"Select"`) shows while nothing is selected. `countLabel` (`"{count} selected"`) is the summary once the joined labels do not fit; `{count}` is replaced with the number selected. Translate both. One selected label that does not fit is truncated, not replaced by a count.
|
|
28
|
+
- `size` (`sm`, `med`, `lg`; default `med`) is the trigger's, as Button's: it matches a SearchField, TextField or Button of the same size in height. The list's options do not change size.
|
|
28
29
|
- `disabled` takes the trigger out of the tab order and mutes the label. `required` sets `aria-required` on the trigger; report a missing value through `error`.
|
|
29
30
|
- The trigger and the open listbox are both named by `label`.
|
|
30
31
|
- Style props, `className` and `style` go to the `Field` around the label, trigger and messages, not to the trigger or the popup.
|
|
@@ -91,9 +92,9 @@ A form field for picking any number of values from a list too long to show at on
|
|
|
91
92
|
- The trigger is a button with `aria-haspopup="listbox"` and `aria-expanded`, labelled by the `Field` label and described by the description and error. The popup is `role="listbox"` with `aria-multiselectable="true"`, options are `role="option"` with `aria-selected`.
|
|
92
93
|
- Opening from the keyboard focuses an option; focus stays in the list until it closes. Choosing an option toggles it and leaves the list open, so several can be picked in one visit. Closed by Esc or the trigger, focus returns at once to the trigger, so a Tab pressed while the list fades goes on to the next control. A press outside or a Tab out leaves focus to Base UI.
|
|
93
94
|
- A closed list stays on the page while it fades out, but is `inert` and `aria-hidden` from the moment it closes, so a list opened during the fade (the next filter's) is the only one a screen reader or a role query finds, and nothing in the fading one takes focus or a click (ADR 0113).
|
|
94
|
-
- Selection is a check mark on the option as well as its highlight, so it is not color alone. The check
|
|
95
|
+
- Selection is a check mark on the option as well as its highlight, so it is not color alone. The check sits at the end of the row; its space is always kept, so the labels start at the option's padding and never shift.
|
|
95
96
|
- The trigger summary is text: the joined labels, or the count. When the count replaces the labels, the labels are hidden from assistive technology so the count is read once.
|
|
96
|
-
- The shared focus ring shows on the trigger and, from the keyboard, on the highlighted option. Under forced colors the highlighted option is `Highlight` with `HighlightText
|
|
97
|
+
- The shared focus ring shows on the trigger and, from the keyboard, on the highlighted option, drawn inset so the popup's edge does not crowd or clip it (ADR 0122). Under forced colors the pointer's highlighted option is `Highlight` with `HighlightText`; a keyboard-focused one keeps `Canvas` so the `Highlight` ring shows, and the popup keeps its border.
|
|
97
98
|
|
|
98
99
|
## Props
|
|
99
100
|
|
|
@@ -115,6 +116,7 @@ Generated from the TypeScript types.
|
|
|
115
116
|
| `options` | `readonly Option[]` | yes | |
|
|
116
117
|
| `placeholder` | `string` | no | |
|
|
117
118
|
| `required` | `boolean` | no | |
|
|
119
|
+
| `size` | `"lg" \| "med" \| "sm"` | no | |
|
|
118
120
|
| `style` | `CSSProperties` | no | |
|
|
119
121
|
| `value` | `readonly string[]` | no | |
|
|
120
122
|
|
package/dist/docs/Tag.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
A label that puts content in a category: a topic, a label, a user-made category. A `span` with a
|
|
5
|
+
A label that puts content in a category: a topic, a label, a user-made category. A `span` with a subtle tone fill (gray by default) and an `lg` radius (half a one-line tag's height, so one line reads as a pill and wrapped lines stay inside the fill) around a `Text` in the `label` font. It can be read-only, a link, or removable.
|
|
6
6
|
|
|
7
7
|
## When to use
|
|
8
8
|
|
|
@@ -20,7 +20,8 @@ A label that puts content in a category: a topic, a label, a user-made category.
|
|
|
20
20
|
|
|
21
21
|
## Rules
|
|
22
22
|
|
|
23
|
-
- `size`: `sm` or `med` (default), the label's `fontSize` step.
|
|
23
|
+
- `size`: `sm` or `med` (default), the label's `fontSize` step.
|
|
24
|
+
- `variant`: the tone's subtle fill, `<variant>SubtleFg` on `<variant>Subtle`. `neutral` (default) for most categories; `accent`, `success`, `warning`, `danger` and `done` only where the category has a meaning those colors already carry in the app, as Badge's variants do. Tag has no solid fills: a category label stays calm (ADR 0121). The tone is never the only cue: the label names the category.
|
|
24
25
|
- Read-only is the default. `href` makes the label a link (a `Clickable` `<a>` around the text, underlined). The tag itself stays a `span`, so a remove button never nests in the link.
|
|
25
26
|
- `onRemove` adds a native `<button>` after the label with an `x` icon. Its name is "Remove <label>" from a string or number label; set `removeLabel` for any other label. Enter and Space activate it; the click calls `onRemove`. Tag handles no keys, so Backspace and Delete do nothing (ADR 0003, 0055).
|
|
26
27
|
- Wrapping: an uncapped label wraps to the width it has, so a tag never overflows a narrow container or a 320px viewport at large text sizes. Every line stays inside the fill and its rounded corners, and inside the outline under forced colors.
|
|
@@ -56,6 +57,12 @@ Tell screen reader users the tag is gone, with the owner's live region or by nam
|
|
|
56
57
|
<Tag style={{ maxInlineSize: "min(10rem, 100%)" }}>{longCategoryName}</Tag>
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
```tsx
|
|
61
|
+
<Tag variant="done" href="/labels/shipped">
|
|
62
|
+
Shipped
|
|
63
|
+
</Tag>
|
|
64
|
+
```
|
|
65
|
+
|
|
59
66
|
## Don't
|
|
60
67
|
|
|
61
68
|
```tsx
|
|
@@ -80,7 +87,8 @@ Tell screen reader users the tag is gone, with the owner's live region or by nam
|
|
|
80
87
|
- A read-only tag is plain text in a `span`, no role. A link tag is a link in the tab order. A removable tag adds one button; each button's name says which tag it removes.
|
|
81
88
|
- Nothing essential is only in a tooltip: truncated text is fully in the DOM and shows on hover and focus.
|
|
82
89
|
- The `x` icon is `aria-hidden`; the button's `aria-label` names it. The link and the button each get the shared focus ring and a 24px target.
|
|
83
|
-
-
|
|
90
|
+
- Each variant's text passes 4.5:1 on its fill and 3:1 on `bg`, and the focus ring 3:1 on the fill, in light and dark mode; the pairs are in `contrastPairs`.
|
|
91
|
+
- Under forced colors every variant is `CanvasText` on `Canvas` with a `CanvasText` outline, as Badge is, and the link and the button keep the `Highlight` focus ring.
|
|
84
92
|
|
|
85
93
|
## Props
|
|
86
94
|
|
|
@@ -94,5 +102,6 @@ Generated from the TypeScript types.
|
|
|
94
102
|
| `onRemove` | `(() => void)` | no | |
|
|
95
103
|
| `removeLabel` | `string` | no | |
|
|
96
104
|
| `size` | `"med" \| "sm"` | no | |
|
|
105
|
+
| `variant` | `"accent" \| "danger" \| "done" \| "neutral" \| "success" \| "warning"` | no | |
|
|
97
106
|
|
|
98
107
|
Also accepts the style props (see `manifest.json` `styleProps`).
|
package/dist/docs/llms.txt
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
- [SearchField](SearchField.md): A form field for a search query: an `<input type="search">` (role `searchbox`) with a search icon at its inline start and, while it holds a value, a clear button at its inline end. Escape empties a field that has a value, and the clear button empties it and puts focus back in the input, so a keyboard user never loses their place (ADR 0087). Built on Base UI's `Input` (ADR 0003); it composes `Field` for its label, description and error (ADR 0026). Three sizes at Button's heights (`sm`, `med`, `lg`: 32, 40 and 48 px).
|
|
25
25
|
- [Stack](Stack.md): A vertical column of children with even space between them. `Stack` is a `Box` with `display="flex"` and `flexDirection="column"` as default style props, spaced by a required `gap`. Children render as they are: no wrapper per child.
|
|
26
26
|
- [Tabs](Tabs.md): Switches between views that share one place on the page. The parts are `Tabs` (the root, a flex column; a row when vertical), `Tabs.List` (the `tablist`), `Tabs.Tab` (a `tab` button on `Clickable`) and `Tabs.Panel` (the `tabpanel`). Built on Base UI `Tabs` (ADR 0003, 0046): it owns the selection, one tab stop with arrow-key roving, `aria-selected`, and the ids that tie each tab to its panel. The selected tab shows an accent bar that fades and grows in (ADR 0049).
|
|
27
|
-
- [Tag](Tag.md): A label that puts content in a category: a topic, a label, a user-made category. A `span` with a
|
|
27
|
+
- [Tag](Tag.md): A label that puts content in a category: a topic, a label, a user-made category. A `span` with a subtle tone fill (gray by default) and an `lg` radius (half a one-line tag's height, so one line reads as a pill and wrapped lines stay inside the fill) around a `Text` in the `label` font. It can be read-only, a link, or removable.
|
|
28
28
|
- [Text](Text.md): The base of all visible text, and the primitive every text component builds on. Capsize trims the box to the cap height and the baseline, so the gap around text is the visible space. It takes every Box style prop (`color`, `textAlign`, margins), plus `size`, `font`, `weight`, `leading`, `truncate` and `wrap`.
|
|
29
29
|
- [TextField](TextField.md): A form field for one line of text that is not a search: an `<input>` (role `textbox`) with SearchField's look and no icon or button over it. `type` picks `text`, `email`, `url` or `tel`. Built on Base UI's `Input` (ADR 0003); it composes `Field` for its label, description and error (ADR 0026, 0110). Three sizes at Button's heights (`sm`, `med`, `lg`: 32, 40 and 48 px).
|
|
30
30
|
- [Toast](Toast.md): Tells the user briefly about the result of an action without interrupting them: "Changes saved", "Message sent", "Couldn't upload the file". Material calls it a snackbar and Atlassian a flag. Built on Base UI's Toast, which owns the queue, the timers and swipe-to-dismiss. The parts add the look: a raised card in a stack at the bottom end corner, an icon per variant, a close button, and a slide-up-and-fade that is a plain fade under reduced motion. A toast never takes focus and moves nothing on the page.
|
package/dist/docs/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./manifest.schema.json",
|
|
3
3
|
"package": "@jonmilner/ui",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.1",
|
|
5
5
|
"components": [
|
|
6
6
|
{
|
|
7
7
|
"name": "AlertDialog",
|
|
@@ -174,13 +174,19 @@
|
|
|
174
174
|
"sm"
|
|
175
175
|
]
|
|
176
176
|
},
|
|
177
|
+
{
|
|
178
|
+
"name": "subtle",
|
|
179
|
+
"type": "boolean",
|
|
180
|
+
"required": false
|
|
181
|
+
},
|
|
177
182
|
{
|
|
178
183
|
"name": "variant",
|
|
179
|
-
"type": "\"accent\" | \"danger\" | \"neutral\" | \"success\" | \"warning\"",
|
|
184
|
+
"type": "\"accent\" | \"danger\" | \"done\" | \"neutral\" | \"success\" | \"warning\"",
|
|
180
185
|
"required": false,
|
|
181
186
|
"values": [
|
|
182
187
|
"accent",
|
|
183
188
|
"danger",
|
|
189
|
+
"done",
|
|
184
190
|
"neutral",
|
|
185
191
|
"success",
|
|
186
192
|
"warning"
|
|
@@ -1061,12 +1067,13 @@
|
|
|
1061
1067
|
"props": [
|
|
1062
1068
|
{
|
|
1063
1069
|
"name": "color",
|
|
1064
|
-
"type": "\"accent\" | \"accentFg\" | \"accentFillFg\" | \"bg\" | \"bgMuted\" | \"bgRaised\" | \"border\" | \"borderStrong\" | \"danger\" | \"dangerFg\" | \"fg\" | \"fgMuted\" | \"focus\" | \"success\" | \"successFg\" | \"warning\" | \"warningFg\"",
|
|
1070
|
+
"type": "\"accent\" | \"accentFg\" | \"accentFillFg\" | \"accentSubtleFg\" | \"bg\" | \"bgMuted\" | \"bgRaised\" | \"border\" | \"borderStrong\" | \"danger\" | \"dangerFg\" | \"dangerSubtleFg\" | \"done\" | \"doneFg\" | \"doneSubtleFg\" | \"fg\" | \"fgMuted\" | \"focus\" | \"neutralSubtleFg\" | \"success\" | \"successFg\" | \"successSubtleFg\" | \"warning\" | \"warningFg\" | \"warningSubtleFg\"",
|
|
1065
1071
|
"required": false,
|
|
1066
1072
|
"values": [
|
|
1067
1073
|
"accent",
|
|
1068
1074
|
"accentFg",
|
|
1069
1075
|
"accentFillFg",
|
|
1076
|
+
"accentSubtleFg",
|
|
1070
1077
|
"bg",
|
|
1071
1078
|
"bgMuted",
|
|
1072
1079
|
"bgRaised",
|
|
@@ -1074,13 +1081,20 @@
|
|
|
1074
1081
|
"borderStrong",
|
|
1075
1082
|
"danger",
|
|
1076
1083
|
"dangerFg",
|
|
1084
|
+
"dangerSubtleFg",
|
|
1085
|
+
"done",
|
|
1086
|
+
"doneFg",
|
|
1087
|
+
"doneSubtleFg",
|
|
1077
1088
|
"fg",
|
|
1078
1089
|
"fgMuted",
|
|
1079
1090
|
"focus",
|
|
1091
|
+
"neutralSubtleFg",
|
|
1080
1092
|
"success",
|
|
1081
1093
|
"successFg",
|
|
1094
|
+
"successSubtleFg",
|
|
1082
1095
|
"warning",
|
|
1083
|
-
"warningFg"
|
|
1096
|
+
"warningFg",
|
|
1097
|
+
"warningSubtleFg"
|
|
1084
1098
|
]
|
|
1085
1099
|
},
|
|
1086
1100
|
{
|
|
@@ -1601,6 +1615,16 @@
|
|
|
1601
1615
|
"type": "boolean",
|
|
1602
1616
|
"required": false
|
|
1603
1617
|
},
|
|
1618
|
+
{
|
|
1619
|
+
"name": "size",
|
|
1620
|
+
"type": "\"lg\" | \"med\" | \"sm\"",
|
|
1621
|
+
"required": false,
|
|
1622
|
+
"values": [
|
|
1623
|
+
"lg",
|
|
1624
|
+
"med",
|
|
1625
|
+
"sm"
|
|
1626
|
+
]
|
|
1627
|
+
},
|
|
1604
1628
|
{
|
|
1605
1629
|
"name": "style",
|
|
1606
1630
|
"type": "CSSProperties",
|
|
@@ -1926,6 +1950,19 @@
|
|
|
1926
1950
|
"med",
|
|
1927
1951
|
"sm"
|
|
1928
1952
|
]
|
|
1953
|
+
},
|
|
1954
|
+
{
|
|
1955
|
+
"name": "variant",
|
|
1956
|
+
"type": "\"accent\" | \"danger\" | \"done\" | \"neutral\" | \"success\" | \"warning\"",
|
|
1957
|
+
"required": false,
|
|
1958
|
+
"values": [
|
|
1959
|
+
"accent",
|
|
1960
|
+
"danger",
|
|
1961
|
+
"done",
|
|
1962
|
+
"neutral",
|
|
1963
|
+
"success",
|
|
1964
|
+
"warning"
|
|
1965
|
+
]
|
|
1929
1966
|
}
|
|
1930
1967
|
],
|
|
1931
1968
|
"parts": [],
|
|
@@ -2196,13 +2233,15 @@
|
|
|
2196
2233
|
},
|
|
2197
2234
|
{
|
|
2198
2235
|
"name": "backgroundColor",
|
|
2199
|
-
"type": "\"accent\" | \"accentFg\" | \"accentFill\" | \"accentFillFg\" | \"bg\" | \"bgMuted\" | \"bgRaised\" | \"border\" | \"borderStrong\" | \"danger\" | \"dangerFg\" | \"fg\" | \"fgMuted\" | \"focus\" | \"success\" | \"successFg\" | \"warning\" | \"warningFg\"",
|
|
2236
|
+
"type": "\"accent\" | \"accentFg\" | \"accentFill\" | \"accentFillFg\" | \"accentSubtle\" | \"accentSubtleFg\" | \"bg\" | \"bgMuted\" | \"bgRaised\" | \"border\" | \"borderStrong\" | \"danger\" | \"dangerFg\" | \"dangerSubtle\" | \"dangerSubtleFg\" | \"done\" | \"doneFg\" | \"doneSubtle\" | \"doneSubtleFg\" | \"fg\" | \"fgMuted\" | \"focus\" | \"neutralSubtle\" | \"neutralSubtleFg\" | \"success\" | \"successFg\" | \"successSubtle\" | \"successSubtleFg\" | \"warning\" | \"warningFg\" | \"warningSubtle\" | \"warningSubtleFg\"",
|
|
2200
2237
|
"required": false,
|
|
2201
2238
|
"values": [
|
|
2202
2239
|
"accent",
|
|
2203
2240
|
"accentFg",
|
|
2204
2241
|
"accentFill",
|
|
2205
2242
|
"accentFillFg",
|
|
2243
|
+
"accentSubtle",
|
|
2244
|
+
"accentSubtleFg",
|
|
2206
2245
|
"bg",
|
|
2207
2246
|
"bgMuted",
|
|
2208
2247
|
"bgRaised",
|
|
@@ -2210,13 +2249,25 @@
|
|
|
2210
2249
|
"borderStrong",
|
|
2211
2250
|
"danger",
|
|
2212
2251
|
"dangerFg",
|
|
2252
|
+
"dangerSubtle",
|
|
2253
|
+
"dangerSubtleFg",
|
|
2254
|
+
"done",
|
|
2255
|
+
"doneFg",
|
|
2256
|
+
"doneSubtle",
|
|
2257
|
+
"doneSubtleFg",
|
|
2213
2258
|
"fg",
|
|
2214
2259
|
"fgMuted",
|
|
2215
2260
|
"focus",
|
|
2261
|
+
"neutralSubtle",
|
|
2262
|
+
"neutralSubtleFg",
|
|
2216
2263
|
"success",
|
|
2217
2264
|
"successFg",
|
|
2265
|
+
"successSubtle",
|
|
2266
|
+
"successSubtleFg",
|
|
2218
2267
|
"warning",
|
|
2219
|
-
"warningFg"
|
|
2268
|
+
"warningFg",
|
|
2269
|
+
"warningSubtle",
|
|
2270
|
+
"warningSubtleFg"
|
|
2220
2271
|
]
|
|
2221
2272
|
},
|
|
2222
2273
|
{
|
|
@@ -2262,12 +2313,13 @@
|
|
|
2262
2313
|
},
|
|
2263
2314
|
{
|
|
2264
2315
|
"name": "color",
|
|
2265
|
-
"type": "\"accent\" | \"accentFg\" | \"accentFillFg\" | \"bg\" | \"bgMuted\" | \"bgRaised\" | \"border\" | \"borderStrong\" | \"danger\" | \"dangerFg\" | \"fg\" | \"fgMuted\" | \"focus\" | \"success\" | \"successFg\" | \"warning\" | \"warningFg\"",
|
|
2316
|
+
"type": "\"accent\" | \"accentFg\" | \"accentFillFg\" | \"accentSubtleFg\" | \"bg\" | \"bgMuted\" | \"bgRaised\" | \"border\" | \"borderStrong\" | \"danger\" | \"dangerFg\" | \"dangerSubtleFg\" | \"done\" | \"doneFg\" | \"doneSubtleFg\" | \"fg\" | \"fgMuted\" | \"focus\" | \"neutralSubtleFg\" | \"success\" | \"successFg\" | \"successSubtleFg\" | \"warning\" | \"warningFg\" | \"warningSubtleFg\"",
|
|
2266
2317
|
"required": false,
|
|
2267
2318
|
"values": [
|
|
2268
2319
|
"accent",
|
|
2269
2320
|
"accentFg",
|
|
2270
2321
|
"accentFillFg",
|
|
2322
|
+
"accentSubtleFg",
|
|
2271
2323
|
"bg",
|
|
2272
2324
|
"bgMuted",
|
|
2273
2325
|
"bgRaised",
|
|
@@ -2275,13 +2327,20 @@
|
|
|
2275
2327
|
"borderStrong",
|
|
2276
2328
|
"danger",
|
|
2277
2329
|
"dangerFg",
|
|
2330
|
+
"dangerSubtleFg",
|
|
2331
|
+
"done",
|
|
2332
|
+
"doneFg",
|
|
2333
|
+
"doneSubtleFg",
|
|
2278
2334
|
"fg",
|
|
2279
2335
|
"fgMuted",
|
|
2280
2336
|
"focus",
|
|
2337
|
+
"neutralSubtleFg",
|
|
2281
2338
|
"success",
|
|
2282
2339
|
"successFg",
|
|
2340
|
+
"successSubtleFg",
|
|
2283
2341
|
"warning",
|
|
2284
|
-
"warningFg"
|
|
2342
|
+
"warningFg",
|
|
2343
|
+
"warningSubtleFg"
|
|
2285
2344
|
]
|
|
2286
2345
|
},
|
|
2287
2346
|
{
|
|
@@ -2989,6 +3048,20 @@
|
|
|
2989
3048
|
"color.successFg": "var(--color-success-fg)",
|
|
2990
3049
|
"color.warning": "var(--color-warning)",
|
|
2991
3050
|
"color.warningFg": "var(--color-warning-fg)",
|
|
3051
|
+
"color.done": "var(--color-done)",
|
|
3052
|
+
"color.doneFg": "var(--color-done-fg)",
|
|
3053
|
+
"color.neutralSubtle": "var(--color-neutral-subtle)",
|
|
3054
|
+
"color.neutralSubtleFg": "var(--color-neutral-subtle-fg)",
|
|
3055
|
+
"color.accentSubtle": "var(--color-accent-subtle)",
|
|
3056
|
+
"color.accentSubtleFg": "var(--color-accent-subtle-fg)",
|
|
3057
|
+
"color.successSubtle": "var(--color-success-subtle)",
|
|
3058
|
+
"color.successSubtleFg": "var(--color-success-subtle-fg)",
|
|
3059
|
+
"color.warningSubtle": "var(--color-warning-subtle)",
|
|
3060
|
+
"color.warningSubtleFg": "var(--color-warning-subtle-fg)",
|
|
3061
|
+
"color.dangerSubtle": "var(--color-danger-subtle)",
|
|
3062
|
+
"color.dangerSubtleFg": "var(--color-danger-subtle-fg)",
|
|
3063
|
+
"color.doneSubtle": "var(--color-done-subtle)",
|
|
3064
|
+
"color.doneSubtleFg": "var(--color-done-subtle-fg)",
|
|
2992
3065
|
"color.focus": "var(--color-focus)",
|
|
2993
3066
|
"color.shadow": "var(--color-shadow)",
|
|
2994
3067
|
"font.body": "var(--font-body)",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# 0.8.0
|
|
2
|
+
|
|
3
|
+
`Brand["color"]` gains fourteen required roles: a violet `done` tone (`done`, `doneFg`) and a subtle fill and text pair for every tone (`neutralSubtle`, `accentSubtle`, `successSubtle`, `warningSubtle`, `dangerSubtle`, `doneSubtle`, each with its `*Fg`), for Badge's new `subtle` variants and Tag's new `variant` (decision 0121). A brand that lists its colors without spreading a preset no longer type-checks, and the neutral Tag's text moves from `fg` to `neutralSubtleFg` on the same gray fill.
|
|
4
|
+
|
|
5
|
+
## Find
|
|
6
|
+
|
|
7
|
+
In the app's source (`*.ts`, `*.tsx`), every `defineTheme(` call and every object typed `Brand` or `Brand["color"]`: regex `defineTheme\(|(:|satisfies|as)\s*Brand\b|Brand\["color"\]`. An aliased import of `Brand` needs its alias in place of `Brand`. For each, check its `color` object:
|
|
8
|
+
|
|
9
|
+
- It lists every role itself: it is missing the fourteen roles.
|
|
10
|
+
- It spreads a preset's colors (`...defaultBrand.color`) and recolors `accent`, `success`, `warning` or `danger`: it type-checks, but that tone's subtle pair stays the preset's.
|
|
11
|
+
- It spreads a preset's colors and recolors no tone: it type-checks, but if it recolors `focus` or `bg`, `defineTheme` now also needs `focus` at 3:1 on each subtle fill and each `*SubtleFg` at 3:1 on `bg`.
|
|
12
|
+
|
|
13
|
+
The only preset brand, `default`, ships every new role; an app on `brand="default"` needs no theme change.
|
|
14
|
+
|
|
15
|
+
In `*.tsx`, a subtle status fill or category label drawn by hand: regex `<Tag\b[^>]*(style|className|backgroundColor|color)=` and `<Badge\b[^>]*(style|className|backgroundColor|color)=`, and app tokens named `\w+Subtle\b`.
|
|
16
|
+
|
|
17
|
+
## Replace
|
|
18
|
+
|
|
19
|
+
Add the roles after `warningFg`, each a `light-dark(<light>, <dark>)` pair. `doneFg` needs 4.5:1 on `done`, and `done` 4.5:1 on each surface. Each `*SubtleFg` needs 4.5:1 on its fill and 3:1 on `bg`, and `focus` 3:1 on each subtle fill; `defineTheme` throws in development if not.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// before
|
|
23
|
+
color: {
|
|
24
|
+
// …every other role
|
|
25
|
+
warning: "light-dark(#b45309, #fbbf24)",
|
|
26
|
+
warningFg: "light-dark(#fff, #111)",
|
|
27
|
+
focus: "light-dark(#2563eb, #60a5fa)",
|
|
28
|
+
},
|
|
29
|
+
// after
|
|
30
|
+
color: {
|
|
31
|
+
// …every other role
|
|
32
|
+
warning: "light-dark(#b45309, #fbbf24)",
|
|
33
|
+
warningFg: "light-dark(#fff, #111)",
|
|
34
|
+
done: "light-dark(#7c3aed, #a78bfa)",
|
|
35
|
+
doneFg: "light-dark(#fff, #111)",
|
|
36
|
+
neutralSubtle: "light-dark(#f3f4f6, #1f2937)",
|
|
37
|
+
neutralSubtleFg: "light-dark(#374151, #e5e7eb)",
|
|
38
|
+
accentSubtle: "light-dark(#dbeafe, #172554)",
|
|
39
|
+
accentSubtleFg: "light-dark(#1d4ed8, #93c5fd)",
|
|
40
|
+
successSubtle: "light-dark(#dcfce7, #052e16)",
|
|
41
|
+
successSubtleFg: "light-dark(#15803d, #86efac)",
|
|
42
|
+
warningSubtle: "light-dark(#fef3c7, #451a03)",
|
|
43
|
+
warningSubtleFg: "light-dark(#b45309, #fcd34d)",
|
|
44
|
+
dangerSubtle: "light-dark(#fee2e2, #450a0a)",
|
|
45
|
+
dangerSubtleFg: "light-dark(#b91c1c, #fca5a5)",
|
|
46
|
+
doneSubtle: "light-dark(#ede9fe, #2e1065)",
|
|
47
|
+
doneSubtleFg: "light-dark(#6d28d9, #c4b5fd)",
|
|
48
|
+
focus: "light-dark(#2563eb, #60a5fa)",
|
|
49
|
+
},
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or take the `default` brand's values for any of them: `done: defaultBrand.color.done`, and so on for each new role.
|
|
53
|
+
|
|
54
|
+
A hand-drawn subtle fill takes a variant:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// before
|
|
58
|
+
<Box backgroundColor="bgMuted" color="fgMuted" borderRadius="sm">Merged</Box>
|
|
59
|
+
<Tag style={{ background: app.color.positiveSubtle }}>Docs</Tag>
|
|
60
|
+
// after
|
|
61
|
+
<Badge subtle variant="done">Merged</Badge>
|
|
62
|
+
<Tag variant="success">Docs</Tag>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A neutral Tag that must keep `fg` text sets it: `<Tag color="fg">`.
|
|
66
|
+
|
|
67
|
+
## Verify
|
|
68
|
+
|
|
69
|
+
Run the app's typecheck (`pnpm typecheck`); it passes with no error naming a `*Subtle` role, `done` or `doneFg`. Then load a page that renders the theme in development; `defineTheme` throws no contrast error, and a `<Badge subtle variant="done">` shows violet text on a pale violet fill in light mode and light violet on deep violet in dark mode.
|
package/dist/docs/theming.md
CHANGED
|
@@ -30,6 +30,8 @@ const inbox = defineTheme({
|
|
|
30
30
|
...defaultBrand.color,
|
|
31
31
|
accent: "light-dark(#b00020, #ff8a80)",
|
|
32
32
|
accentFill: "light-dark(#b00020, #8e001a)",
|
|
33
|
+
accentSubtle: "light-dark(#fde2e4, #3b0008)",
|
|
34
|
+
accentSubtleFg: "light-dark(#b00020, #ff8a80)",
|
|
33
35
|
},
|
|
34
36
|
});
|
|
35
37
|
|
|
@@ -38,7 +40,9 @@ const inbox = defineTheme({
|
|
|
38
40
|
</ThemeProvider>;
|
|
39
41
|
```
|
|
40
42
|
|
|
41
|
-
`accent` colors links, tabs, checked boxes and pressed chips, and is also text on a surface; `accentFill` is the primary Button's solid fill, with `accentFillFg` text on it in both modes. Recolor both together.
|
|
43
|
+
`accent` colors links, tabs, checked boxes and pressed chips, and is also text on a surface; `accentFill` is the primary Button's solid fill, with `accentFillFg` text on it in both modes. Recolor both together, and the accent's subtle pair so an accent Tag or subtle Badge turns red too.
|
|
44
|
+
|
|
45
|
+
Each tone (`neutral`, `accent`, `success`, `warning`, `danger`, `done`) also has a subtle pair, `<tone>Subtle` with `<tone>SubtleFg` text, for Badge's `subtle` variants and Tag's fills. Recolor a tone's subtle pair with it: the text needs 4.5:1 on its fill and 3:1 on `bg`, and `focus` 3:1 on the fill.
|
|
42
46
|
|
|
43
47
|
`defineTheme(brand)` takes a full `Brand`: every color role as `light-dark(<light>, <dark>)` and every font role. A missing role is a type error. It returns CSS that `ThemeProvider` renders as `<style href precedence="ui-theme">`; React hoists it into `<head>` and renders it once per name.
|
|
44
48
|
|