@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 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 };