@jonmilner/ui 0.6.1 → 0.8.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/dist/Badge/Badge.css.mjs +8 -3
- package/dist/Badge/index.d.mts +4 -2
- package/dist/Badge/index.mjs +27 -21
- package/dist/Button/Button.css +1 -1
- 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 +37 -9
- package/dist/brands/forced-colors.mjs +16 -0
- package/dist/contrast/contrast.mjs +36 -29
- package/dist/contrast/mixes.mjs +20 -11
- package/dist/docs/Badge.md +13 -4
- package/dist/docs/Button.md +1 -1
- package/dist/docs/Icon.md +1 -1
- package/dist/docs/Tag.md +12 -3
- package/dist/docs/llms.txt +1 -1
- package/dist/docs/manifest.json +77 -8
- package/dist/docs/migrations/0.7.0.md +53 -0
- package/dist/docs/migrations/0.8.0.md +69 -0
- package/dist/docs/theming.md +12 -2
- package/dist/styles/atoms.css +1 -1
- package/dist/styles/atoms.css.d.mts +2 -2
- package/dist/styles/atoms.css.mjs +78 -53
- package/dist/styles/button-fills.mjs +2 -2
- 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 -19
- package/package.json +1 -1
|
@@ -1,56 +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
|
-
|
|
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);
|
|
28
35
|
}
|
|
29
|
-
function
|
|
30
|
-
return
|
|
36
|
+
function l(e, t, r = "light") {
|
|
37
|
+
return u(n(e, r), n(t, r), t);
|
|
31
38
|
}
|
|
32
|
-
function
|
|
39
|
+
function u(e, t, n) {
|
|
33
40
|
if (t.alpha < 1) throw Error(`Contrast background must be opaque: ${n}`);
|
|
34
|
-
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({
|
|
35
42
|
r: r(e.r, t.r),
|
|
36
43
|
g: r(e.g, t.g),
|
|
37
44
|
b: r(e.b, t.b),
|
|
38
45
|
alpha: 1
|
|
39
|
-
}),
|
|
46
|
+
}), c(t)].toSorted((e, t) => t - e);
|
|
40
47
|
return (i + .05) / (a + .05);
|
|
41
48
|
}
|
|
42
|
-
function
|
|
43
|
-
return
|
|
44
|
-
let r = /* @__PURE__ */ new Map(),
|
|
45
|
-
return
|
|
46
|
-
let r =
|
|
47
|
-
return r <
|
|
48
|
-
...
|
|
49
|
-
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,
|
|
50
57
|
ratio: r
|
|
51
58
|
}] : [];
|
|
52
59
|
});
|
|
53
60
|
});
|
|
54
61
|
}
|
|
55
62
|
//#endregion
|
|
56
|
-
export {
|
|
63
|
+
export { d as checkContrast, o as contrastPairs, l as contrastRatio };
|
package/dist/contrast/mixes.mjs
CHANGED
|
@@ -3,17 +3,26 @@ import { contrastRatio as t } from "./contrast.mjs";
|
|
|
3
3
|
import { highlightFill as n, highlightText as r } from "../styles/highlight.mjs";
|
|
4
4
|
import { hoverFill as i } from "../styles/hover-tint.mjs";
|
|
5
5
|
//#region src/contrast/mixes.ts
|
|
6
|
-
var a = [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
var a = [
|
|
7
|
+
{
|
|
8
|
+
name: "Menu highlight",
|
|
9
|
+
fill: n,
|
|
10
|
+
text: r,
|
|
11
|
+
min: 4.5
|
|
12
|
+
},
|
|
13
|
+
...Object.entries(e).map(([e, { backgroundColor: t, color: n }]) => ({
|
|
14
|
+
name: `${e} Button hover`,
|
|
15
|
+
fill: (e) => i(e.fg, e[t]),
|
|
16
|
+
text: [n],
|
|
17
|
+
min: 4.5
|
|
18
|
+
})),
|
|
19
|
+
{
|
|
20
|
+
name: "pressed Chip hover",
|
|
21
|
+
fill: (e) => i(e.fg, e.accent),
|
|
22
|
+
text: ["accentFg"],
|
|
23
|
+
min: 4.5
|
|
24
|
+
}
|
|
25
|
+
];
|
|
17
26
|
function o(e) {
|
|
18
27
|
return ["light", "dark"].flatMap((n) => a.flatMap(({ name: r, fill: i, text: a, min: o }) => {
|
|
19
28
|
let s = i(e.color);
|
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/Button.md
CHANGED
|
@@ -18,7 +18,7 @@ The labelled action: a `<button type="button">` with the house look, or a link s
|
|
|
18
18
|
|
|
19
19
|
## Rules
|
|
20
20
|
|
|
21
|
-
- `primary` marks the one action a view or dialog exists for; everything else is `secondary`, the default. One primary per group.
|
|
21
|
+
- `primary` marks the one action a view or dialog exists for; everything else is `secondary`, the default. One primary per group. Its fill is `accentFill` with `accentFillFg` text (ADR 0119): light text on a solid accent in both modes, 4.5:1 at rest and on hover.
|
|
22
22
|
- `danger` (`dangerFg` on `danger`, ADR 0060) replaces `primary` when that action destroys data or cannot be undone, such as a confirm dialog's "Delete project". It takes `primary`'s place, so a group has one `primary` or one `danger`, not both. The label names the consequence ("Delete project", not "OK"): under forced colors `danger` looks like `primary`, so the word carries the meaning, not the color. Confirm before an irreversible action runs; `danger` does not.
|
|
23
23
|
- Sizes are `sm`, `med` (default) and `lg`, never `md`. The label and the icons take the `fontSize` step of the same name.
|
|
24
24
|
- `loading` renders `aria-busy="true"`, blocks clicks, Enter and Space, and swaps the label for three pulsing dots in the same space: the width never changes, and the label stays the accessible name. A label narrower than the dots (one character, such as a page number) widens by a few pixels; give such buttons a fixed `minInlineSize`. Announce the outcome elsewhere (a toast or a status message); `aria-busy` alone is not announced.
|
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" \| "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/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.0",
|
|
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,11 +1067,13 @@
|
|
|
1061
1067
|
"props": [
|
|
1062
1068
|
{
|
|
1063
1069
|
"name": "color",
|
|
1064
|
-
"type": "\"accent\" | \"accentFg\" | \"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",
|
|
1075
|
+
"accentFillFg",
|
|
1076
|
+
"accentSubtleFg",
|
|
1069
1077
|
"bg",
|
|
1070
1078
|
"bgMuted",
|
|
1071
1079
|
"bgRaised",
|
|
@@ -1073,13 +1081,20 @@
|
|
|
1073
1081
|
"borderStrong",
|
|
1074
1082
|
"danger",
|
|
1075
1083
|
"dangerFg",
|
|
1084
|
+
"dangerSubtleFg",
|
|
1085
|
+
"done",
|
|
1086
|
+
"doneFg",
|
|
1087
|
+
"doneSubtleFg",
|
|
1076
1088
|
"fg",
|
|
1077
1089
|
"fgMuted",
|
|
1078
1090
|
"focus",
|
|
1091
|
+
"neutralSubtleFg",
|
|
1079
1092
|
"success",
|
|
1080
1093
|
"successFg",
|
|
1094
|
+
"successSubtleFg",
|
|
1081
1095
|
"warning",
|
|
1082
|
-
"warningFg"
|
|
1096
|
+
"warningFg",
|
|
1097
|
+
"warningSubtleFg"
|
|
1083
1098
|
]
|
|
1084
1099
|
},
|
|
1085
1100
|
{
|
|
@@ -1925,6 +1940,19 @@
|
|
|
1925
1940
|
"med",
|
|
1926
1941
|
"sm"
|
|
1927
1942
|
]
|
|
1943
|
+
},
|
|
1944
|
+
{
|
|
1945
|
+
"name": "variant",
|
|
1946
|
+
"type": "\"accent\" | \"danger\" | \"done\" | \"neutral\" | \"success\" | \"warning\"",
|
|
1947
|
+
"required": false,
|
|
1948
|
+
"values": [
|
|
1949
|
+
"accent",
|
|
1950
|
+
"danger",
|
|
1951
|
+
"done",
|
|
1952
|
+
"neutral",
|
|
1953
|
+
"success",
|
|
1954
|
+
"warning"
|
|
1955
|
+
]
|
|
1928
1956
|
}
|
|
1929
1957
|
],
|
|
1930
1958
|
"parts": [],
|
|
@@ -2195,11 +2223,15 @@
|
|
|
2195
2223
|
},
|
|
2196
2224
|
{
|
|
2197
2225
|
"name": "backgroundColor",
|
|
2198
|
-
"type": "\"accent\" | \"accentFg\" | \"bg\" | \"bgMuted\" | \"bgRaised\" | \"border\" | \"borderStrong\" | \"danger\" | \"dangerFg\" | \"fg\" | \"fgMuted\" | \"focus\" | \"success\" | \"successFg\" | \"warning\" | \"warningFg\"",
|
|
2226
|
+
"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\"",
|
|
2199
2227
|
"required": false,
|
|
2200
2228
|
"values": [
|
|
2201
2229
|
"accent",
|
|
2202
2230
|
"accentFg",
|
|
2231
|
+
"accentFill",
|
|
2232
|
+
"accentFillFg",
|
|
2233
|
+
"accentSubtle",
|
|
2234
|
+
"accentSubtleFg",
|
|
2203
2235
|
"bg",
|
|
2204
2236
|
"bgMuted",
|
|
2205
2237
|
"bgRaised",
|
|
@@ -2207,13 +2239,25 @@
|
|
|
2207
2239
|
"borderStrong",
|
|
2208
2240
|
"danger",
|
|
2209
2241
|
"dangerFg",
|
|
2242
|
+
"dangerSubtle",
|
|
2243
|
+
"dangerSubtleFg",
|
|
2244
|
+
"done",
|
|
2245
|
+
"doneFg",
|
|
2246
|
+
"doneSubtle",
|
|
2247
|
+
"doneSubtleFg",
|
|
2210
2248
|
"fg",
|
|
2211
2249
|
"fgMuted",
|
|
2212
2250
|
"focus",
|
|
2251
|
+
"neutralSubtle",
|
|
2252
|
+
"neutralSubtleFg",
|
|
2213
2253
|
"success",
|
|
2214
2254
|
"successFg",
|
|
2255
|
+
"successSubtle",
|
|
2256
|
+
"successSubtleFg",
|
|
2215
2257
|
"warning",
|
|
2216
|
-
"warningFg"
|
|
2258
|
+
"warningFg",
|
|
2259
|
+
"warningSubtle",
|
|
2260
|
+
"warningSubtleFg"
|
|
2217
2261
|
]
|
|
2218
2262
|
},
|
|
2219
2263
|
{
|
|
@@ -2259,11 +2303,13 @@
|
|
|
2259
2303
|
},
|
|
2260
2304
|
{
|
|
2261
2305
|
"name": "color",
|
|
2262
|
-
"type": "\"accent\" | \"accentFg\" | \"bg\" | \"bgMuted\" | \"bgRaised\" | \"border\" | \"borderStrong\" | \"danger\" | \"dangerFg\" | \"fg\" | \"fgMuted\" | \"focus\" | \"success\" | \"successFg\" | \"warning\" | \"warningFg\"",
|
|
2306
|
+
"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\"",
|
|
2263
2307
|
"required": false,
|
|
2264
2308
|
"values": [
|
|
2265
2309
|
"accent",
|
|
2266
2310
|
"accentFg",
|
|
2311
|
+
"accentFillFg",
|
|
2312
|
+
"accentSubtleFg",
|
|
2267
2313
|
"bg",
|
|
2268
2314
|
"bgMuted",
|
|
2269
2315
|
"bgRaised",
|
|
@@ -2271,13 +2317,20 @@
|
|
|
2271
2317
|
"borderStrong",
|
|
2272
2318
|
"danger",
|
|
2273
2319
|
"dangerFg",
|
|
2320
|
+
"dangerSubtleFg",
|
|
2321
|
+
"done",
|
|
2322
|
+
"doneFg",
|
|
2323
|
+
"doneSubtleFg",
|
|
2274
2324
|
"fg",
|
|
2275
2325
|
"fgMuted",
|
|
2276
2326
|
"focus",
|
|
2327
|
+
"neutralSubtleFg",
|
|
2277
2328
|
"success",
|
|
2278
2329
|
"successFg",
|
|
2330
|
+
"successSubtleFg",
|
|
2279
2331
|
"warning",
|
|
2280
|
-
"warningFg"
|
|
2332
|
+
"warningFg",
|
|
2333
|
+
"warningSubtleFg"
|
|
2281
2334
|
]
|
|
2282
2335
|
},
|
|
2283
2336
|
{
|
|
@@ -2977,12 +3030,28 @@
|
|
|
2977
3030
|
"color.borderStrong": "var(--color-border-strong)",
|
|
2978
3031
|
"color.accent": "var(--color-accent)",
|
|
2979
3032
|
"color.accentFg": "var(--color-accent-fg)",
|
|
3033
|
+
"color.accentFill": "var(--color-accent-fill)",
|
|
3034
|
+
"color.accentFillFg": "var(--color-accent-fill-fg)",
|
|
2980
3035
|
"color.danger": "var(--color-danger)",
|
|
2981
3036
|
"color.dangerFg": "var(--color-danger-fg)",
|
|
2982
3037
|
"color.success": "var(--color-success)",
|
|
2983
3038
|
"color.successFg": "var(--color-success-fg)",
|
|
2984
3039
|
"color.warning": "var(--color-warning)",
|
|
2985
3040
|
"color.warningFg": "var(--color-warning-fg)",
|
|
3041
|
+
"color.done": "var(--color-done)",
|
|
3042
|
+
"color.doneFg": "var(--color-done-fg)",
|
|
3043
|
+
"color.neutralSubtle": "var(--color-neutral-subtle)",
|
|
3044
|
+
"color.neutralSubtleFg": "var(--color-neutral-subtle-fg)",
|
|
3045
|
+
"color.accentSubtle": "var(--color-accent-subtle)",
|
|
3046
|
+
"color.accentSubtleFg": "var(--color-accent-subtle-fg)",
|
|
3047
|
+
"color.successSubtle": "var(--color-success-subtle)",
|
|
3048
|
+
"color.successSubtleFg": "var(--color-success-subtle-fg)",
|
|
3049
|
+
"color.warningSubtle": "var(--color-warning-subtle)",
|
|
3050
|
+
"color.warningSubtleFg": "var(--color-warning-subtle-fg)",
|
|
3051
|
+
"color.dangerSubtle": "var(--color-danger-subtle)",
|
|
3052
|
+
"color.dangerSubtleFg": "var(--color-danger-subtle-fg)",
|
|
3053
|
+
"color.doneSubtle": "var(--color-done-subtle)",
|
|
3054
|
+
"color.doneSubtleFg": "var(--color-done-subtle-fg)",
|
|
2986
3055
|
"color.focus": "var(--color-focus)",
|
|
2987
3056
|
"color.shadow": "var(--color-shadow)",
|
|
2988
3057
|
"font.body": "var(--font-body)",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# 0.7.0
|
|
2
|
+
|
|
3
|
+
`Brand["color"]` gains two required roles, `accentFill` and `accentFillFg`, and the primary Button and IconButton fill with them instead of `accent` and `accentFg`, so the primary action carries white text in both modes (decision 0119). A brand that lists its colors without spreading a preset no longer type-checks, and one that recolors `accent` alone keeps the preset's blue primary Button.
|
|
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 `accentFill` and `accentFillFg`.
|
|
10
|
+
- It spreads a preset's colors (`...defaultBrand.color`) and sets `accent`: it type-checks, but its primary Button stays the preset's blue.
|
|
11
|
+
- It spreads a preset's colors and leaves `accent` alone: no change.
|
|
12
|
+
|
|
13
|
+
In `*.tsx`, a primary-looking fill drawn by hand from the accent pair, regex `backgroundColor=["{]["']?accent["'}]`, and in `*.css.ts`, `vars\.color\.accent\b` used as a `backgroundColor` beside `vars.color.accentFg`.
|
|
14
|
+
|
|
15
|
+
## Replace
|
|
16
|
+
|
|
17
|
+
Add the two roles after `accentFg`, each a `light-dark(<light>, <dark>)` pair. `accentFillFg` must pass 4.5:1 on `accentFill` in both modes, at rest and under the hover tint (`fg` at 15% over the fill); `defineTheme` throws in development if not. Under white text, keep the dark step a shade darker than the light one, since dark mode's hover tint lightens the fill.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// before
|
|
21
|
+
color: {
|
|
22
|
+
// …every other role
|
|
23
|
+
accent: "light-dark(#1d4ed8, #93c5fd)",
|
|
24
|
+
accentFg: "light-dark(#fff, #111)",
|
|
25
|
+
danger: "light-dark(#c62828, #ef9a9a)",
|
|
26
|
+
},
|
|
27
|
+
// after
|
|
28
|
+
color: {
|
|
29
|
+
// …every other role
|
|
30
|
+
accent: "light-dark(#1d4ed8, #93c5fd)",
|
|
31
|
+
accentFg: "light-dark(#fff, #111)",
|
|
32
|
+
accentFill: "light-dark(#1d4ed8, #1e40af)",
|
|
33
|
+
accentFillFg: "light-dark(#fff, #fff)",
|
|
34
|
+
danger: "light-dark(#c62828, #ef9a9a)",
|
|
35
|
+
},
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or take the `default` brand's values: `accentFill: defaultBrand.color.accentFill` and `accentFillFg: defaultBrand.color.accentFillFg` (`light-dark(oklch(0.546 0.19 258), oklch(0.488 0.18 258))` and `light-dark(oklch(1 0 0), oklch(1 0 0))`).
|
|
39
|
+
|
|
40
|
+
A hand-drawn primary fill takes the new pair:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// before
|
|
44
|
+
<Box backgroundColor="accent" color="accentFg" />
|
|
45
|
+
// after
|
|
46
|
+
<Box backgroundColor="accentFill" color="accentFillFg" />
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Leave other `accent` uses alone: links, tabs, checked boxes, pressed chips and Badge's `accent` tone keep `accent` and `accentFg`.
|
|
50
|
+
|
|
51
|
+
## Verify
|
|
52
|
+
|
|
53
|
+
Run the app's typecheck (`pnpm typecheck`); it passes with no error naming `accentFill` or `accentFillFg`. Then load a page that renders the theme in development; `defineTheme` throws no contrast error, and a primary Button in dark mode shows white text.
|
|
@@ -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
|
@@ -26,7 +26,13 @@ import { defaultBrand } from "@jonmilner/ui/brands";
|
|
|
26
26
|
const inbox = defineTheme({
|
|
27
27
|
...defaultBrand,
|
|
28
28
|
name: "inbox",
|
|
29
|
-
color: {
|
|
29
|
+
color: {
|
|
30
|
+
...defaultBrand.color,
|
|
31
|
+
accent: "light-dark(#b00020, #ff8a80)",
|
|
32
|
+
accentFill: "light-dark(#b00020, #8e001a)",
|
|
33
|
+
accentSubtle: "light-dark(#fde2e4, #3b0008)",
|
|
34
|
+
accentSubtleFg: "light-dark(#b00020, #ff8a80)",
|
|
35
|
+
},
|
|
30
36
|
});
|
|
31
37
|
|
|
32
38
|
<ThemeProvider theme={inbox} mode="system">
|
|
@@ -34,10 +40,14 @@ const inbox = defineTheme({
|
|
|
34
40
|
</ThemeProvider>;
|
|
35
41
|
```
|
|
36
42
|
|
|
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.
|
|
46
|
+
|
|
37
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.
|
|
38
48
|
|
|
39
49
|
- **Give a custom brand its own name.** A theme applies page-wide, not only inside its provider: a theme named `default` recolors every `default` provider on the page. The name must be a CSS identifier.
|
|
40
|
-
- **Contrast is checked in development.** Outside production, `defineTheme` throws when a text, fill or focus pair fails WCAG AA in either mode, and lists the pairs. It also checks text on component fills mixed from your roles, such as the dark-mode menu highlight (`fg` 10% into `bgRaised`), where `fg`, `fgMuted` and `danger` need 4.5:1 too, and Button's hover (`fg` 15% over each variant's fill), where `fg`, `
|
|
50
|
+
- **Contrast is checked in development.** Outside production, `defineTheme` throws when a text, fill or focus pair fails WCAG AA in either mode, and lists the pairs. It also checks text on component fills mixed from your roles, such as the dark-mode menu highlight (`fg` 10% into `bgRaised`), where `fg`, `fgMuted` and `danger` need 4.5:1 too, and Button's hover (`fg` 15% over each variant's fill), where `fg`, `accentFillFg` and `dangerFg` need 4.5:1 on `bg`, `accentFill` and `danger` tinted, and a pressed Chip's, where `accentFg` does on `accent`. The roles a fill mixes must be opaque. Colors must be literal: hex, `rgb()`, `oklab()`, `oklch()`, `color-mix()` or `light-dark()`. Named colors other than `white`, `black` and `transparent` are rejected, as is a translucent background. `var()` may only reference another color role (`var(--color-accent)`).
|
|
41
51
|
- **Override theme tokens in unlayered CSS or in the theme itself.** The theme is unlayered, so a token set in `@layer app` loses to it. Preset tokens live in `@layer tokens` and can be overridden from `@layer app`.
|
|
42
52
|
- **Fallback faces are global.** A brand font's fallback faces are named for the web font and the local font, `"<Family> Fallback <Local>"` (`"Inter Fallback Arial"`). The theme declares only faces `base.css` lacks. A font reusing a family name must agree with every face it shares, weight and style included: a fallback listing fewer variants (such as capsize's per-font `arial` import) reuses the others' faces, but one that sizes a shared face differently, or adds a weight or style to a preset's family, throws.
|
|
43
53
|
- **Values must be plain.** Color values and font names may not contain CSS syntax (`;`, braces, `<`, `>`, `\`, `!`, `/*`) or, for font names, quotes; `defineTheme` throws in every mode.
|