@rhombuskit/theme-builder 1.15.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/LICENSE +21 -0
- package/README.md +38 -0
- package/dist/index.d.mts +113 -0
- package/dist/index.mjs +833 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Doug Williamson
|
|
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,38 @@
|
|
|
1
|
+
# @rhombuskit/theme-builder
|
|
2
|
+
|
|
3
|
+
Generate WCAG-AA-validated [RhombusKit](https://rhombuskit.online) themes from a few seed
|
|
4
|
+
colours. Framework-agnostic (no Angular peer) — usable from a build script, a CLI, or the
|
|
5
|
+
interactive [`/theme-builder`](https://rhombuskit.online/theme-builder) page.
|
|
6
|
+
|
|
7
|
+
You give it a brand accent (and, optionally, a neutral); it emits **both** a light and a dark
|
|
8
|
+
theme covering the full 60-token RhombusKit CONTRACT, then re-validates every text/background
|
|
9
|
+
pair for WCAG 2.1 AA and either returns an AA-clean theme or throws — it never returns sub-AA
|
|
10
|
+
output.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm i @rhombuskit/theme-builder @rhombuskit/tokens
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { generateTheme, serializeThemeCss, toRegisteredThemes } from '@rhombuskit/theme-builder';
|
|
22
|
+
|
|
23
|
+
const theme = generateTheme({ accent: '#0f766e', name: 'teal' });
|
|
24
|
+
|
|
25
|
+
// Paste-ready [data-theme] CSS over all 60 CONTRACT tokens (light + dark):
|
|
26
|
+
console.log(serializeThemeCss(theme));
|
|
27
|
+
|
|
28
|
+
// Metadata for the theme registry (pair with the CSS above):
|
|
29
|
+
const registered = toRegisteredThemes(theme); // RegisteredTheme[]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The emitted CSS + registration follow the sanctioned custom-theme path in
|
|
33
|
+
[the theming guide](https://rhombuskit.online/theming). A bare `generateTheme()` reproduces the
|
|
34
|
+
built-in `rhombus-light`/`rhombus-dark` packs byte-for-byte.
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { SemanticTokenName } from '@rhombuskit/tokens';
|
|
2
|
+
|
|
3
|
+
declare const RUNGS: readonly [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
|
|
4
|
+
type Rung = (typeof RUNGS)[number];
|
|
5
|
+
type FullRamp = Record<Rung, string>;
|
|
6
|
+
|
|
7
|
+
/** A few seed colours in. See the spec for the derivation. */
|
|
8
|
+
interface ThemeSeed {
|
|
9
|
+
/** Brand accent hex. Omitted OR === #7c3aed → shipped violet verbatim. */
|
|
10
|
+
accent?: string;
|
|
11
|
+
/** Neutral/surface base hex. Omitted OR === #64748b (slate-500) → shipped slate verbatim. */
|
|
12
|
+
neutral?: string;
|
|
13
|
+
/** Registry id (default: the OKLCH hue name of the accent, e.g. "indigo"). */
|
|
14
|
+
name?: string;
|
|
15
|
+
/** Human label (default: Title-Case of name). */
|
|
16
|
+
label?: string;
|
|
17
|
+
/** Chroma-envelope multiplier for generated ramps (default 1, clamped [0.5, 1.5]). */
|
|
18
|
+
vividness?: number;
|
|
19
|
+
}
|
|
20
|
+
/** A complete light + dark theme over the 60-token CONTRACT, plus a fidelity report. */
|
|
21
|
+
interface GeneratedTheme {
|
|
22
|
+
name: string;
|
|
23
|
+
label: string;
|
|
24
|
+
light: Record<SemanticTokenName, string>;
|
|
25
|
+
dark: Record<SemanticTokenName, string>;
|
|
26
|
+
report: {
|
|
27
|
+
warnings: string[];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
interface BuildOptions {
|
|
31
|
+
/** Disable the AA-rescue nudge. Only for the red-gate test — a green seed then throws. */
|
|
32
|
+
nudge?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Structural match for `@rhombuskit/theme-engine`'s `RegisteredTheme` — declared locally so this
|
|
36
|
+
* package stays free of an Angular/theme-engine dependency. Assignable to `provideRhombusThemes()`.
|
|
37
|
+
*/
|
|
38
|
+
interface RegisteredThemeMeta {
|
|
39
|
+
name: string;
|
|
40
|
+
label: string;
|
|
41
|
+
mode: 'light' | 'dark';
|
|
42
|
+
palette: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A dark, hue-preserving tint for the one dark background no ramp step can supply. */
|
|
46
|
+
declare function deepTint(hex: string): string;
|
|
47
|
+
/** Synthesize an 11-rung ramp from a seed: fixed lightness ladder, seed hue + scaled chroma. */
|
|
48
|
+
declare function buildRamp(seedHex: string, Lcurve: readonly number[], Cenv: readonly number[], cap?: readonly number[], vividness?: number): FullRamp;
|
|
49
|
+
/** Generate a complete, AA-clean light + dark theme from a few seed colours. */
|
|
50
|
+
declare function build(seed?: ThemeSeed, opts?: BuildOptions): GeneratedTheme;
|
|
51
|
+
|
|
52
|
+
interface ThemeAAFailure {
|
|
53
|
+
fg: SemanticTokenName;
|
|
54
|
+
bg: SemanticTokenName;
|
|
55
|
+
ratio: number;
|
|
56
|
+
min: number;
|
|
57
|
+
}
|
|
58
|
+
interface ThemeAAResult {
|
|
59
|
+
ok: boolean;
|
|
60
|
+
failures: ThemeAAFailure[];
|
|
61
|
+
}
|
|
62
|
+
type ThemeRecord = Partial<Record<SemanticTokenName, string>>;
|
|
63
|
+
declare function validateThemeAA(record: ThemeRecord): ThemeAAResult;
|
|
64
|
+
|
|
65
|
+
/** Two `[data-theme="<name>"]{…}` blocks (light + `-dark`) over every CONTRACT token. */
|
|
66
|
+
declare function serializeThemeCss(theme: GeneratedTheme): string;
|
|
67
|
+
/** Light + dark `RegisteredTheme` metadata sharing one palette (pair with the CSS above). */
|
|
68
|
+
declare function toRegisteredThemes(theme: GeneratedTheme): RegisteredThemeMeta[];
|
|
69
|
+
/** The `declare module '@rhombuskit/theme-engine'` snippet so `setTheme('<name>')` type-checks. */
|
|
70
|
+
declare function toAugmentation(theme: GeneratedTheme): string;
|
|
71
|
+
|
|
72
|
+
/** [r, g, b] in 0–255, a in 0–1. */
|
|
73
|
+
type Rgba = [number, number, number, number];
|
|
74
|
+
/**
|
|
75
|
+
* Parse `#rgb` / `#rrggbb` / `#rrggbbaa` / `rgb()` / `rgba()` (space- or comma-separated,
|
|
76
|
+
* optional slash or 4th-value alpha). Returns null for var()/color-mix/named/anything else —
|
|
77
|
+
* such values are not contrast-checkable and are skipped by callers.
|
|
78
|
+
*/
|
|
79
|
+
declare function parseColor(css: string): Rgba | null;
|
|
80
|
+
/** Normalise any parseable colour to lowercase `#rrggbb` (alpha stripped); null if unparseable. */
|
|
81
|
+
declare function normalizeHex(css: string): string | null;
|
|
82
|
+
/** WCAG 2.1 relative luminance (sRGB linearisation, 0.2126/0.7152/0.0722). */
|
|
83
|
+
declare function relativeLuminance([r, g, b]: readonly [number, number, number]): number;
|
|
84
|
+
/** WCAG contrast ratio (Lhi+0.05)/(Llo+0.05); null if either side is non-concrete. */
|
|
85
|
+
declare function contrastRatio(fg: string, bg: string): number | null;
|
|
86
|
+
/**
|
|
87
|
+
* Composite a (possibly translucent) foreground over a solid background, returning the opaque
|
|
88
|
+
* hex the viewer actually sees. Used before measuring contrast so alpha can never pass vacuously.
|
|
89
|
+
* An opaque foreground is returned unchanged (as hex).
|
|
90
|
+
*/
|
|
91
|
+
declare function alphaComposite(fg: string, bg: string): string;
|
|
92
|
+
interface Oklch {
|
|
93
|
+
L: number;
|
|
94
|
+
C: number;
|
|
95
|
+
H: number;
|
|
96
|
+
}
|
|
97
|
+
declare function toOKLCH(hex: string): Oklch;
|
|
98
|
+
/** OKLCH → hex (channels clamped to 0–255; use gamutClampToSrgb first for faithful hues). */
|
|
99
|
+
declare function fromOKLCH(L: number, C: number, H: number): string;
|
|
100
|
+
/**
|
|
101
|
+
* Reduce chroma (holding L and H) until the colour fits inside sRGB, then convert to hex.
|
|
102
|
+
* An already-in-gamut colour is returned exactly. CSS Color 4 chroma reduction by bisection.
|
|
103
|
+
*/
|
|
104
|
+
declare function gamutClampToSrgb(L: number, C: number, H: number): string;
|
|
105
|
+
|
|
106
|
+
/** Thrown when a generated theme cannot be made AA-clean — the generator never returns sub-AA. */
|
|
107
|
+
declare class ThemeAAError extends Error {
|
|
108
|
+
readonly mode: 'light' | 'dark';
|
|
109
|
+
readonly failures: ThemeAAFailure[];
|
|
110
|
+
constructor(mode: 'light' | 'dark', failures: ThemeAAFailure[]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { type BuildOptions, type GeneratedTheme, type Oklch, type RegisteredThemeMeta, type Rgba, ThemeAAError, type ThemeAAFailure, type ThemeAAResult, type ThemeSeed, alphaComposite, buildRamp, contrastRatio, deepTint, fromOKLCH, gamutClampToSrgb, build as generateTheme, normalizeHex, parseColor, relativeLuminance, serializeThemeCss, toAugmentation, toOKLCH, toRegisteredThemes, validateThemeAA };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/color-math.ts
|
|
5
|
+
var clampByte = /* @__PURE__ */ __name((n) => Math.min(255, Math.max(0, Math.round(n))), "clampByte");
|
|
6
|
+
var toHex = /* @__PURE__ */ __name((r, g, b) => "#" + [
|
|
7
|
+
r,
|
|
8
|
+
g,
|
|
9
|
+
b
|
|
10
|
+
].map((c) => clampByte(c).toString(16).padStart(2, "0")).join(""), "toHex");
|
|
11
|
+
function parseColor(css) {
|
|
12
|
+
const s = css.trim();
|
|
13
|
+
let m = /^#([0-9a-f]{3})$/i.exec(s);
|
|
14
|
+
if (m) {
|
|
15
|
+
const h = m[1];
|
|
16
|
+
return [
|
|
17
|
+
parseInt(h[0] + h[0], 16),
|
|
18
|
+
parseInt(h[1] + h[1], 16),
|
|
19
|
+
parseInt(h[2] + h[2], 16),
|
|
20
|
+
1
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s);
|
|
24
|
+
if (m) {
|
|
25
|
+
const h = m[1];
|
|
26
|
+
return [
|
|
27
|
+
parseInt(h.slice(0, 2), 16),
|
|
28
|
+
parseInt(h.slice(2, 4), 16),
|
|
29
|
+
parseInt(h.slice(4, 6), 16),
|
|
30
|
+
m[2] === void 0 ? 1 : parseInt(m[2], 16) / 255
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
m = /^rgba?\(\s*([0-9.]+)[\s,]+([0-9.]+)[\s,]+([0-9.]+)(?:\s*[,/]\s*([0-9.]+))?\s*\)$/i.exec(s);
|
|
34
|
+
if (m) {
|
|
35
|
+
return [
|
|
36
|
+
Number(m[1]),
|
|
37
|
+
Number(m[2]),
|
|
38
|
+
Number(m[3]),
|
|
39
|
+
m[4] === void 0 ? 1 : Number(m[4])
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
__name(parseColor, "parseColor");
|
|
45
|
+
function normalizeHex(css) {
|
|
46
|
+
const p = parseColor(css);
|
|
47
|
+
return p ? toHex(p[0], p[1], p[2]) : null;
|
|
48
|
+
}
|
|
49
|
+
__name(normalizeHex, "normalizeHex");
|
|
50
|
+
function relativeLuminance([r, g, b]) {
|
|
51
|
+
const channel = /* @__PURE__ */ __name((v) => {
|
|
52
|
+
const c = v / 255;
|
|
53
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
54
|
+
}, "channel");
|
|
55
|
+
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
56
|
+
}
|
|
57
|
+
__name(relativeLuminance, "relativeLuminance");
|
|
58
|
+
function contrastRatio(fg, bg) {
|
|
59
|
+
const a = parseColor(fg);
|
|
60
|
+
const b = parseColor(bg);
|
|
61
|
+
if (!a || !b) return null;
|
|
62
|
+
const l1 = relativeLuminance([
|
|
63
|
+
a[0],
|
|
64
|
+
a[1],
|
|
65
|
+
a[2]
|
|
66
|
+
]);
|
|
67
|
+
const l2 = relativeLuminance([
|
|
68
|
+
b[0],
|
|
69
|
+
b[1],
|
|
70
|
+
b[2]
|
|
71
|
+
]);
|
|
72
|
+
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
73
|
+
}
|
|
74
|
+
__name(contrastRatio, "contrastRatio");
|
|
75
|
+
function alphaComposite(fg, bg) {
|
|
76
|
+
const f = parseColor(fg);
|
|
77
|
+
const b = parseColor(bg);
|
|
78
|
+
if (!f || !b) return fg;
|
|
79
|
+
const a = f[3];
|
|
80
|
+
return toHex(f[0] * a + b[0] * (1 - a), f[1] * a + b[1] * (1 - a), f[2] * a + b[2] * (1 - a));
|
|
81
|
+
}
|
|
82
|
+
__name(alphaComposite, "alphaComposite");
|
|
83
|
+
var linearize = /* @__PURE__ */ __name((c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4), "linearize");
|
|
84
|
+
var delinearize = /* @__PURE__ */ __name((c) => c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055, "delinearize");
|
|
85
|
+
function linearSrgbToOklab(r, g, b) {
|
|
86
|
+
const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
|
87
|
+
const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
|
88
|
+
const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
|
89
|
+
const l_ = Math.cbrt(l);
|
|
90
|
+
const m_ = Math.cbrt(m);
|
|
91
|
+
const s_ = Math.cbrt(s);
|
|
92
|
+
return [
|
|
93
|
+
0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
|
|
94
|
+
1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
|
|
95
|
+
0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
__name(linearSrgbToOklab, "linearSrgbToOklab");
|
|
99
|
+
function oklabToLinearSrgb(L, a, b) {
|
|
100
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
101
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
102
|
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b;
|
|
103
|
+
const l = l_ * l_ * l_;
|
|
104
|
+
const m = m_ * m_ * m_;
|
|
105
|
+
const s = s_ * s_ * s_;
|
|
106
|
+
return [
|
|
107
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
108
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
109
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
__name(oklabToLinearSrgb, "oklabToLinearSrgb");
|
|
113
|
+
function toOKLCH(hex) {
|
|
114
|
+
const p = parseColor(hex);
|
|
115
|
+
if (!p) throw new Error(`toOKLCH: unparseable colour "${hex}"`);
|
|
116
|
+
const [L, a, b] = linearSrgbToOklab(linearize(p[0] / 255), linearize(p[1] / 255), linearize(p[2] / 255));
|
|
117
|
+
const C = Math.hypot(a, b);
|
|
118
|
+
let H = Math.atan2(b, a) * 180 / Math.PI;
|
|
119
|
+
if (H < 0) H += 360;
|
|
120
|
+
return {
|
|
121
|
+
L,
|
|
122
|
+
C,
|
|
123
|
+
H
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
__name(toOKLCH, "toOKLCH");
|
|
127
|
+
function fromOKLCH(L, C, H) {
|
|
128
|
+
const rad = H * Math.PI / 180;
|
|
129
|
+
const [r, g, b] = oklabToLinearSrgb(L, C * Math.cos(rad), C * Math.sin(rad));
|
|
130
|
+
return toHex(delinearize(r) * 255, delinearize(g) * 255, delinearize(b) * 255);
|
|
131
|
+
}
|
|
132
|
+
__name(fromOKLCH, "fromOKLCH");
|
|
133
|
+
function inGamut(L, C, H) {
|
|
134
|
+
const rad = H * Math.PI / 180;
|
|
135
|
+
const lin = oklabToLinearSrgb(L, C * Math.cos(rad), C * Math.sin(rad));
|
|
136
|
+
const eps = 1e-4;
|
|
137
|
+
return lin.every((c) => c >= -eps && c <= 1 + eps);
|
|
138
|
+
}
|
|
139
|
+
__name(inGamut, "inGamut");
|
|
140
|
+
function gamutClampToSrgb(L, C, H) {
|
|
141
|
+
if (inGamut(L, C, H)) return fromOKLCH(L, C, H);
|
|
142
|
+
let lo = 0;
|
|
143
|
+
let hi = C;
|
|
144
|
+
for (let i = 0; i < 28; i++) {
|
|
145
|
+
const mid = (lo + hi) / 2;
|
|
146
|
+
if (inGamut(L, mid, H)) lo = mid;
|
|
147
|
+
else hi = mid;
|
|
148
|
+
}
|
|
149
|
+
return fromOKLCH(L, lo, H);
|
|
150
|
+
}
|
|
151
|
+
__name(gamutClampToSrgb, "gamutClampToSrgb");
|
|
152
|
+
|
|
153
|
+
// src/constants.ts
|
|
154
|
+
import { tokens } from "@rhombuskit/tokens";
|
|
155
|
+
var RUNGS = [
|
|
156
|
+
50,
|
|
157
|
+
100,
|
|
158
|
+
200,
|
|
159
|
+
300,
|
|
160
|
+
400,
|
|
161
|
+
500,
|
|
162
|
+
600,
|
|
163
|
+
700,
|
|
164
|
+
800,
|
|
165
|
+
900,
|
|
166
|
+
950
|
|
167
|
+
];
|
|
168
|
+
var prim = tokens.primitives;
|
|
169
|
+
function sourceRamp(family) {
|
|
170
|
+
const ramp = {};
|
|
171
|
+
for (const step of RUNGS) {
|
|
172
|
+
const value = prim[`${family}-${step}`];
|
|
173
|
+
if (value) ramp[step] = value;
|
|
174
|
+
}
|
|
175
|
+
return ramp;
|
|
176
|
+
}
|
|
177
|
+
__name(sourceRamp, "sourceRamp");
|
|
178
|
+
var RAMPS = {
|
|
179
|
+
violet: sourceRamp("violet"),
|
|
180
|
+
slate: sourceRamp("slate"),
|
|
181
|
+
green: sourceRamp("green"),
|
|
182
|
+
amber: sourceRamp("amber"),
|
|
183
|
+
red: sourceRamp("red")
|
|
184
|
+
};
|
|
185
|
+
var L_ACCENT_CURVE = RUNGS.map((r) => toOKLCH(RAMPS.violet[r]).L);
|
|
186
|
+
var CHROMA_ENV = RUNGS.map((r) => toOKLCH(RAMPS.violet[r]).C);
|
|
187
|
+
var L_NEUTRAL_CURVE = RUNGS.map((r) => toOKLCH(RAMPS.slate[r]).L);
|
|
188
|
+
var C_NEUTRAL_CAP = RUNGS.map((r) => toOKLCH(RAMPS.slate[r]).C);
|
|
189
|
+
var BASE = {
|
|
190
|
+
light: tokens.themes["rhombus-light"],
|
|
191
|
+
dark: tokens.themes["rhombus-dark"]
|
|
192
|
+
};
|
|
193
|
+
var CONTRACT_KEYS = Object.keys(BASE.light);
|
|
194
|
+
var TEXT_PAIRS = [
|
|
195
|
+
[
|
|
196
|
+
"--text-primary",
|
|
197
|
+
"--bg"
|
|
198
|
+
],
|
|
199
|
+
[
|
|
200
|
+
"--text-primary",
|
|
201
|
+
"--surface-0"
|
|
202
|
+
],
|
|
203
|
+
[
|
|
204
|
+
"--text-primary",
|
|
205
|
+
"--surface-1"
|
|
206
|
+
],
|
|
207
|
+
[
|
|
208
|
+
"--text-primary",
|
|
209
|
+
"--surface-2"
|
|
210
|
+
],
|
|
211
|
+
[
|
|
212
|
+
"--text-secondary",
|
|
213
|
+
"--bg"
|
|
214
|
+
],
|
|
215
|
+
[
|
|
216
|
+
"--text-secondary",
|
|
217
|
+
"--surface-0"
|
|
218
|
+
],
|
|
219
|
+
[
|
|
220
|
+
"--text-muted",
|
|
221
|
+
"--bg"
|
|
222
|
+
],
|
|
223
|
+
[
|
|
224
|
+
"--text-muted",
|
|
225
|
+
"--surface-0"
|
|
226
|
+
],
|
|
227
|
+
[
|
|
228
|
+
"--text-accent",
|
|
229
|
+
"--bg"
|
|
230
|
+
],
|
|
231
|
+
[
|
|
232
|
+
"--text-accent",
|
|
233
|
+
"--surface-0"
|
|
234
|
+
],
|
|
235
|
+
[
|
|
236
|
+
"--btn-primary-text",
|
|
237
|
+
"--btn-primary-bg"
|
|
238
|
+
],
|
|
239
|
+
[
|
|
240
|
+
"--text-on-accent",
|
|
241
|
+
"--btn-primary-bg"
|
|
242
|
+
],
|
|
243
|
+
[
|
|
244
|
+
"--nav-active-text",
|
|
245
|
+
"--nav-active-bg"
|
|
246
|
+
],
|
|
247
|
+
[
|
|
248
|
+
"--tooltip-text",
|
|
249
|
+
"--tooltip-bg"
|
|
250
|
+
],
|
|
251
|
+
[
|
|
252
|
+
"--toast-info-text",
|
|
253
|
+
"--toast-info-bg"
|
|
254
|
+
],
|
|
255
|
+
[
|
|
256
|
+
"--toast-success-text",
|
|
257
|
+
"--toast-success-bg"
|
|
258
|
+
],
|
|
259
|
+
[
|
|
260
|
+
"--toast-warning-text",
|
|
261
|
+
"--toast-warning-bg"
|
|
262
|
+
],
|
|
263
|
+
[
|
|
264
|
+
"--toast-error-text",
|
|
265
|
+
"--toast-error-bg"
|
|
266
|
+
],
|
|
267
|
+
[
|
|
268
|
+
"--code-keyword",
|
|
269
|
+
"--surface-0"
|
|
270
|
+
],
|
|
271
|
+
[
|
|
272
|
+
"--code-string",
|
|
273
|
+
"--surface-0"
|
|
274
|
+
],
|
|
275
|
+
[
|
|
276
|
+
"--code-function",
|
|
277
|
+
"--surface-0"
|
|
278
|
+
],
|
|
279
|
+
[
|
|
280
|
+
"--code-number",
|
|
281
|
+
"--surface-0"
|
|
282
|
+
],
|
|
283
|
+
[
|
|
284
|
+
"--code-comment",
|
|
285
|
+
"--surface-0"
|
|
286
|
+
],
|
|
287
|
+
[
|
|
288
|
+
"--code-punctuation",
|
|
289
|
+
"--surface-0"
|
|
290
|
+
]
|
|
291
|
+
];
|
|
292
|
+
var BADGE_PAIRS = [
|
|
293
|
+
[
|
|
294
|
+
"--status-draft-text",
|
|
295
|
+
"--status-draft-bg"
|
|
296
|
+
],
|
|
297
|
+
[
|
|
298
|
+
"--status-published-text",
|
|
299
|
+
"--status-published-bg"
|
|
300
|
+
],
|
|
301
|
+
[
|
|
302
|
+
"--status-scheduled-text",
|
|
303
|
+
"--status-scheduled-bg"
|
|
304
|
+
],
|
|
305
|
+
[
|
|
306
|
+
"--status-archived-text",
|
|
307
|
+
"--status-archived-bg"
|
|
308
|
+
]
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
// src/validate.ts
|
|
312
|
+
function solidify(value, bg) {
|
|
313
|
+
if (value === void 0) return void 0;
|
|
314
|
+
const parsed = parseColor(value);
|
|
315
|
+
if (parsed && parsed[3] < 1) return alphaComposite(value, bg);
|
|
316
|
+
return value;
|
|
317
|
+
}
|
|
318
|
+
__name(solidify, "solidify");
|
|
319
|
+
function validateThemeAA(record) {
|
|
320
|
+
const bg = record["--bg"] ?? "#ffffff";
|
|
321
|
+
const failures = [];
|
|
322
|
+
const check = /* @__PURE__ */ __name((pairs, min) => {
|
|
323
|
+
for (const [fgTok, bgTok] of pairs) {
|
|
324
|
+
const fg = solidify(record[fgTok], bg);
|
|
325
|
+
const bgVal = solidify(record[bgTok], bg);
|
|
326
|
+
if (fg === void 0 || bgVal === void 0) continue;
|
|
327
|
+
const ratio = contrastRatio(fg, bgVal);
|
|
328
|
+
if (ratio === null) continue;
|
|
329
|
+
if (ratio < min) failures.push({
|
|
330
|
+
fg: fgTok,
|
|
331
|
+
bg: bgTok,
|
|
332
|
+
ratio,
|
|
333
|
+
min
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}, "check");
|
|
337
|
+
check(TEXT_PAIRS, 4.5);
|
|
338
|
+
check(BADGE_PAIRS, 3);
|
|
339
|
+
return {
|
|
340
|
+
ok: failures.length === 0,
|
|
341
|
+
failures
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
__name(validateThemeAA, "validateThemeAA");
|
|
345
|
+
|
|
346
|
+
// src/errors.ts
|
|
347
|
+
var ThemeAAError = class extends Error {
|
|
348
|
+
static {
|
|
349
|
+
__name(this, "ThemeAAError");
|
|
350
|
+
}
|
|
351
|
+
mode;
|
|
352
|
+
failures;
|
|
353
|
+
constructor(mode, failures) {
|
|
354
|
+
super(`Generated ${mode} theme failed WCAG AA on ${failures.length} pair(s): ` + failures.map((f) => `${f.fg} on ${f.bg} = ${f.ratio.toFixed(2)}:1 (needs ${f.min}:1)`).join("; ")), this.mode = mode, this.failures = failures;
|
|
355
|
+
this.name = "ThemeAAError";
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// src/generate.ts
|
|
360
|
+
var CANON_ACCENT = "#7c3aed";
|
|
361
|
+
var CANON_NEUTRAL = "#64748b";
|
|
362
|
+
var alpha = /* @__PURE__ */ __name((hex, a) => {
|
|
363
|
+
const p = parseColor(hex);
|
|
364
|
+
return `rgb(${p[0]} ${p[1]} ${p[2]} / ${a})`;
|
|
365
|
+
}, "alpha");
|
|
366
|
+
function deepTint(hex) {
|
|
367
|
+
return gamutClampToSrgb(0.2, 0.07, toOKLCH(hex).H);
|
|
368
|
+
}
|
|
369
|
+
__name(deepTint, "deepTint");
|
|
370
|
+
var ACCENT = {
|
|
371
|
+
light: {
|
|
372
|
+
"--text-accent": {
|
|
373
|
+
rung: 600
|
|
374
|
+
},
|
|
375
|
+
"--border-accent": {
|
|
376
|
+
rung: 200
|
|
377
|
+
},
|
|
378
|
+
"--focus-ring": {
|
|
379
|
+
fn: /* @__PURE__ */ __name((r) => alpha(r[600], "0.22"), "fn")
|
|
380
|
+
},
|
|
381
|
+
"--focus-border": {
|
|
382
|
+
rung: 600
|
|
383
|
+
},
|
|
384
|
+
"--btn-primary-bg": {
|
|
385
|
+
rung: 600
|
|
386
|
+
},
|
|
387
|
+
"--btn-primary-hover": {
|
|
388
|
+
rung: 700
|
|
389
|
+
},
|
|
390
|
+
"--nav-active-bg": {
|
|
391
|
+
rung: 50
|
|
392
|
+
},
|
|
393
|
+
"--nav-active-text": {
|
|
394
|
+
rung: 700
|
|
395
|
+
},
|
|
396
|
+
"--switch-track-on": {
|
|
397
|
+
rung: 600
|
|
398
|
+
},
|
|
399
|
+
"--shadow-btn-primary": {
|
|
400
|
+
fn: /* @__PURE__ */ __name((r) => `0 1px 2px 0 ${alpha(r[600], "0.25")}`, "fn")
|
|
401
|
+
},
|
|
402
|
+
"--status-scheduled-bg": {
|
|
403
|
+
rung: 50
|
|
404
|
+
},
|
|
405
|
+
"--status-scheduled-text": {
|
|
406
|
+
rung: 600
|
|
407
|
+
},
|
|
408
|
+
"--code-keyword": {
|
|
409
|
+
rung: 700
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
dark: {
|
|
413
|
+
"--text-accent": {
|
|
414
|
+
rung: 400
|
|
415
|
+
},
|
|
416
|
+
"--border-accent": {
|
|
417
|
+
fn: /* @__PURE__ */ __name((r) => alpha(r[400], "0.40"), "fn")
|
|
418
|
+
},
|
|
419
|
+
"--focus-ring": {
|
|
420
|
+
fn: /* @__PURE__ */ __name((r) => alpha(r[400], "0.28"), "fn")
|
|
421
|
+
},
|
|
422
|
+
"--focus-border": {
|
|
423
|
+
rung: 400
|
|
424
|
+
},
|
|
425
|
+
"--btn-primary-bg": {
|
|
426
|
+
rung: 600
|
|
427
|
+
},
|
|
428
|
+
"--btn-primary-hover": {
|
|
429
|
+
rung: 700
|
|
430
|
+
},
|
|
431
|
+
// Solid deep-tint (not the built-in's translucent alpha) so the generated theme also
|
|
432
|
+
// clears a raw-channel contrast guard — matching the shipped community themes, which use a
|
|
433
|
+
// solid dark nav-active-bg for exactly this reason (community-themes.ts). See validate.ts.
|
|
434
|
+
"--nav-active-bg": {
|
|
435
|
+
fn: /* @__PURE__ */ __name((r) => deepTint(r[600]), "fn")
|
|
436
|
+
},
|
|
437
|
+
"--switch-track-on": {
|
|
438
|
+
rung: 500
|
|
439
|
+
},
|
|
440
|
+
"--status-scheduled-bg": {
|
|
441
|
+
fn: /* @__PURE__ */ __name((r) => deepTint(r[600]), "fn")
|
|
442
|
+
},
|
|
443
|
+
"--status-scheduled-text": {
|
|
444
|
+
rung: 400
|
|
445
|
+
},
|
|
446
|
+
"--code-keyword": {
|
|
447
|
+
rung: 300
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
var NEUTRAL = {
|
|
452
|
+
light: {
|
|
453
|
+
"--bg": {
|
|
454
|
+
rung: 50
|
|
455
|
+
},
|
|
456
|
+
"--bg-subtle": {
|
|
457
|
+
rung: 100
|
|
458
|
+
},
|
|
459
|
+
"--surface-1": {
|
|
460
|
+
rung: 100
|
|
461
|
+
},
|
|
462
|
+
"--surface-2": {
|
|
463
|
+
rung: 200
|
|
464
|
+
},
|
|
465
|
+
"--surface-3": {
|
|
466
|
+
rung: 300
|
|
467
|
+
},
|
|
468
|
+
"--text-primary": {
|
|
469
|
+
rung: 900
|
|
470
|
+
},
|
|
471
|
+
"--text-secondary": {
|
|
472
|
+
rung: 600
|
|
473
|
+
},
|
|
474
|
+
"--text-muted": {
|
|
475
|
+
rung: 500
|
|
476
|
+
},
|
|
477
|
+
"--text-disabled": {
|
|
478
|
+
rung: 300
|
|
479
|
+
},
|
|
480
|
+
"--border": {
|
|
481
|
+
rung: 200
|
|
482
|
+
},
|
|
483
|
+
"--border-strong": {
|
|
484
|
+
rung: 400
|
|
485
|
+
},
|
|
486
|
+
"--switch-track-off": {
|
|
487
|
+
rung: 300
|
|
488
|
+
},
|
|
489
|
+
"--tooltip-bg": {
|
|
490
|
+
rung: 900
|
|
491
|
+
},
|
|
492
|
+
"--toast-info-bg": {
|
|
493
|
+
rung: 100
|
|
494
|
+
},
|
|
495
|
+
"--toast-info-text": {
|
|
496
|
+
rung: 700
|
|
497
|
+
},
|
|
498
|
+
"--status-archived-bg": {
|
|
499
|
+
rung: 100
|
|
500
|
+
},
|
|
501
|
+
"--status-archived-text": {
|
|
502
|
+
rung: 600
|
|
503
|
+
},
|
|
504
|
+
"--code-comment": {
|
|
505
|
+
rung: 500
|
|
506
|
+
},
|
|
507
|
+
"--code-punctuation": {
|
|
508
|
+
rung: 600
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
dark: {
|
|
512
|
+
"--bg": {
|
|
513
|
+
rung: 950
|
|
514
|
+
},
|
|
515
|
+
"--bg-subtle": {
|
|
516
|
+
rung: 900
|
|
517
|
+
},
|
|
518
|
+
"--surface-0": {
|
|
519
|
+
rung: 900
|
|
520
|
+
},
|
|
521
|
+
"--surface-1": {
|
|
522
|
+
rung: 800
|
|
523
|
+
},
|
|
524
|
+
"--surface-2": {
|
|
525
|
+
rung: 700
|
|
526
|
+
},
|
|
527
|
+
"--surface-3": {
|
|
528
|
+
rung: 600
|
|
529
|
+
},
|
|
530
|
+
"--text-primary": {
|
|
531
|
+
rung: 50
|
|
532
|
+
},
|
|
533
|
+
"--text-secondary": {
|
|
534
|
+
rung: 300
|
|
535
|
+
},
|
|
536
|
+
"--text-muted": {
|
|
537
|
+
rung: 400
|
|
538
|
+
},
|
|
539
|
+
"--text-disabled": {
|
|
540
|
+
rung: 600
|
|
541
|
+
},
|
|
542
|
+
"--border": {
|
|
543
|
+
rung: 700
|
|
544
|
+
},
|
|
545
|
+
"--border-strong": {
|
|
546
|
+
rung: 500
|
|
547
|
+
},
|
|
548
|
+
"--switch-track-off": {
|
|
549
|
+
rung: 600
|
|
550
|
+
},
|
|
551
|
+
"--tooltip-bg": {
|
|
552
|
+
rung: 700
|
|
553
|
+
},
|
|
554
|
+
"--toast-info-bg": {
|
|
555
|
+
rung: 800
|
|
556
|
+
},
|
|
557
|
+
"--toast-info-text": {
|
|
558
|
+
rung: 200
|
|
559
|
+
},
|
|
560
|
+
"--status-archived-bg": {
|
|
561
|
+
rung: 800
|
|
562
|
+
},
|
|
563
|
+
"--status-archived-text": {
|
|
564
|
+
rung: 400
|
|
565
|
+
},
|
|
566
|
+
"--code-comment": {
|
|
567
|
+
rung: 400
|
|
568
|
+
},
|
|
569
|
+
"--code-punctuation": {
|
|
570
|
+
rung: 300
|
|
571
|
+
},
|
|
572
|
+
"--nav-active-text": {
|
|
573
|
+
rung: 50
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
function applyTable(rec, ramp, table, prov) {
|
|
578
|
+
for (const key of Object.keys(table)) {
|
|
579
|
+
const recipe = table[key];
|
|
580
|
+
if ("rung" in recipe) {
|
|
581
|
+
rec[key] = ramp[recipe.rung];
|
|
582
|
+
if (prov) prov[key] = {
|
|
583
|
+
ramp,
|
|
584
|
+
idx: RUNGS.indexOf(recipe.rung)
|
|
585
|
+
};
|
|
586
|
+
} else {
|
|
587
|
+
rec[key] = recipe.fn(ramp);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
__name(applyTable, "applyTable");
|
|
592
|
+
function overrideAccent(rec, ramp, mode, prov) {
|
|
593
|
+
applyTable(rec, ramp, ACCENT[mode], prov);
|
|
594
|
+
}
|
|
595
|
+
__name(overrideAccent, "overrideAccent");
|
|
596
|
+
function overrideNeutral(rec, ramp, mode, prov) {
|
|
597
|
+
applyTable(rec, ramp, NEUTRAL[mode], prov);
|
|
598
|
+
}
|
|
599
|
+
__name(overrideNeutral, "overrideNeutral");
|
|
600
|
+
function anchorOf(curve, L) {
|
|
601
|
+
let best = 0;
|
|
602
|
+
for (let i = 1; i < curve.length; i++) {
|
|
603
|
+
if (Math.abs(curve[i] - L) < Math.abs(curve[best] - L)) best = i;
|
|
604
|
+
}
|
|
605
|
+
return best;
|
|
606
|
+
}
|
|
607
|
+
__name(anchorOf, "anchorOf");
|
|
608
|
+
function buildRamp(seedHex, Lcurve, Cenv, cap, vividness = 1) {
|
|
609
|
+
const { L: Ls, C: Cs, H: Hs } = toOKLCH(seedHex);
|
|
610
|
+
const anchor = anchorOf(Lcurve, Ls);
|
|
611
|
+
const kC = Cs / (Cenv[anchor] || 1e-6);
|
|
612
|
+
const ramp = {};
|
|
613
|
+
for (let i = 0; i < RUNGS.length; i++) {
|
|
614
|
+
const Li = Lcurve[i];
|
|
615
|
+
let Ci = Cenv[i] * kC * vividness;
|
|
616
|
+
if (cap) Ci = Math.min(Ci, cap[i]);
|
|
617
|
+
ramp[RUNGS[i]] = i === anchor && Math.abs(Li - Ls) < 0.02 ? seedHex : gamutClampToSrgb(Li, Ci, Hs);
|
|
618
|
+
}
|
|
619
|
+
return ramp;
|
|
620
|
+
}
|
|
621
|
+
__name(buildRamp, "buildRamp");
|
|
622
|
+
function nudgeFailing(rec, mode, prov, budget) {
|
|
623
|
+
const dir = mode === "light" ? 1 : -1;
|
|
624
|
+
const bg = rec["--bg"];
|
|
625
|
+
for (const [pairs, min] of [
|
|
626
|
+
[
|
|
627
|
+
TEXT_PAIRS,
|
|
628
|
+
4.5
|
|
629
|
+
],
|
|
630
|
+
[
|
|
631
|
+
BADGE_PAIRS,
|
|
632
|
+
3
|
|
633
|
+
]
|
|
634
|
+
]) {
|
|
635
|
+
for (const [fgTok, bgTok] of pairs) {
|
|
636
|
+
const p = prov[fgTok];
|
|
637
|
+
if (!p) continue;
|
|
638
|
+
for (let k = 0; k < budget; k++) {
|
|
639
|
+
const fg = solidify(rec[fgTok], bg);
|
|
640
|
+
const bgVal = solidify(rec[bgTok], bg);
|
|
641
|
+
const ratio = fg && bgVal ? contrastRatio(fg, bgVal) : null;
|
|
642
|
+
if (ratio === null || ratio >= min) break;
|
|
643
|
+
const idx = p.idx + dir;
|
|
644
|
+
if (idx < 0 || idx >= RUNGS.length) break;
|
|
645
|
+
p.idx = idx;
|
|
646
|
+
rec[fgTok] = p.ramp[RUNGS[idx]];
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
__name(nudgeFailing, "nudgeFailing");
|
|
652
|
+
var HUE_NAMES = [
|
|
653
|
+
[
|
|
654
|
+
15,
|
|
655
|
+
"red"
|
|
656
|
+
],
|
|
657
|
+
[
|
|
658
|
+
45,
|
|
659
|
+
"orange"
|
|
660
|
+
],
|
|
661
|
+
[
|
|
662
|
+
70,
|
|
663
|
+
"amber"
|
|
664
|
+
],
|
|
665
|
+
[
|
|
666
|
+
95,
|
|
667
|
+
"yellow"
|
|
668
|
+
],
|
|
669
|
+
[
|
|
670
|
+
135,
|
|
671
|
+
"lime"
|
|
672
|
+
],
|
|
673
|
+
[
|
|
674
|
+
165,
|
|
675
|
+
"green"
|
|
676
|
+
],
|
|
677
|
+
[
|
|
678
|
+
195,
|
|
679
|
+
"teal"
|
|
680
|
+
],
|
|
681
|
+
[
|
|
682
|
+
225,
|
|
683
|
+
"cyan"
|
|
684
|
+
],
|
|
685
|
+
[
|
|
686
|
+
265,
|
|
687
|
+
"blue"
|
|
688
|
+
],
|
|
689
|
+
[
|
|
690
|
+
295,
|
|
691
|
+
"indigo"
|
|
692
|
+
],
|
|
693
|
+
[
|
|
694
|
+
330,
|
|
695
|
+
"violet"
|
|
696
|
+
],
|
|
697
|
+
[
|
|
698
|
+
350,
|
|
699
|
+
"magenta"
|
|
700
|
+
],
|
|
701
|
+
[
|
|
702
|
+
360,
|
|
703
|
+
"red"
|
|
704
|
+
]
|
|
705
|
+
];
|
|
706
|
+
var hueName = /* @__PURE__ */ __name((h) => HUE_NAMES.find(([max]) => h < max)?.[1] ?? "custom", "hueName");
|
|
707
|
+
var clampVividness = /* @__PURE__ */ __name((v) => v === void 0 ? 1 : Math.min(1.5, Math.max(0.5, v)), "clampVividness");
|
|
708
|
+
function build(seed = {}, opts = {}) {
|
|
709
|
+
const nudge = opts.nudge !== false;
|
|
710
|
+
const vividness = clampVividness(seed.vividness);
|
|
711
|
+
const warnings = [];
|
|
712
|
+
const accentHex = seed.accent === void 0 ? CANON_ACCENT : normalizeHex(seed.accent);
|
|
713
|
+
if (accentHex === null) throw new Error(`Invalid accent colour: "${seed.accent}"`);
|
|
714
|
+
const neutralHex = seed.neutral === void 0 ? CANON_NEUTRAL : normalizeHex(seed.neutral);
|
|
715
|
+
if (neutralHex === null) throw new Error(`Invalid neutral colour: "${seed.neutral}"`);
|
|
716
|
+
const accentCustom = accentHex !== CANON_ACCENT;
|
|
717
|
+
const neutralCustom = neutralHex !== CANON_NEUTRAL;
|
|
718
|
+
const light = {
|
|
719
|
+
...BASE.light
|
|
720
|
+
};
|
|
721
|
+
const dark = {
|
|
722
|
+
...BASE.dark
|
|
723
|
+
};
|
|
724
|
+
const provLight = {};
|
|
725
|
+
const provDark = {};
|
|
726
|
+
if (accentCustom) {
|
|
727
|
+
const A = buildRamp(accentHex, L_ACCENT_CURVE, CHROMA_ENV, void 0, vividness);
|
|
728
|
+
overrideAccent(light, A, "light", provLight);
|
|
729
|
+
overrideAccent(dark, A, "dark", provDark);
|
|
730
|
+
const { L: Ls, C: Cs } = toOKLCH(accentHex);
|
|
731
|
+
const drift = Math.abs(RUNGS.indexOf(600) - anchorOf(L_ACCENT_CURVE, Ls));
|
|
732
|
+
if (drift > 2) {
|
|
733
|
+
warnings.push(`Accent ${accentHex} sits ${drift} rungs from the accessible accent lightness; it was adjusted to meet contrast, so the rendered accent is darker/lighter than the seed.`);
|
|
734
|
+
}
|
|
735
|
+
if (Cs < 0.04) {
|
|
736
|
+
warnings.push(`Accent ${accentHex} has very low chroma and will read close to neutral.`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (neutralCustom) {
|
|
740
|
+
const N = buildRamp(neutralHex, L_NEUTRAL_CURVE, C_NEUTRAL_CAP, C_NEUTRAL_CAP, vividness);
|
|
741
|
+
overrideNeutral(light, N, "light", provLight);
|
|
742
|
+
overrideNeutral(dark, N, "dark", provDark);
|
|
743
|
+
}
|
|
744
|
+
for (const [rec, mode, prov] of [
|
|
745
|
+
[
|
|
746
|
+
light,
|
|
747
|
+
"light",
|
|
748
|
+
provLight
|
|
749
|
+
],
|
|
750
|
+
[
|
|
751
|
+
dark,
|
|
752
|
+
"dark",
|
|
753
|
+
provDark
|
|
754
|
+
]
|
|
755
|
+
]) {
|
|
756
|
+
if (nudge) nudgeFailing(rec, mode, prov, 2);
|
|
757
|
+
const result = validateThemeAA(rec);
|
|
758
|
+
if (!result.ok) throw new ThemeAAError(mode, result.failures);
|
|
759
|
+
}
|
|
760
|
+
const name = seed.name ?? (accentCustom ? hueName(toOKLCH(accentHex).H) : "rhombus");
|
|
761
|
+
const label = seed.label ?? name.charAt(0).toUpperCase() + name.slice(1);
|
|
762
|
+
return {
|
|
763
|
+
name,
|
|
764
|
+
label,
|
|
765
|
+
light,
|
|
766
|
+
dark,
|
|
767
|
+
report: {
|
|
768
|
+
warnings
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
__name(build, "build");
|
|
773
|
+
|
|
774
|
+
// src/serialize.ts
|
|
775
|
+
function block(name, values) {
|
|
776
|
+
const lines = CONTRACT_KEYS.map((key) => ` ${key}: ${values[key]};`).join("\n");
|
|
777
|
+
return `[data-theme="${name}"] {
|
|
778
|
+
${lines}
|
|
779
|
+
}`;
|
|
780
|
+
}
|
|
781
|
+
__name(block, "block");
|
|
782
|
+
function serializeThemeCss(theme) {
|
|
783
|
+
return `${block(theme.name, theme.light)}
|
|
784
|
+
|
|
785
|
+
${block(`${theme.name}-dark`, theme.dark)}
|
|
786
|
+
`;
|
|
787
|
+
}
|
|
788
|
+
__name(serializeThemeCss, "serializeThemeCss");
|
|
789
|
+
function toRegisteredThemes(theme) {
|
|
790
|
+
return [
|
|
791
|
+
{
|
|
792
|
+
name: theme.name,
|
|
793
|
+
label: theme.label,
|
|
794
|
+
mode: "light",
|
|
795
|
+
palette: theme.name
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
name: `${theme.name}-dark`,
|
|
799
|
+
label: theme.label,
|
|
800
|
+
mode: "dark",
|
|
801
|
+
palette: theme.name
|
|
802
|
+
}
|
|
803
|
+
];
|
|
804
|
+
}
|
|
805
|
+
__name(toRegisteredThemes, "toRegisteredThemes");
|
|
806
|
+
function toAugmentation(theme) {
|
|
807
|
+
return `declare module '@rhombuskit/theme-engine' {
|
|
808
|
+
interface ThemeRegistry {
|
|
809
|
+
'${theme.name}': true;
|
|
810
|
+
'${theme.name}-dark': true;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
`;
|
|
814
|
+
}
|
|
815
|
+
__name(toAugmentation, "toAugmentation");
|
|
816
|
+
export {
|
|
817
|
+
ThemeAAError,
|
|
818
|
+
alphaComposite,
|
|
819
|
+
buildRamp,
|
|
820
|
+
contrastRatio,
|
|
821
|
+
deepTint,
|
|
822
|
+
fromOKLCH,
|
|
823
|
+
gamutClampToSrgb,
|
|
824
|
+
build as generateTheme,
|
|
825
|
+
normalizeHex,
|
|
826
|
+
parseColor,
|
|
827
|
+
relativeLuminance,
|
|
828
|
+
serializeThemeCss,
|
|
829
|
+
toAugmentation,
|
|
830
|
+
toOKLCH,
|
|
831
|
+
toRegisteredThemes,
|
|
832
|
+
validateThemeAA
|
|
833
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rhombuskit/theme-builder",
|
|
3
|
+
"version": "1.15.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Generate WCAG-AA-validated RhombusKit token-override themes from a few seed colours — a framework-agnostic seed→theme generator that emits both light and dark packs over the full 60-token CONTRACT.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"design-tokens",
|
|
8
|
+
"theming",
|
|
9
|
+
"color",
|
|
10
|
+
"oklch",
|
|
11
|
+
"wcag",
|
|
12
|
+
"accessibility",
|
|
13
|
+
"design-system",
|
|
14
|
+
"rhombuskit"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/doug-williamson/RhombusKit.git",
|
|
22
|
+
"directory": "packages/theme-builder"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://rhombuskit.online",
|
|
25
|
+
"funding": "https://github.com/sponsors/doug-williamson",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/doug-williamson/RhombusKit/issues"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"import": "./dist/index.mjs",
|
|
33
|
+
"default": "./dist/index.mjs"
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"main": "./dist/index.mjs",
|
|
38
|
+
"types": "./dist/index.d.mts",
|
|
39
|
+
"files": [
|
|
40
|
+
"dist"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@rhombuskit/tokens": "1.15.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup"
|
|
47
|
+
}
|
|
48
|
+
}
|