@crossworks/share-ui 0.230.43
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.md +135 -0
- package/package.json +68 -0
- package/src/app-bridge-protocol.ts +115 -0
- package/src/app-presenter.tsx +25 -0
- package/src/app-sandbox.tsx +552 -0
- package/src/appearance.ts +192 -0
- package/src/avatar.test.ts +229 -0
- package/src/avatar.ts +731 -0
- package/src/backgrounds.test.ts +119 -0
- package/src/backgrounds.ts +118 -0
- package/src/draw-presenter.tsx +39 -0
- package/src/event-presenter.tsx +62 -0
- package/src/file-presenter.tsx +76 -0
- package/src/formula-calculator.tsx +209 -0
- package/src/formula-presenter.test.ts +128 -0
- package/src/formula-presenter.tsx +301 -0
- package/src/help-topics.ts +104 -0
- package/src/lib/ink-audit.test.ts +314 -0
- package/src/lib/theme-css-blocks.ts +26 -0
- package/src/lib/theme-generator.test.ts +179 -0
- package/src/lib/theme-registry.gen.ts +352 -0
- package/src/lib/themes.test.ts +308 -0
- package/src/lib/themes.ts +75 -0
- package/src/lib/utils.ts +6 -0
- package/src/nav-items.ts +225 -0
- package/src/note-presenter.tsx +14 -0
- package/src/page-outline.tsx +127 -0
- package/src/table-presenter.tsx +226 -0
- package/src/task-presenter.tsx +60 -0
- package/src/ui/button.tsx +50 -0
- package/src/ui/input.tsx +18 -0
- package/src/ui/label.tsx +20 -0
- package/src/view-payload.ts +82 -0
- package/styles/app.css +1098 -0
- package/styles/themes.css +6198 -0
- package/themes/generate.d.mts +11 -0
- package/themes/generate.mjs +618 -0
- package/themes/model.d.mts +24 -0
- package/themes/model.mjs +213 -0
- package/themes/preview.html +145 -0
- package/themes/seeds.d.mts +16 -0
- package/themes/seeds.mjs +3694 -0
- package/tsconfig.json +15 -0
- package/tsconfig.tsbuildinfo +1 -0
package/themes/model.mjs
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colour model for the theme generator — OKLab/OKLCH math, WCAG contrast, and
|
|
3
|
+
* the anchored AA solver.
|
|
4
|
+
*
|
|
5
|
+
* THE PRINCIPLE. A theme's identity is its surfaces and fills; text colour is a
|
|
6
|
+
* pure function of (hue, surface, target ratio). So every text token is SOLVED,
|
|
7
|
+
* not authored: keep the anchor's hue and chroma (the theme's identity), scan
|
|
8
|
+
* lightness outward from the anchor in small steps, and take the first value
|
|
9
|
+
* that clears the required ratio against every surface it must sit on. The
|
|
10
|
+
* anchor is the authored value wherever one exists, so a token that already
|
|
11
|
+
* passes is emitted byte-for-byte unchanged, and one that fails moves the
|
|
12
|
+
* minimum perceptible amount. Measured across the 172 ink tokens shipped in
|
|
13
|
+
* v0.206.12: 0 infeasible, max |ΔL| 0.43, most far smaller.
|
|
14
|
+
*
|
|
15
|
+
* TRAP (cost a real session): validate the ROUNDED 8-BIT HEX you will emit,
|
|
16
|
+
* not the float candidate. Values that clear 4.5:1 in float land at 4.483 once
|
|
17
|
+
* written as hex. Every solver in this file round-trips through `toHex` before
|
|
18
|
+
* measuring.
|
|
19
|
+
*
|
|
20
|
+
* The sRGB<->OKLab matrices mirror the ones proven in
|
|
21
|
+
* `src/lib/themes.test.ts`; the test recomputes every pair independently, so a
|
|
22
|
+
* transcription error here fails CI rather than shipping.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
26
|
+
const srgbToLin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
27
|
+
const linToSrgb = (c) => (c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055);
|
|
28
|
+
|
|
29
|
+
/** Any notation used in theme sources -> sRGB [r,g,b] in 0..1 (gamut-clamped,
|
|
30
|
+
* which is what a browser renders for out-of-gamut oklch()). */
|
|
31
|
+
export function parseColor(value) {
|
|
32
|
+
const v = value.trim();
|
|
33
|
+
if (v.startsWith('#')) {
|
|
34
|
+
let h = v.slice(1);
|
|
35
|
+
if (h.length === 3) h = [...h].map((c) => c + c).join('');
|
|
36
|
+
if (h.length === 8) h = h.slice(0, 6);
|
|
37
|
+
if (!/^[0-9a-f]{6}$/i.test(h)) throw new Error(`bad hex: ${value}`);
|
|
38
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
|
|
39
|
+
}
|
|
40
|
+
const args = v
|
|
41
|
+
.slice(v.indexOf('(') + 1, v.lastIndexOf(')'))
|
|
42
|
+
.split('/')[0]
|
|
43
|
+
.trim()
|
|
44
|
+
.split(/[\s,]+/);
|
|
45
|
+
if (v.startsWith('oklch')) {
|
|
46
|
+
const L = args[0].endsWith('%') ? parseFloat(args[0]) / 100 : parseFloat(args[0]);
|
|
47
|
+
const C = args[1].endsWith('%') ? (parseFloat(args[1]) / 100) * 0.4 : parseFloat(args[1]);
|
|
48
|
+
const H = parseFloat(args[2] ?? '0') || 0;
|
|
49
|
+
return oklchToSrgb([L, C, H]);
|
|
50
|
+
}
|
|
51
|
+
if (v.startsWith('hsl')) {
|
|
52
|
+
const [h, sPct, lPct] = args.map(parseFloat);
|
|
53
|
+
const [S, L] = [sPct / 100, lPct / 100];
|
|
54
|
+
const k = (n) => (n + h / 30) % 12;
|
|
55
|
+
const a = S * Math.min(L, 1 - L);
|
|
56
|
+
const f = (n) => L - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
57
|
+
return [f(0), f(8), f(4)];
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`unsupported colour notation: ${value}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** OKLCH [L, C, H°] -> sRGB 0..1, per-channel gamut clamp. */
|
|
63
|
+
export function oklchToSrgb([L, C, Hdeg]) {
|
|
64
|
+
const H = (Hdeg * Math.PI) / 180;
|
|
65
|
+
const [a, b] = [C * Math.cos(H), C * Math.sin(H)];
|
|
66
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
67
|
+
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
68
|
+
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
69
|
+
return [
|
|
70
|
+
clamp01(linToSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s)),
|
|
71
|
+
clamp01(linToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s)),
|
|
72
|
+
clamp01(linToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s)),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** sRGB 0..1 -> OKLab [L, a, b]. */
|
|
77
|
+
export function srgbToOklab([r, g, b]) {
|
|
78
|
+
const [lr, lg, lb] = [r, g, b].map(srgbToLin);
|
|
79
|
+
const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
|
|
80
|
+
const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
|
|
81
|
+
const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
|
|
82
|
+
return [
|
|
83
|
+
0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
84
|
+
1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
85
|
+
0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** sRGB 0..1 -> OKLCH [L, C, H°]. */
|
|
90
|
+
export function srgbToOklch(rgb) {
|
|
91
|
+
const [L, a, b] = srgbToOklab(rgb);
|
|
92
|
+
const C = Math.hypot(a, b);
|
|
93
|
+
let H = (Math.atan2(b, a) * 180) / Math.PI;
|
|
94
|
+
if (H < 0) H += 360;
|
|
95
|
+
return [L, C, H];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const parseOklch = (css) => srgbToOklch(parseColor(css));
|
|
99
|
+
|
|
100
|
+
export function toHex(rgb) {
|
|
101
|
+
return (
|
|
102
|
+
'#' +
|
|
103
|
+
rgb
|
|
104
|
+
.map((c) =>
|
|
105
|
+
Math.round(clamp01(c) * 255)
|
|
106
|
+
.toString(16)
|
|
107
|
+
.padStart(2, '0'),
|
|
108
|
+
)
|
|
109
|
+
.join('')
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Normalise any supported notation to the 6-digit hex a browser renders. */
|
|
114
|
+
export const cssToHex = (css) => toHex(parseColor(css));
|
|
115
|
+
|
|
116
|
+
export const luminance = ([r, g, b]) => {
|
|
117
|
+
const [lr, lg, lb] = [r, g, b].map(srgbToLin);
|
|
118
|
+
return 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** WCAG 2.1 contrast between two CSS values, measured AS RENDERED — both sides
|
|
122
|
+
* are first snapped to the 8-bit hex the browser will actually paint. */
|
|
123
|
+
export function contrast(cssA, cssB) {
|
|
124
|
+
const [a, b] = [luminance(parseColor(cssToHex(cssA))), luminance(parseColor(cssToHex(cssB)))];
|
|
125
|
+
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Perceptual distance in OKLab. Rule of thumb: <0.01 imperceptible, <0.02
|
|
129
|
+
* barely noticeable side by side, >0.05 clearly a different colour. */
|
|
130
|
+
export function deltaE(cssA, cssB) {
|
|
131
|
+
const [l1, a1, b1] = srgbToOklab(parseColor(cssA));
|
|
132
|
+
const [l2, a2, b2] = srgbToOklab(parseColor(cssB));
|
|
133
|
+
return Math.hypot(l1 - l2, a1 - a2, b1 - b2);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function hueDistance(a, b) {
|
|
137
|
+
const d = Math.abs(a - b) % 360;
|
|
138
|
+
return d > 180 ? 360 - d : d;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const L_STEP = 0.0025;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The anchored AA solver.
|
|
145
|
+
*
|
|
146
|
+
* Finds the colour nearest the anchor (in lightness, then chroma) whose EMITTED
|
|
147
|
+
* HEX clears `ratio` against every surface in `against`. Returns
|
|
148
|
+
* `{ hex, dL, dC, feasible }`; `feasible: false` only when even the achromatic
|
|
149
|
+
* extremes fail, which for AA text against real surfaces does not happen —
|
|
150
|
+
* pure black or white clears 4.5:1 against anything a UI uses as a surface.
|
|
151
|
+
*
|
|
152
|
+
* Order of preference:
|
|
153
|
+
* 1. anchor as-is (a passing authored value is emitted unchanged),
|
|
154
|
+
* 2. lightness moved outward, smallest |ΔL| first (hue + chroma untouched),
|
|
155
|
+
* 3. only then chroma reduced (needed for 2 of 172 tokens in the ink split).
|
|
156
|
+
*/
|
|
157
|
+
export function solveText(anchorCss, against, { ratio = 4.5 } = {}) {
|
|
158
|
+
const [L0, C0, H] = parseOklch(anchorCss);
|
|
159
|
+
const surfaces = against.map(cssToHex);
|
|
160
|
+
const passes = (hex) => surfaces.every((s) => contrast(hex, s) >= ratio);
|
|
161
|
+
|
|
162
|
+
for (let scale = 1; scale >= -1e-9; scale -= 0.1) {
|
|
163
|
+
const C = C0 * Math.max(0, scale);
|
|
164
|
+
// Scan L outward from the anchor, both directions, nearest first.
|
|
165
|
+
for (let i = 0; ; i++) {
|
|
166
|
+
const up = L0 + i * L_STEP;
|
|
167
|
+
const down = L0 - i * L_STEP;
|
|
168
|
+
if (up > 1 + L_STEP && down < -L_STEP) break;
|
|
169
|
+
for (const L of i === 0 ? [L0] : [down, up]) {
|
|
170
|
+
if (L < 0 || L > 1) continue;
|
|
171
|
+
const hex = toHex(oklchToSrgb([L, C, H]));
|
|
172
|
+
if (passes(hex)) {
|
|
173
|
+
return { hex, dL: L - L0, dC: C - C0, feasible: true };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { hex: toHex(oklchToSrgb([L0, C0, H])), dL: 0, dC: 0, feasible: false };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Joint solve for a fill and its own -foreground.
|
|
183
|
+
*
|
|
184
|
+
* WHY THE FILL MAY MOVE AT ALL. A white foreground on a mid-saturation brand
|
|
185
|
+
* fill (3.9:1, the most common authored defect) has no light value that clears
|
|
186
|
+
* 4.5:1 — the nearest one-sided fix is near-black text, an identity flip far
|
|
187
|
+
* louder than deepening the fill a step. The v0.206.7 accent repair set the
|
|
188
|
+
* precedent by hand: "move the LIGHTNESS of whichever of the two shifts
|
|
189
|
+
* least". This does that mechanically: scan fill ΔL outward (hue and chroma
|
|
190
|
+
* pinned), give the foreground a minimal solve against each candidate, and
|
|
191
|
+
* take the pair with the least total movement — fill weighted heavier, since a
|
|
192
|
+
* fill repaints every component wearing it. A pair that already passes costs 0
|
|
193
|
+
* and ships byte-for-byte as authored.
|
|
194
|
+
*/
|
|
195
|
+
export function solvePair(fillCss, fgAnchorCss, { ratio = 4.5, fillWeight = 1.5 } = {}) {
|
|
196
|
+
const [fL, fC, fH] = parseOklch(fillCss);
|
|
197
|
+
let best = null;
|
|
198
|
+
for (let i = 0; i * L_STEP * 2 <= 1; i++) {
|
|
199
|
+
const dLf = i * L_STEP * 2;
|
|
200
|
+
if (best && fillWeight * dLf >= best.cost) break; // no candidate can win
|
|
201
|
+
for (const sign of i === 0 ? [0] : [-1, 1]) {
|
|
202
|
+
const L = fL + sign * dLf;
|
|
203
|
+
if (L < 0 || L > 1) continue;
|
|
204
|
+
const fillHex = toHex(oklchToSrgb([L, fC, fH]));
|
|
205
|
+
const fg = solveText(fgAnchorCss, [fillHex], { ratio });
|
|
206
|
+
if (!fg.feasible) continue;
|
|
207
|
+
const cost = fillWeight * dLf + Math.abs(fg.dL) + Math.abs(fg.dC);
|
|
208
|
+
if (!best || cost < best.cost) best = { fill: fillHex, fg: fg.hex, cost };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!best) throw new Error(`no feasible pair for fill ${fillCss}`);
|
|
212
|
+
return best;
|
|
213
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!-- Theme preview harness — renders the GENERATED themes.css with zero app
|
|
3
|
+
code, so a seeds/generator change can be eyeballed across all themes and
|
|
4
|
+
both modes before it ships. Serve the package directory statically
|
|
5
|
+
(e.g. `python3 -m http.server -d packages/web-ui 4173`) and open
|
|
6
|
+
/themes/preview.html. Nothing here ships to a browser in production. -->
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="utf-8" />
|
|
10
|
+
<title>Mantle theme preview</title>
|
|
11
|
+
<link rel="stylesheet" href="../styles/themes.css" />
|
|
12
|
+
<style>
|
|
13
|
+
html { font-family: var(--font-sans, Inter, sans-serif); }
|
|
14
|
+
body { margin: 0; background: var(--background); color: var(--foreground); transition: background 0.15s; }
|
|
15
|
+
.bar { display: flex; gap: 0.75rem; align-items: center; padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--card); z-index: 2; flex-wrap: wrap; }
|
|
16
|
+
main { padding: 1rem; display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }
|
|
17
|
+
section { border: 1px solid var(--border); border-radius: var(--radius, 8px); background: var(--card); color: var(--card-foreground); padding: 1rem; }
|
|
18
|
+
h2 { margin: 0 0 0.6rem; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted-foreground); }
|
|
19
|
+
.row { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
|
|
20
|
+
.btn { border: 0; border-radius: var(--radius, 8px); padding: 0.45rem 0.9rem; font-weight: 600; font-size: 0.85rem; }
|
|
21
|
+
.chip { border-radius: 999px; padding: 0.2rem 0.7rem; font-size: 0.75rem; font-weight: 600; }
|
|
22
|
+
.surface { border-radius: var(--radius, 8px); padding: 0.6rem 0.8rem; font-size: 0.85rem; }
|
|
23
|
+
pre { background: var(--muted); color: var(--foreground); border-radius: var(--radius, 8px); padding: 0.8rem 1rem; font-family: var(--font-mono, monospace); font-size: 0.8rem; line-height: 1.5; overflow-x: auto; }
|
|
24
|
+
.callout { border-left: 3px solid; border-radius: var(--radius, 8px); padding: 0.5rem 0.9rem; font-size: 0.85rem; margin-bottom: 0.5rem; }
|
|
25
|
+
.swatches { display: flex; height: 2rem; border-radius: var(--radius, 8px); overflow: hidden; }
|
|
26
|
+
.swatches div { flex: 1; }
|
|
27
|
+
.inks span { display: block; font-size: 0.85rem; padding: 0.1rem 0; }
|
|
28
|
+
select, .toggle { font: inherit; padding: 0.3rem 0.5rem; border-radius: 6px; border: 1px solid var(--border); background: var(--background); color: var(--foreground); }
|
|
29
|
+
a { color: var(--primary-ink); font-weight: 500; text-decoration: none; }
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<div class="bar">
|
|
34
|
+
<select id="theme"></select>
|
|
35
|
+
<button class="toggle" id="mode">dark ⇄ light</button>
|
|
36
|
+
<span id="label" style="font-size:0.8rem;color:var(--muted-foreground)"></span>
|
|
37
|
+
</div>
|
|
38
|
+
<main>
|
|
39
|
+
<section>
|
|
40
|
+
<h2>Fills + on-fill text</h2>
|
|
41
|
+
<div class="row">
|
|
42
|
+
<button class="btn" style="background:var(--primary);color:var(--primary-foreground)">Primary</button>
|
|
43
|
+
<button class="btn" style="background:var(--secondary);color:var(--secondary-foreground)">Secondary</button>
|
|
44
|
+
<button class="btn" style="background:var(--accent);color:var(--accent-foreground)">Accent</button>
|
|
45
|
+
<button class="btn" style="background:var(--destructive);color:var(--destructive-foreground)">Delete</button>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="row">
|
|
48
|
+
<button class="btn" style="background:var(--success);color:var(--success-foreground)">Success</button>
|
|
49
|
+
<button class="btn" style="background:var(--warning);color:var(--warning-foreground)">Warning</button>
|
|
50
|
+
<button class="btn" style="background:var(--info);color:var(--info-foreground)">Info</button>
|
|
51
|
+
</div>
|
|
52
|
+
<h2>Inks on surfaces</h2>
|
|
53
|
+
<div class="row">
|
|
54
|
+
<div class="surface inks" style="background:var(--background);border:1px solid var(--border)">
|
|
55
|
+
<span style="color:var(--primary-ink)">primary-ink</span>
|
|
56
|
+
<span style="color:var(--destructive-ink)">destructive-ink</span>
|
|
57
|
+
<span style="color:var(--success-ink)">success-ink</span>
|
|
58
|
+
<span style="color:var(--warning-ink)">warning-ink</span>
|
|
59
|
+
<span style="color:var(--info-ink)">info-ink</span>
|
|
60
|
+
<span style="color:var(--muted-foreground)">muted-foreground</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="surface inks" style="background:var(--muted)">
|
|
63
|
+
<span style="color:var(--primary-ink)">primary-ink</span>
|
|
64
|
+
<span style="color:var(--destructive-ink)">destructive-ink</span>
|
|
65
|
+
<span style="color:var(--success-ink)">success-ink</span>
|
|
66
|
+
<span style="color:var(--warning-ink)">warning-ink</span>
|
|
67
|
+
<span style="color:var(--info-ink)">info-ink</span>
|
|
68
|
+
<span style="color:var(--muted-foreground)">muted-foreground</span>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="surface inks" style="background:var(--sidebar);color:var(--sidebar-foreground)">
|
|
71
|
+
<span>sidebar-foreground</span>
|
|
72
|
+
<span style="color:var(--primary-ink)">primary-ink</span>
|
|
73
|
+
<span style="color:var(--muted-foreground)">muted-foreground</span>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
77
|
+
<section>
|
|
78
|
+
<h2>Code (the --code-* palette on --muted)</h2>
|
|
79
|
+
<pre><span style="color:var(--code-keyword)">export function</span> <span style="color:var(--code-title)">solveInk</span>(<span style="color:var(--code-variable)">anchor</span>, <span style="color:var(--code-variable)">surfaces</span>) {
|
|
80
|
+
<span style="color:var(--code-keyword)">const</span> <span style="color:var(--code-variable)">ratio</span> = <span style="color:var(--code-number)">4.5</span>; <span style="color:var(--muted-foreground);font-style:italic">// AA, measured on the emitted hex</span>
|
|
81
|
+
<span style="color:var(--code-keyword)">return</span> <span style="color:var(--code-title)">scan</span>(<span style="color:var(--code-string)">'lightness'</span>, <span style="color:var(--code-variable)">anchor</span>);
|
|
82
|
+
}</pre>
|
|
83
|
+
<h2>Callouts</h2>
|
|
84
|
+
<div class="callout" style="border-color:var(--info);background:color-mix(in oklab, var(--info) 8%, transparent)">ℹ️ Info — border + 8% tint from <b>--info</b>.</div>
|
|
85
|
+
<div class="callout" style="border-color:var(--success);background:color-mix(in oklab, var(--success) 8%, transparent)">✅ Success — the role, not chart-4.</div>
|
|
86
|
+
<div class="callout" style="border-color:var(--warning);background:color-mix(in oklab, var(--warning) 8%, transparent)">⚠️ Warning — the role, not chart-5.</div>
|
|
87
|
+
<div class="callout" style="border-color:var(--destructive);background:color-mix(in oklab, var(--destructive) 8%, transparent)">🛑 Danger — destructive, as before.</div>
|
|
88
|
+
</section>
|
|
89
|
+
<section>
|
|
90
|
+
<h2>Charts (categorical)</h2>
|
|
91
|
+
<div class="swatches" id="charts"></div>
|
|
92
|
+
<h2>Prose accents</h2>
|
|
93
|
+
<div style="color:var(--primary-ink);font-size:1.3rem;font-weight:700">Heading (primary-ink)</div>
|
|
94
|
+
<div style="color:var(--info-ink);font-weight:600">h4 accent (info-ink)</div>
|
|
95
|
+
<p style="margin:0.4rem 0">Body text with <a href="#">a themed link</a> and a <span style="border-radius:0.3rem;background:color-mix(in oklab, var(--info) 16%, transparent);color:var(--info-ink);padding:0.05em 0.3em;font-weight:500">@node mention</span>.</p>
|
|
96
|
+
<h2>All themes</h2>
|
|
97
|
+
<div id="grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:0.4rem"></div>
|
|
98
|
+
</section>
|
|
99
|
+
</main>
|
|
100
|
+
<script type="module">
|
|
101
|
+
// The registry is TS; scrape ids straight from the stylesheet instead so the
|
|
102
|
+
// harness has zero build steps.
|
|
103
|
+
const css = await (await fetch('../styles/themes.css')).text();
|
|
104
|
+
const ids = ['clean-slate', ...new Set([...css.matchAll(/^\[data-color-theme="([^"]+)"\]/gm)].map((m) => m[1]))];
|
|
105
|
+
const sel = document.getElementById('theme');
|
|
106
|
+
for (const id of ids) sel.add(new Option(id, id));
|
|
107
|
+
const apply = () => {
|
|
108
|
+
const id = sel.value;
|
|
109
|
+
const root = document.documentElement;
|
|
110
|
+
if (id === 'clean-slate') delete root.dataset.colorTheme;
|
|
111
|
+
else root.dataset.colorTheme = id;
|
|
112
|
+
document.getElementById('label').textContent =
|
|
113
|
+
`${id} — ${root.classList.contains('dark') ? 'dark' : 'light'}`;
|
|
114
|
+
const charts = document.getElementById('charts');
|
|
115
|
+
charts.innerHTML = '';
|
|
116
|
+
for (let n = 1; n <= 5; n++) {
|
|
117
|
+
const d = document.createElement('div');
|
|
118
|
+
d.style.background = `var(--chart-${n})`;
|
|
119
|
+
charts.appendChild(d);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
sel.onchange = apply;
|
|
123
|
+
document.getElementById('mode').onclick = () => {
|
|
124
|
+
document.documentElement.classList.toggle('dark');
|
|
125
|
+
apply();
|
|
126
|
+
};
|
|
127
|
+
const grid = document.getElementById('grid');
|
|
128
|
+
for (const id of ids) {
|
|
129
|
+
const card = document.createElement('button');
|
|
130
|
+
card.style.cssText = 'border:1px solid var(--border);border-radius:6px;padding:0.3rem;background:var(--card);color:var(--card-foreground);cursor:pointer;font-size:0.65rem';
|
|
131
|
+
card.dataset.colorTheme = id === 'clean-slate' ? '' : id;
|
|
132
|
+
if (id !== 'clean-slate') card.setAttribute('data-color-theme', id);
|
|
133
|
+
card.innerHTML = `<div style="display:flex;height:1.1rem;border-radius:4px;overflow:hidden">
|
|
134
|
+
<div style="flex:1;background:var(--primary)"></div>
|
|
135
|
+
<div style="flex:1;background:var(--accent)"></div>
|
|
136
|
+
<div style="flex:1;background:var(--success)"></div>
|
|
137
|
+
<div style="flex:1;background:var(--warning)"></div>
|
|
138
|
+
<div style="flex:1;background:var(--info)"></div></div>${id}`;
|
|
139
|
+
card.onclick = () => { sel.value = id; apply(); };
|
|
140
|
+
grid.appendChild(card);
|
|
141
|
+
}
|
|
142
|
+
apply();
|
|
143
|
+
</script>
|
|
144
|
+
</body>
|
|
145
|
+
</html>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Hand-written declarations for seeds.mjs. */
|
|
2
|
+
export type ThemeModeSeed = {
|
|
3
|
+
charts?: string[];
|
|
4
|
+
extras?: Record<string, string>;
|
|
5
|
+
} & {
|
|
6
|
+
[token: string]: string | string[] | Record<string, string> | undefined;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export interface ThemeSeed {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
light: ThemeModeSeed;
|
|
13
|
+
dark: ThemeModeSeed;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const THEME_SEEDS: ThemeSeed[];
|