@cueplusplus/theme-base 1.0.2
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/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/base.css +201 -0
- package/dist/base.json +58 -0
- package/dist/contrast.d.mts +123 -0
- package/dist/contrast.mjs +226 -0
- package/dist/index.d.mts +182 -0
- package/dist/index.mjs +393 -0
- package/dist/manifest.schema.json +741 -0
- package/package.json +64 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @cueplusplus/theme-base
|
|
2
|
+
|
|
3
|
+
## 1.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b1c2f25: **Every package now ships the licence it declares.** All sixteen have said `"license": "MIT"` in
|
|
8
|
+
their manifest since they were first published, and none of them carried the text. Installing one
|
|
9
|
+
gave you a licence field and nothing to read; anyone auditing their dependency tree found a package
|
|
10
|
+
that claimed a licence it did not include. Each package root now holds the MIT notice, and npm packs
|
|
11
|
+
a root `LICENSE` whether or not `files` lists it, so it is in the tarball you install.
|
|
12
|
+
|
|
13
|
+
`@cueplusplus/ui` additionally ships `NOTICE.md`, the third-party notice for the assistant-ui code
|
|
14
|
+
vendored into its elements. That one is not automatic — `files` had to name it — and it is byte-for-byte
|
|
15
|
+
the notice that already sat beside the source it covers, because a notice that travels without the
|
|
16
|
+
bytes it describes is not a notice.
|
|
17
|
+
|
|
18
|
+
Nothing else about these packages changed: same exports, same versions of every dependency, same
|
|
19
|
+
registry. If you are upgrading only for this, the diff you receive is licence text.
|
|
20
|
+
|
|
21
|
+
## 1.0.1
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 64b4bab: Every package manifest's `repository` field now names `cueplusplus/cue-ui`, following the
|
|
26
|
+
repository's rename from an earlier working title.
|
|
27
|
+
|
|
28
|
+
**Nothing to do.** No code, no exports, no types, and no package name: the scope is the
|
|
29
|
+
organisation, not the repository, so every specifier you already import is the one you keep
|
|
30
|
+
importing. What moves is metadata — GitHub Packages matches a package to a repository through that
|
|
31
|
+
field, so from this release the "source" link on each package's registry page, and the repository
|
|
32
|
+
the next publish is attributed to, name `cue-ui`. The `directory` beside it is unchanged, so each
|
|
33
|
+
link still lands on that package rather than on the monorepo root.
|
|
34
|
+
|
|
35
|
+
If you have a bookmark or a script pointing at the old URL, GitHub's rename redirect answers it;
|
|
36
|
+
updating it to `https://github.com/cueplusplus/cue-ui` is tidiness rather than repair.
|
|
37
|
+
|
|
38
|
+
- 362cdda: Each of these packages ships its `README.md` inside its tarball, and that file is the page a
|
|
39
|
+
consumer lands on at the registry; until now the ten presets were a dozen lines with nothing to
|
|
40
|
+
copy. The README now carries the install lines and a quick start — the `.npmrc` scope routing and
|
|
41
|
+
the user-level credential GitHub Packages needs, the `pnpm add` line with the peers the package
|
|
42
|
+
declares, the stylesheet import in the order the cascade requires, and the `ThemeProvider` call
|
|
43
|
+
that registers the manifest — plus what each subpath is, the modes and font stack the theme
|
|
44
|
+
actually defines, and where the theming guide and `docs/CONSUMING.md` are. No code, no exports, no
|
|
45
|
+
types.
|
|
46
|
+
|
|
47
|
+
`@cueplusplus/theme-base`'s quick start says that its second import is a palette the base does not
|
|
48
|
+
ship, and names the install line for one.
|
|
49
|
+
|
|
50
|
+
## 1.0.0
|
|
51
|
+
|
|
52
|
+
### Major Changes
|
|
53
|
+
|
|
54
|
+
- 814c859: `@cueplusplus/theme-base` is the blank abstract theme every CUE++ theme extends: the neutral base palette and `base.css`, the colour, geometry and font contracts, the manifest schema, the contrast report, and the resolver that turns a theme's deltas into a complete token map. It peers on `@cueplusplus/tokens` at `>=0.9.0 <1` — the axes and the mono handshake come from there, colour does not — and this is its first published version, `1.0.0`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CUE++ contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# @cueplusplus/theme-base
|
|
2
|
+
|
|
3
|
+
The blank abstract theme every CUE++ theme extends, and the contract that makes "a palette is a
|
|
4
|
+
package" work — addressed to the two people who need it by name: whoever writes a theme package by
|
|
5
|
+
hand, and whoever writes a tool that reads one.
|
|
6
|
+
|
|
7
|
+
**Most apps never reach for it by name.** `@cueplusplus/ui` re-exports the types,
|
|
8
|
+
`@cueplusplus/ui/styles.css` already imports `base.css`, and every `@cueplusplus/theme-*` preset
|
|
9
|
+
declares this package as a peer — so it is usually in the tree before you think about it.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
The `@cueplusplus` scope is served by **GitHub Packages**, which authenticates every read — a public
|
|
14
|
+
package included. Two lines set that up, and they live in two different files on purpose: the scope
|
|
15
|
+
routing is committed with your project, the credential never is.
|
|
16
|
+
|
|
17
|
+
```ini
|
|
18
|
+
# .npmrc, committed at the root of your project
|
|
19
|
+
@cueplusplus:registry=https://npm.pkg.github.com
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
# the credential, once per machine, at the user level
|
|
24
|
+
pnpm config set //npm.pkg.github.com/:_authToken ghp_…
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The token has to be a **classic** personal access token with the `read:packages` scope — GitHub
|
|
28
|
+
Packages accepts neither a fine-grained PAT nor npm's OIDC flow — and it has to be at the user
|
|
29
|
+
level: pnpm 11 ignores an env-expanded `_authToken` in a project `.npmrc`, and the install fails
|
|
30
|
+
with a `401` and a warning that the setting was ignored. [`docs/CONSUMING.md` §1][consuming] has the
|
|
31
|
+
same credential for CI and for Vercel.
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pnpm add @cueplusplus/theme-base @cueplusplus/tokens
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`@cueplusplus/tokens` is the one peer this package declares (`>=0.9.0 <1`): the density and type
|
|
38
|
+
axes this package does not own. It is a peer rather than a dependency because a tree with two copies
|
|
39
|
+
of the token layer in it has two token vocabularies, and only one of them is the one the stylesheets
|
|
40
|
+
on the page were built against. `culori` comes along as a dependency; nothing else does.
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
Four things live here, and every one of them is a fact more than one package has to agree on.
|
|
45
|
+
|
|
46
|
+
### The blank base
|
|
47
|
+
|
|
48
|
+
`base.css` is every `--cue-*` colour at a neutral default, declared on a bare `:root`, with
|
|
49
|
+
`@cueplusplus/tokens/axes.css` imported for the density and type axes. It is what
|
|
50
|
+
`@cueplusplus/ui/styles.css` brings in, so a page that registers no theme still paints something
|
|
51
|
+
legible: greys, one desaturated accent, and the platform monospace.
|
|
52
|
+
|
|
53
|
+
Import it yourself only when you are assembling the stylesheet without `@cueplusplus/ui/styles.css`
|
|
54
|
+
— a theme configurator, a token playground, an email template that wants the vocabulary and not the
|
|
55
|
+
components:
|
|
56
|
+
|
|
57
|
+
The second line below is a palette, and this package ships none: install one preset beside the
|
|
58
|
+
base (`pnpm add @cueplusplus/theme-cue`, or any other `@cueplusplus/theme-*`) before that import
|
|
59
|
+
resolves.
|
|
60
|
+
|
|
61
|
+
```css
|
|
62
|
+
@import "@cueplusplus/theme-base/base.css";
|
|
63
|
+
@import "@cueplusplus/theme-cue/theme.css"; /* any installed palette, and it must come after */
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The order is not decorative. A theme declares its colours under `[data-theme="cue"]`, which ties
|
|
67
|
+
with this bare `:root` at the same specificity, so the one declared **later** wins.
|
|
68
|
+
|
|
69
|
+
### The contract a theme package satisfies
|
|
70
|
+
|
|
71
|
+
A theme package is a manifest plus the stylesheet that paints it. The manifest has eleven required
|
|
72
|
+
fields — `schemaVersion`, `name`, `package`, `extends`, `mode`, `supportsLight`, `colors`, `fonts`,
|
|
73
|
+
`densities`, `fontPairings`, `contrast` — and `assertManifest` is what says so at runtime;
|
|
74
|
+
`manifest.schema.json` is the same statement for a validator that is not JavaScript. `mode` is
|
|
75
|
+
`complete` when `theme.css` stands on its own and `delta` when it holds only what differs from the
|
|
76
|
+
base. `extends` carries the `theme-base` range the manifest was resolved against, and it is the
|
|
77
|
+
package's own `peerDependencies` entry rather than a second copy of it.
|
|
78
|
+
|
|
79
|
+
The typed half is `ThemeRegistry`, which a theme package augments with `declare module` in its
|
|
80
|
+
`index.d.ts`. `ThemeName` is the keys of that interface, so it widens with each theme a consumer
|
|
81
|
+
installs rather than naming a list compiled in here.
|
|
82
|
+
|
|
83
|
+
### Reading a theme
|
|
84
|
+
|
|
85
|
+
`resolve()` turns a theme's deltas into a complete token map, and is what a tool that has to know a
|
|
86
|
+
theme's *effective* colours calls instead of parsing CSS:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { assertManifest, resolve } from "@cueplusplus/theme-base";
|
|
90
|
+
|
|
91
|
+
export function darkColors(manifest: unknown) {
|
|
92
|
+
assertManifest(manifest); // throws, listing every problem, if it is not one
|
|
93
|
+
return resolve(manifest, { mode: "dark" }).colors;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`assertManifest` is an assertion function rather than a returning one — it narrows `manifest` in
|
|
98
|
+
place and gives back `void`, so it is a statement of its own and not an argument to `resolve`.
|
|
99
|
+
`validateManifest` is the same check as a list of problems, for a caller that wants to report them
|
|
100
|
+
all rather than throw on the first.
|
|
101
|
+
|
|
102
|
+
`resolve(null, …)` answers the same question about the blank base itself. `resolveDensities()` and
|
|
103
|
+
`resolveFonts()` are the two axes on their own.
|
|
104
|
+
|
|
105
|
+
### The contrast report
|
|
106
|
+
|
|
107
|
+
`@cueplusplus/theme-base/contrast` is the WCAG measurement `cue-theme build` refuses a theme on —
|
|
108
|
+
`contrastReport()` over `CONTRAST_REQUIREMENTS`, with `contrastRatio()` and `relativeLuminance()`
|
|
109
|
+
underneath and `WCAG_AA_NON_TEXT` (3), `WCAG_AA_TEXT` (4.5) and `WCAG_AAA_TEXT` (7) as the floors.
|
|
110
|
+
|
|
111
|
+
To build a theme rather than read one, use
|
|
112
|
+
[`@cueplusplus/theme-tools`](https://github.com/cueplusplus/cue-ui/tree/main/packages/theme-tools) —
|
|
113
|
+
`cue-theme init` scaffolds a package that peers on this one and builds before it is edited.
|
|
114
|
+
|
|
115
|
+
## What it ships
|
|
116
|
+
|
|
117
|
+
| Subpath | What it is |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| `@cueplusplus/theme-base` | the contract (`ThemeManifest`, `ThemeRegistry`, `ThemeName`, the colour and geometry token lists), `assertManifest` / `validateManifest`, and `resolve` / `resolveDensities` / `resolveFonts` |
|
|
120
|
+
| `@cueplusplus/theme-base/contrast` | `contrastReport`, `contrastRatio`, `relativeLuminance`, `CONTRAST_REQUIREMENTS` and the three WCAG floors |
|
|
121
|
+
| `@cueplusplus/theme-base/base.css` | the blank base, importing `@cueplusplus/tokens/axes.css` |
|
|
122
|
+
| `@cueplusplus/theme-base/manifest.schema.json` | the manifest contract as JSON Schema, for a validator that is not TypeScript |
|
|
123
|
+
| `@cueplusplus/theme-base/package.json` | |
|
|
124
|
+
|
|
125
|
+
ESM only, Node 22 or newer, types beside every entry.
|
|
126
|
+
|
|
127
|
+
## Where the rest is
|
|
128
|
+
|
|
129
|
+
- **The theming guide** — <https://ui.cueplusplus.com/docs/theming>. The token vocabulary, the
|
|
130
|
+
density and mode axes, the ten shipped presets, and `createTheme()` for a palette of your own.
|
|
131
|
+
- **[`docs/CONSUMING.md`][consuming]** — registry access in full, the peer matrix per subpath, and
|
|
132
|
+
the table of what a failed install means.
|
|
133
|
+
- **`CHANGELOG.md`**, in this package — one entry per release, addressed to you rather than to the
|
|
134
|
+
diff, with a **Migrating** section on anything that needs an edit. Read it before an upgrade; the
|
|
135
|
+
version number alone does not say what moved.
|
|
136
|
+
|
|
137
|
+
[consuming]: https://github.com/cueplusplus/cue-ui/blob/main/docs/CONSUMING.md
|
package/dist/base.css
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
@import "@cueplusplus/tokens/axes.css";
|
|
2
|
+
|
|
3
|
+
/*! @cueplusplus/theme-base — the blank theme: every token at a neutral default
|
|
4
|
+
* Generated by build.mjs from src/base.*.tokens.json. Do not edit by hand.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
color-scheme: dark;
|
|
9
|
+
--cue-bg: #0a0a0a;
|
|
10
|
+
--cue-sunken: #000000;
|
|
11
|
+
--cue-surface-1: #121212;
|
|
12
|
+
--cue-surface-2: #1a1a1a;
|
|
13
|
+
--cue-surface-3: #242424;
|
|
14
|
+
--cue-fg: #f5f5f5;
|
|
15
|
+
--cue-fg-muted: rgba(255,255,255,0.55);
|
|
16
|
+
--cue-fg-subtle: rgba(255,255,255,0.35);
|
|
17
|
+
--cue-border: rgba(255,255,255,0.10);
|
|
18
|
+
--cue-border-strong: rgba(255,255,255,0.25);
|
|
19
|
+
--cue-border-overlay: rgba(255,255,255,0.25);
|
|
20
|
+
--cue-accent: #8a93a6;
|
|
21
|
+
--cue-accent-hover: #9aa3b6;
|
|
22
|
+
--cue-accent-fg: #0a0a0a;
|
|
23
|
+
--cue-ok: oklch(0.72 0.17 149);
|
|
24
|
+
--cue-busy: oklch(0.78 0.13 220);
|
|
25
|
+
--cue-warn: oklch(0.8 0.17 75);
|
|
26
|
+
--cue-warn-fg: #000000;
|
|
27
|
+
--cue-danger: #ff5f57;
|
|
28
|
+
--cue-danger-fg: #000000;
|
|
29
|
+
--cue-info: oklch(0.75 0.12 252);
|
|
30
|
+
--cue-stream: oklch(0.707 0.165 254.624);
|
|
31
|
+
--cue-selection: rgba(255,255,255,0.20);
|
|
32
|
+
--cue-focus: #8a93a6;
|
|
33
|
+
--cue-data-ground: #000000;
|
|
34
|
+
--cue-scrim: rgba(0,0,0,0.55);
|
|
35
|
+
|
|
36
|
+
/* derived — kept inside the block so color-mix() reads this theme's values */
|
|
37
|
+
--cue-accent-soft: color-mix(in oklch, var(--cue-accent) 14%, transparent);
|
|
38
|
+
--cue-ok-soft: color-mix(in oklch, var(--cue-ok) 12%, transparent);
|
|
39
|
+
--cue-ok-border: color-mix(in oklch, var(--cue-ok) 35%, transparent);
|
|
40
|
+
--cue-busy-soft: color-mix(in oklch, var(--cue-busy) 12%, transparent);
|
|
41
|
+
--cue-busy-border: color-mix(in oklch, var(--cue-busy) 35%, transparent);
|
|
42
|
+
--cue-warn-soft: color-mix(in oklch, var(--cue-warn) 12%, transparent);
|
|
43
|
+
--cue-warn-border: color-mix(in oklch, var(--cue-warn) 35%, transparent);
|
|
44
|
+
--cue-danger-soft: color-mix(in oklch, var(--cue-danger) 12%, transparent);
|
|
45
|
+
--cue-danger-border: color-mix(in oklch, var(--cue-danger) 35%, transparent);
|
|
46
|
+
--cue-info-soft: color-mix(in oklch, var(--cue-info) 12%, transparent);
|
|
47
|
+
--cue-info-border: color-mix(in oklch, var(--cue-info) 35%, transparent);
|
|
48
|
+
--cue-stream-soft: color-mix(in oklch, var(--cue-stream) 12%, transparent);
|
|
49
|
+
--cue-stream-border: color-mix(in oklch, var(--cue-stream) 35%, transparent);
|
|
50
|
+
--cue-hair: color-mix(in oklch, var(--cue-fg) 6%, transparent);
|
|
51
|
+
--cue-hair-strong: color-mix(in oklch, var(--cue-fg) 12%, transparent);
|
|
52
|
+
--cue-row-hover: color-mix(in oklch, var(--cue-fg) 3.5%, transparent);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
[data-mode="light"] {
|
|
56
|
+
color-scheme: light;
|
|
57
|
+
--cue-bg: #fcfcfc;
|
|
58
|
+
--cue-sunken: #f0f0f0;
|
|
59
|
+
--cue-surface-1: #ffffff;
|
|
60
|
+
--cue-surface-2: #f6f6f6;
|
|
61
|
+
--cue-surface-3: #ededed;
|
|
62
|
+
--cue-fg: #0a0a0a;
|
|
63
|
+
--cue-fg-muted: rgba(0,0,0,0.55);
|
|
64
|
+
--cue-fg-subtle: rgba(0,0,0,0.35);
|
|
65
|
+
--cue-border: rgba(0,0,0,0.10);
|
|
66
|
+
--cue-border-strong: rgba(0,0,0,0.25);
|
|
67
|
+
--cue-border-overlay: rgba(0,0,0,0.25);
|
|
68
|
+
--cue-accent: #3f4759;
|
|
69
|
+
--cue-accent-hover: #333a4a;
|
|
70
|
+
--cue-accent-fg: #ffffff;
|
|
71
|
+
--cue-ok: oklch(0.53 0.14 149);
|
|
72
|
+
--cue-busy: oklch(0.54 0.1 225);
|
|
73
|
+
--cue-warn: oklch(0.55 0.115 70);
|
|
74
|
+
--cue-warn-fg: #fcfcfc;
|
|
75
|
+
--cue-danger: #d03030;
|
|
76
|
+
--cue-danger-fg: #fcfcfc;
|
|
77
|
+
--cue-info: oklch(0.55 0.16 258);
|
|
78
|
+
--cue-stream: oklch(0.55 0.21 259.815);
|
|
79
|
+
--cue-selection: rgba(0,0,0,0.20);
|
|
80
|
+
--cue-focus: #3f4759;
|
|
81
|
+
--cue-data-ground: #000000;
|
|
82
|
+
--cue-scrim: rgba(0,0,0,0.55);
|
|
83
|
+
|
|
84
|
+
/* derived — kept inside the block so color-mix() reads this theme's values */
|
|
85
|
+
--cue-accent-soft: color-mix(in oklch, var(--cue-accent) 14%, transparent);
|
|
86
|
+
--cue-ok-soft: color-mix(in oklch, var(--cue-ok) 12%, transparent);
|
|
87
|
+
--cue-ok-border: color-mix(in oklch, var(--cue-ok) 35%, transparent);
|
|
88
|
+
--cue-busy-soft: color-mix(in oklch, var(--cue-busy) 12%, transparent);
|
|
89
|
+
--cue-busy-border: color-mix(in oklch, var(--cue-busy) 35%, transparent);
|
|
90
|
+
--cue-warn-soft: color-mix(in oklch, var(--cue-warn) 12%, transparent);
|
|
91
|
+
--cue-warn-border: color-mix(in oklch, var(--cue-warn) 35%, transparent);
|
|
92
|
+
--cue-danger-soft: color-mix(in oklch, var(--cue-danger) 12%, transparent);
|
|
93
|
+
--cue-danger-border: color-mix(in oklch, var(--cue-danger) 35%, transparent);
|
|
94
|
+
--cue-info-soft: color-mix(in oklch, var(--cue-info) 12%, transparent);
|
|
95
|
+
--cue-info-border: color-mix(in oklch, var(--cue-info) 35%, transparent);
|
|
96
|
+
--cue-stream-soft: color-mix(in oklch, var(--cue-stream) 12%, transparent);
|
|
97
|
+
--cue-stream-border: color-mix(in oklch, var(--cue-stream) 35%, transparent);
|
|
98
|
+
--cue-hair: color-mix(in oklch, var(--cue-fg) 6%, transparent);
|
|
99
|
+
--cue-hair-strong: color-mix(in oklch, var(--cue-fg) 12%, transparent);
|
|
100
|
+
--cue-row-hover: color-mix(in oklch, var(--cue-fg) 3.5%, transparent);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@media (prefers-color-scheme: dark) {
|
|
104
|
+
[data-mode="system"] {
|
|
105
|
+
color-scheme: light dark;
|
|
106
|
+
--cue-bg: #0a0a0a;
|
|
107
|
+
--cue-sunken: #000000;
|
|
108
|
+
--cue-surface-1: #121212;
|
|
109
|
+
--cue-surface-2: #1a1a1a;
|
|
110
|
+
--cue-surface-3: #242424;
|
|
111
|
+
--cue-fg: #f5f5f5;
|
|
112
|
+
--cue-fg-muted: rgba(255,255,255,0.55);
|
|
113
|
+
--cue-fg-subtle: rgba(255,255,255,0.35);
|
|
114
|
+
--cue-border: rgba(255,255,255,0.10);
|
|
115
|
+
--cue-border-strong: rgba(255,255,255,0.25);
|
|
116
|
+
--cue-border-overlay: rgba(255,255,255,0.25);
|
|
117
|
+
--cue-accent: #8a93a6;
|
|
118
|
+
--cue-accent-hover: #9aa3b6;
|
|
119
|
+
--cue-accent-fg: #0a0a0a;
|
|
120
|
+
--cue-ok: oklch(0.72 0.17 149);
|
|
121
|
+
--cue-busy: oklch(0.78 0.13 220);
|
|
122
|
+
--cue-warn: oklch(0.8 0.17 75);
|
|
123
|
+
--cue-warn-fg: #000000;
|
|
124
|
+
--cue-danger: #ff5f57;
|
|
125
|
+
--cue-danger-fg: #000000;
|
|
126
|
+
--cue-info: oklch(0.75 0.12 252);
|
|
127
|
+
--cue-stream: oklch(0.707 0.165 254.624);
|
|
128
|
+
--cue-selection: rgba(255,255,255,0.20);
|
|
129
|
+
--cue-focus: #8a93a6;
|
|
130
|
+
--cue-data-ground: #000000;
|
|
131
|
+
--cue-scrim: rgba(0,0,0,0.55);
|
|
132
|
+
|
|
133
|
+
/* derived — kept inside the block so color-mix() reads this theme's values */
|
|
134
|
+
--cue-accent-soft: color-mix(in oklch, var(--cue-accent) 14%, transparent);
|
|
135
|
+
--cue-ok-soft: color-mix(in oklch, var(--cue-ok) 12%, transparent);
|
|
136
|
+
--cue-ok-border: color-mix(in oklch, var(--cue-ok) 35%, transparent);
|
|
137
|
+
--cue-busy-soft: color-mix(in oklch, var(--cue-busy) 12%, transparent);
|
|
138
|
+
--cue-busy-border: color-mix(in oklch, var(--cue-busy) 35%, transparent);
|
|
139
|
+
--cue-warn-soft: color-mix(in oklch, var(--cue-warn) 12%, transparent);
|
|
140
|
+
--cue-warn-border: color-mix(in oklch, var(--cue-warn) 35%, transparent);
|
|
141
|
+
--cue-danger-soft: color-mix(in oklch, var(--cue-danger) 12%, transparent);
|
|
142
|
+
--cue-danger-border: color-mix(in oklch, var(--cue-danger) 35%, transparent);
|
|
143
|
+
--cue-info-soft: color-mix(in oklch, var(--cue-info) 12%, transparent);
|
|
144
|
+
--cue-info-border: color-mix(in oklch, var(--cue-info) 35%, transparent);
|
|
145
|
+
--cue-stream-soft: color-mix(in oklch, var(--cue-stream) 12%, transparent);
|
|
146
|
+
--cue-stream-border: color-mix(in oklch, var(--cue-stream) 35%, transparent);
|
|
147
|
+
--cue-hair: color-mix(in oklch, var(--cue-fg) 6%, transparent);
|
|
148
|
+
--cue-hair-strong: color-mix(in oklch, var(--cue-fg) 12%, transparent);
|
|
149
|
+
--cue-row-hover: color-mix(in oklch, var(--cue-fg) 3.5%, transparent);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@media (prefers-color-scheme: light) {
|
|
154
|
+
[data-mode="system"] {
|
|
155
|
+
color-scheme: light dark;
|
|
156
|
+
--cue-bg: #fcfcfc;
|
|
157
|
+
--cue-sunken: #f0f0f0;
|
|
158
|
+
--cue-surface-1: #ffffff;
|
|
159
|
+
--cue-surface-2: #f6f6f6;
|
|
160
|
+
--cue-surface-3: #ededed;
|
|
161
|
+
--cue-fg: #0a0a0a;
|
|
162
|
+
--cue-fg-muted: rgba(0,0,0,0.55);
|
|
163
|
+
--cue-fg-subtle: rgba(0,0,0,0.35);
|
|
164
|
+
--cue-border: rgba(0,0,0,0.10);
|
|
165
|
+
--cue-border-strong: rgba(0,0,0,0.25);
|
|
166
|
+
--cue-border-overlay: rgba(0,0,0,0.25);
|
|
167
|
+
--cue-accent: #3f4759;
|
|
168
|
+
--cue-accent-hover: #333a4a;
|
|
169
|
+
--cue-accent-fg: #ffffff;
|
|
170
|
+
--cue-ok: oklch(0.53 0.14 149);
|
|
171
|
+
--cue-busy: oklch(0.54 0.1 225);
|
|
172
|
+
--cue-warn: oklch(0.55 0.115 70);
|
|
173
|
+
--cue-warn-fg: #fcfcfc;
|
|
174
|
+
--cue-danger: #d03030;
|
|
175
|
+
--cue-danger-fg: #fcfcfc;
|
|
176
|
+
--cue-info: oklch(0.55 0.16 258);
|
|
177
|
+
--cue-stream: oklch(0.55 0.21 259.815);
|
|
178
|
+
--cue-selection: rgba(0,0,0,0.20);
|
|
179
|
+
--cue-focus: #3f4759;
|
|
180
|
+
--cue-data-ground: #000000;
|
|
181
|
+
--cue-scrim: rgba(0,0,0,0.55);
|
|
182
|
+
|
|
183
|
+
/* derived — kept inside the block so color-mix() reads this theme's values */
|
|
184
|
+
--cue-accent-soft: color-mix(in oklch, var(--cue-accent) 14%, transparent);
|
|
185
|
+
--cue-ok-soft: color-mix(in oklch, var(--cue-ok) 12%, transparent);
|
|
186
|
+
--cue-ok-border: color-mix(in oklch, var(--cue-ok) 35%, transparent);
|
|
187
|
+
--cue-busy-soft: color-mix(in oklch, var(--cue-busy) 12%, transparent);
|
|
188
|
+
--cue-busy-border: color-mix(in oklch, var(--cue-busy) 35%, transparent);
|
|
189
|
+
--cue-warn-soft: color-mix(in oklch, var(--cue-warn) 12%, transparent);
|
|
190
|
+
--cue-warn-border: color-mix(in oklch, var(--cue-warn) 35%, transparent);
|
|
191
|
+
--cue-danger-soft: color-mix(in oklch, var(--cue-danger) 12%, transparent);
|
|
192
|
+
--cue-danger-border: color-mix(in oklch, var(--cue-danger) 35%, transparent);
|
|
193
|
+
--cue-info-soft: color-mix(in oklch, var(--cue-info) 12%, transparent);
|
|
194
|
+
--cue-info-border: color-mix(in oklch, var(--cue-info) 35%, transparent);
|
|
195
|
+
--cue-stream-soft: color-mix(in oklch, var(--cue-stream) 12%, transparent);
|
|
196
|
+
--cue-stream-border: color-mix(in oklch, var(--cue-stream) 35%, transparent);
|
|
197
|
+
--cue-hair: color-mix(in oklch, var(--cue-fg) 6%, transparent);
|
|
198
|
+
--cue-hair-strong: color-mix(in oklch, var(--cue-fg) 12%, transparent);
|
|
199
|
+
--cue-row-hover: color-mix(in oklch, var(--cue-fg) 3.5%, transparent);
|
|
200
|
+
}
|
|
201
|
+
}
|
package/dist/base.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dark": {
|
|
3
|
+
"bg": "#0a0a0a",
|
|
4
|
+
"sunken": "#000000",
|
|
5
|
+
"surface-1": "#121212",
|
|
6
|
+
"surface-2": "#1a1a1a",
|
|
7
|
+
"surface-3": "#242424",
|
|
8
|
+
"fg": "#f5f5f5",
|
|
9
|
+
"fg-muted": "rgba(255,255,255,0.55)",
|
|
10
|
+
"fg-subtle": "rgba(255,255,255,0.35)",
|
|
11
|
+
"border": "rgba(255,255,255,0.10)",
|
|
12
|
+
"border-strong": "rgba(255,255,255,0.25)",
|
|
13
|
+
"border-overlay": "rgba(255,255,255,0.25)",
|
|
14
|
+
"accent": "#8a93a6",
|
|
15
|
+
"accent-hover": "#9aa3b6",
|
|
16
|
+
"accent-fg": "#0a0a0a",
|
|
17
|
+
"ok": "oklch(0.72 0.17 149)",
|
|
18
|
+
"busy": "oklch(0.78 0.13 220)",
|
|
19
|
+
"warn": "oklch(0.8 0.17 75)",
|
|
20
|
+
"warn-fg": "#000000",
|
|
21
|
+
"danger": "#ff5f57",
|
|
22
|
+
"danger-fg": "#000000",
|
|
23
|
+
"info": "oklch(0.75 0.12 252)",
|
|
24
|
+
"stream": "oklch(0.707 0.165 254.624)",
|
|
25
|
+
"selection": "rgba(255,255,255,0.20)",
|
|
26
|
+
"focus": "#8a93a6",
|
|
27
|
+
"data-ground": "#000000",
|
|
28
|
+
"scrim": "rgba(0,0,0,0.55)"
|
|
29
|
+
},
|
|
30
|
+
"light": {
|
|
31
|
+
"bg": "#fcfcfc",
|
|
32
|
+
"sunken": "#f0f0f0",
|
|
33
|
+
"surface-1": "#ffffff",
|
|
34
|
+
"surface-2": "#f6f6f6",
|
|
35
|
+
"surface-3": "#ededed",
|
|
36
|
+
"fg": "#0a0a0a",
|
|
37
|
+
"fg-muted": "rgba(0,0,0,0.55)",
|
|
38
|
+
"fg-subtle": "rgba(0,0,0,0.35)",
|
|
39
|
+
"border": "rgba(0,0,0,0.10)",
|
|
40
|
+
"border-strong": "rgba(0,0,0,0.25)",
|
|
41
|
+
"border-overlay": "rgba(0,0,0,0.25)",
|
|
42
|
+
"accent": "#3f4759",
|
|
43
|
+
"accent-hover": "#333a4a",
|
|
44
|
+
"accent-fg": "#ffffff",
|
|
45
|
+
"ok": "oklch(0.53 0.14 149)",
|
|
46
|
+
"busy": "oklch(0.54 0.1 225)",
|
|
47
|
+
"warn": "oklch(0.55 0.115 70)",
|
|
48
|
+
"warn-fg": "#fcfcfc",
|
|
49
|
+
"danger": "#d03030",
|
|
50
|
+
"danger-fg": "#fcfcfc",
|
|
51
|
+
"info": "oklch(0.55 0.16 258)",
|
|
52
|
+
"stream": "oklch(0.55 0.21 259.815)",
|
|
53
|
+
"selection": "rgba(0,0,0,0.20)",
|
|
54
|
+
"focus": "#3f4759",
|
|
55
|
+
"data-ground": "#000000",
|
|
56
|
+
"scrim": "rgba(0,0,0,0.55)"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
//#region src/contrast.d.ts
|
|
2
|
+
/** Non-text and large-text AA: UI components, graphics, status dots. */
|
|
3
|
+
declare const WCAG_AA_NON_TEXT = 3;
|
|
4
|
+
/** Body-text AA. The floor for anything a reader is expected to read. */
|
|
5
|
+
declare const WCAG_AA_TEXT = 4.5;
|
|
6
|
+
/** Body-text AAA. What this portfolio holds its primary ink to. */
|
|
7
|
+
declare const WCAG_AAA_TEXT = 7;
|
|
8
|
+
/**
|
|
9
|
+
* WCAG relative luminance: 0 for black, 1 for white.
|
|
10
|
+
*
|
|
11
|
+
* Alpha is ignored — a translucent colour has no luminance of its own, only one
|
|
12
|
+
* in front of something. {@link contrastRatio} is where that composite happens.
|
|
13
|
+
*
|
|
14
|
+
* @param color - Any CSS colour string.
|
|
15
|
+
* @returns Relative luminance in `[0, 1]`.
|
|
16
|
+
* @throws TypeError if the value is not a colour.
|
|
17
|
+
* @example
|
|
18
|
+
* relativeLuminance("#767676"); // → 0.1845…
|
|
19
|
+
*/
|
|
20
|
+
declare function relativeLuminance(color: string): number;
|
|
21
|
+
/**
|
|
22
|
+
* WCAG contrast ratio between two colours, from 1:1 to 21:1.
|
|
23
|
+
*
|
|
24
|
+
* Symmetric, because a ratio has no direction — the arguments are named for
|
|
25
|
+
* readability, not for order. A **translucent foreground is composited over the
|
|
26
|
+
* background first**, which is not decoration: half this system's `fg-muted`
|
|
27
|
+
* tokens are white-alpha ink, and measuring them raw would report the ratio of
|
|
28
|
+
* pure white and pass everything.
|
|
29
|
+
*
|
|
30
|
+
* @param foreground - The ink. Composited over `background` if it has alpha.
|
|
31
|
+
* @param background - The ground. Its own alpha is ignored — nothing is behind it.
|
|
32
|
+
* @returns The ratio, `>= 1`.
|
|
33
|
+
* @throws TypeError naming whichever value could not be read.
|
|
34
|
+
* @example
|
|
35
|
+
* contrastRatio("#767676", "#ffffff"); // → 4.5422…
|
|
36
|
+
*/
|
|
37
|
+
declare function contrastRatio(foreground: string, background: string): number;
|
|
38
|
+
/** One pair a theme is checked on. */
|
|
39
|
+
interface ContrastRequirement {
|
|
40
|
+
/** `"<foreground>/<background>"` — stable, and readable in a failure message. */
|
|
41
|
+
id: string;
|
|
42
|
+
/** Bare token name of the ink. */
|
|
43
|
+
foreground: string;
|
|
44
|
+
/** Bare token name of the ground. */
|
|
45
|
+
background: string;
|
|
46
|
+
/** The ratio the pair has to reach. */
|
|
47
|
+
minimum: number;
|
|
48
|
+
/**
|
|
49
|
+
* Measured and reported, but not counted in the verdict.
|
|
50
|
+
*
|
|
51
|
+
* The tier exists because of one pair. `focus` is the only role this system
|
|
52
|
+
* has empirically caught failing — three shipped presets at 2.45:1 against
|
|
53
|
+
* the fill a focused field paints it over — and it was the one role the
|
|
54
|
+
* report did not measure, because adding it as a requirement would newly fail
|
|
55
|
+
* consumers' previously-clean generated themes. That concern is real. The
|
|
56
|
+
* conclusion drawn from it was to remove the measurement, when the choice was
|
|
57
|
+
* never between failing a consumer and telling them nothing.
|
|
58
|
+
*/
|
|
59
|
+
advisory?: boolean;
|
|
60
|
+
/** Why this minimum, in one line — the report carries it into every check. */
|
|
61
|
+
reason: string;
|
|
62
|
+
}
|
|
63
|
+
declare const CONTRAST_REQUIREMENTS: readonly ContrastRequirement[];
|
|
64
|
+
/** One measured pair. */
|
|
65
|
+
interface ContrastCheck extends ContrastRequirement {
|
|
66
|
+
/** Which block the pair was measured in. */
|
|
67
|
+
mode: "dark" | "light";
|
|
68
|
+
/** The colour the foreground token resolved to. */
|
|
69
|
+
foregroundValue: string;
|
|
70
|
+
/** The colour the background token resolved to. */
|
|
71
|
+
backgroundValue: string;
|
|
72
|
+
/** The measured ratio. */
|
|
73
|
+
ratio: number;
|
|
74
|
+
/** Whether {@link ContrastRequirement.minimum} was reached. */
|
|
75
|
+
passes: boolean;
|
|
76
|
+
/** Whether the values were derived rather than authored — see the light variant. */
|
|
77
|
+
provisional: boolean;
|
|
78
|
+
}
|
|
79
|
+
/** One block's worth of resolved colours. */
|
|
80
|
+
interface ContrastInput {
|
|
81
|
+
/** Which block these are. */
|
|
82
|
+
mode: "dark" | "light";
|
|
83
|
+
/** Bare token name → resolved CSS colour. Pairs with a missing token are skipped. */
|
|
84
|
+
tokens: Readonly<Record<string, string>>;
|
|
85
|
+
/** Mark the resulting checks provisional. Defaults to `false`. */
|
|
86
|
+
provisional?: boolean;
|
|
87
|
+
}
|
|
88
|
+
/** Every pair, measured, plus the verdict. */
|
|
89
|
+
interface ContrastReport {
|
|
90
|
+
/** True when nothing failed. */
|
|
91
|
+
passes: boolean;
|
|
92
|
+
/** True when any measured block was derived rather than authored. */
|
|
93
|
+
provisional: boolean;
|
|
94
|
+
/** Every pair that could be measured, in requirement order, dark block first. */
|
|
95
|
+
checks: readonly ContrastCheck[];
|
|
96
|
+
/** The required pairs that failed. Empty when {@link ContrastReport.passes}. */
|
|
97
|
+
failures: readonly ContrastCheck[];
|
|
98
|
+
/**
|
|
99
|
+
* The advisory pairs that missed their floor.
|
|
100
|
+
*
|
|
101
|
+
* Disjoint from {@link ContrastReport.failures}, not a superset of it: a
|
|
102
|
+
* caller reading `failures` is reading what makes the theme unusable, and a
|
|
103
|
+
* caller reading `advisories` is reading what nobody has failed them on yet.
|
|
104
|
+
* Folding the two together would make `failures` name pairs the verdict did
|
|
105
|
+
* not turn on, which is the compatibility promise this tier exists to keep.
|
|
106
|
+
*/
|
|
107
|
+
advisories: readonly ContrastCheck[];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Measure every requirement against every block handed in.
|
|
111
|
+
*
|
|
112
|
+
* A pair whose tokens are missing is **skipped**, not failed: the report says
|
|
113
|
+
* what it measured, and a caller checking a partial palette should not be told
|
|
114
|
+
* that a colour it never supplied is unreadable.
|
|
115
|
+
*
|
|
116
|
+
* @param inputs - One entry per block, dark first by convention.
|
|
117
|
+
* @returns The report.
|
|
118
|
+
* @example
|
|
119
|
+
* contrastReport([{ mode: "dark", tokens: { fg: "#fff", bg: "#000" } }]).passes; // → true
|
|
120
|
+
*/
|
|
121
|
+
declare function contrastReport(inputs: readonly ContrastInput[]): ContrastReport;
|
|
122
|
+
//#endregion
|
|
123
|
+
export { CONTRAST_REQUIREMENTS, ContrastCheck, ContrastInput, ContrastReport, ContrastRequirement, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport, relativeLuminance };
|