@omniharmonic/openspace-theme 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2342 @@
1
+ // src/color.ts
2
+ var GAMUT_EPSILON = 1e-6;
3
+ var ACHROMATIC_CHROMA = 1e-4;
4
+ function srgbChannelToLinear(v) {
5
+ const abs = Math.abs(v);
6
+ const lin = abs <= 0.04045 ? abs / 12.92 : ((abs + 0.055) / 1.055) ** 2.4;
7
+ return Math.sign(v) * lin;
8
+ }
9
+ function linearChannelToSrgb(v) {
10
+ const abs = Math.abs(v);
11
+ const enc = abs <= 31308e-7 ? abs * 12.92 : 1.055 * abs ** (1 / 2.4) - 0.055;
12
+ return Math.sign(v) * enc;
13
+ }
14
+ function srgbToLinear({ r, g, b }) {
15
+ return { r: srgbChannelToLinear(r), g: srgbChannelToLinear(g), b: srgbChannelToLinear(b) };
16
+ }
17
+ function linearToSrgb({ r, g, b }) {
18
+ return { r: linearChannelToSrgb(r), g: linearChannelToSrgb(g), b: linearChannelToSrgb(b) };
19
+ }
20
+ function linearSrgbToOklab({ r, g, b }) {
21
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
22
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
23
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
24
+ return {
25
+ l: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
26
+ a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
27
+ b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
28
+ };
29
+ }
30
+ function oklabToLinearSrgb({ l: L, a, b }) {
31
+ const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
32
+ const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
33
+ const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
34
+ return {
35
+ r: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
36
+ g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
37
+ b: -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
38
+ };
39
+ }
40
+ function normalizeHue(h) {
41
+ const n = h % 360;
42
+ return n < 0 ? n + 360 : n;
43
+ }
44
+ function oklabToOklch({ l, a, b }) {
45
+ const c2 = Math.sqrt(a * a + b * b);
46
+ const h = c2 < ACHROMATIC_CHROMA ? 0 : normalizeHue(Math.atan2(b, a) * 180 / Math.PI);
47
+ return { l, c: c2, h };
48
+ }
49
+ function oklchToOklab({ l, c: c2, h }) {
50
+ const rad = h * Math.PI / 180;
51
+ return { l, a: c2 * Math.cos(rad), b: c2 * Math.sin(rad) };
52
+ }
53
+ function srgbToOklch(rgb) {
54
+ return oklabToOklch(linearSrgbToOklab(srgbToLinear(rgb)));
55
+ }
56
+ function oklchToSrgb(color) {
57
+ return linearToSrgb(oklabToLinearSrgb(oklchToOklab(color)));
58
+ }
59
+ function inSrgbGamut(color) {
60
+ const { r, g, b } = oklabToLinearSrgb(oklchToOklab(color));
61
+ return [r, g, b].every((v) => v >= -GAMUT_EPSILON && v <= 1 + GAMUT_EPSILON);
62
+ }
63
+ function gamutMap(color) {
64
+ const l = Math.min(1, Math.max(0, color.l));
65
+ const base = { l, c: Math.max(0, color.c), h: normalizeHue(color.h) };
66
+ if (l <= 0) return { l: 0, c: 0, h: base.h };
67
+ if (l >= 1) return { l: 1, c: 0, h: base.h };
68
+ if (inSrgbGamut(base)) return base;
69
+ let lo = 0;
70
+ let hi = base.c;
71
+ for (let i = 0; i < 24; i++) {
72
+ const mid = (lo + hi) / 2;
73
+ if (inSrgbGamut({ ...base, c: mid })) lo = mid;
74
+ else hi = mid;
75
+ }
76
+ return { ...base, c: lo };
77
+ }
78
+ var clamp01 = (v) => Math.min(1, Math.max(0, v));
79
+ function oklchToSrgbInGamut(color) {
80
+ const { r, g, b } = oklchToSrgb(gamutMap(color));
81
+ return { r: clamp01(r), g: clamp01(g), b: clamp01(b) };
82
+ }
83
+ var hex2 = (v) => Math.round(clamp01(v) * 255).toString(16).padStart(2, "0");
84
+ function rgbToHex({ r, g, b }) {
85
+ return `#${hex2(r)}${hex2(g)}${hex2(b)}`;
86
+ }
87
+ function hexToRgb(hex) {
88
+ const m = /^#?([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(hex.trim());
89
+ if (!m?.[1]) throw new Error(`Invalid hex color: ${hex}`);
90
+ let h = m[1];
91
+ if (h.length <= 4) h = [...h].map((ch) => ch + ch).join("");
92
+ const n = (i) => Number.parseInt(h.slice(i, i + 2), 16) / 255;
93
+ return { r: n(0), g: n(2), b: n(4) };
94
+ }
95
+ function oklchToHex(color) {
96
+ return rgbToHex(oklchToSrgbInGamut(color));
97
+ }
98
+ function hexToOklch(hex) {
99
+ return srgbToOklch(hexToRgb(hex));
100
+ }
101
+ function num(token, percentRef) {
102
+ const t = token.trim().toLowerCase();
103
+ if (t === "none") return 0;
104
+ if (t.endsWith("%")) return Number.parseFloat(t) / 100 * percentRef;
105
+ const v = Number.parseFloat(t);
106
+ if (!Number.isFinite(v)) throw new Error(`Invalid number: ${token}`);
107
+ return v;
108
+ }
109
+ function hue(token) {
110
+ const t = token.trim().toLowerCase();
111
+ if (t === "none") return 0;
112
+ const v = Number.parseFloat(t);
113
+ if (!Number.isFinite(v)) throw new Error(`Invalid hue: ${token}`);
114
+ if (t.endsWith("turn")) return normalizeHue(v * 360);
115
+ if (t.endsWith("grad")) return normalizeHue(v * 0.9);
116
+ if (t.endsWith("rad")) return normalizeHue(v * 180 / Math.PI);
117
+ return normalizeHue(v);
118
+ }
119
+ function args(body) {
120
+ const noAlpha = body.split("/")[0] ?? "";
121
+ const parts = noAlpha.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
122
+ return parts.slice(0, 3);
123
+ }
124
+ var D50 = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
125
+ function labToLinearSrgb(L, a, b) {
126
+ const k = 24389 / 27;
127
+ const e = 216 / 24389;
128
+ const fy = (L + 16) / 116;
129
+ const fx = a / 500 + fy;
130
+ const fz = fy - b / 200;
131
+ const xr = fx ** 3 > e ? fx ** 3 : (116 * fx - 16) / k;
132
+ const yr = L > k * e ? fy ** 3 : L / k;
133
+ const zr = fz ** 3 > e ? fz ** 3 : (116 * fz - 16) / k;
134
+ const x50 = xr * D50[0];
135
+ const y50 = yr * D50[1];
136
+ const z50 = zr * D50[2];
137
+ const x = 0.955473421488075 * x50 - 0.02309845494876471 * y50 + 0.06325924320057072 * z50;
138
+ const y = -0.0283697093338637 * x50 + 1.0099953980813041 * y50 + 0.021041441191917323 * z50;
139
+ const z = 0.012314014864481998 * x50 - 0.020507649298898964 * y50 + 1.330365926242124 * z50;
140
+ return {
141
+ r: 3.2409699419045226 * x - 1.537383177570094 * y - 0.4986107602930034 * z,
142
+ g: -0.9692436362808796 * x + 1.8759675015077202 * y + 0.04155505740717559 * z,
143
+ b: 0.05563007969699366 * x - 0.20397695888897652 * y + 1.0569715142428786 * z
144
+ };
145
+ }
146
+ function hslToRgb(h, s, l) {
147
+ const f = (n) => {
148
+ const k = (n + h / 30) % 12;
149
+ const a = s * Math.min(l, 1 - l);
150
+ return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
151
+ };
152
+ return { r: f(0), g: f(8), b: f(4) };
153
+ }
154
+ function parseColor(input) {
155
+ const s = input.trim().toLowerCase();
156
+ if (s.startsWith("#")) return hexToOklch(s);
157
+ const m = /^([a-z]+)\(([^()]*)\)$/.exec(s);
158
+ if (!m?.[1] || m[2] === void 0) throw new Error(`Unsupported color: ${input}`);
159
+ const fn = m[1];
160
+ const parts = args(m[2]);
161
+ if (parts.length !== 3) throw new Error(`Expected 3 components: ${input}`);
162
+ const [p0, p1, p2] = parts;
163
+ switch (fn) {
164
+ case "oklch":
165
+ return { l: num(p0, 1), c: Math.max(0, num(p1, 0.4)), h: hue(p2) };
166
+ case "oklab":
167
+ return oklabToOklch({ l: num(p0, 1), a: num(p1, 0.4), b: num(p2, 0.4) });
168
+ case "rgb":
169
+ case "rgba":
170
+ return srgbToOklch({ r: num(p0, 255) / 255, g: num(p1, 255) / 255, b: num(p2, 255) / 255 });
171
+ case "hsl":
172
+ case "hsla":
173
+ return srgbToOklch(hslToRgb(hue(p0), num(p1, 100) / 100, num(p2, 100) / 100));
174
+ case "lab":
175
+ return oklabToOklch(
176
+ linearSrgbToOklab(labToLinearSrgb(num(p0, 100), num(p1, 125), num(p2, 125)))
177
+ );
178
+ case "lch": {
179
+ const L = num(p0, 100);
180
+ const C = num(p1, 150);
181
+ const H = hue(p2) * Math.PI / 180;
182
+ return oklabToOklch(linearSrgbToOklab(labToLinearSrgb(L, C * Math.cos(H), C * Math.sin(H))));
183
+ }
184
+ default:
185
+ throw new Error(`Unsupported color function: ${fn}()`);
186
+ }
187
+ }
188
+ function isColor(input) {
189
+ try {
190
+ parseColor(input);
191
+ return true;
192
+ } catch {
193
+ return false;
194
+ }
195
+ }
196
+ var round = (v, digits) => {
197
+ const f = 10 ** digits;
198
+ const r = Math.round(v * f) / f;
199
+ return Object.is(r, -0) ? 0 : r;
200
+ };
201
+ function formatOklch({ l, c: c2, h }) {
202
+ const cc = round(c2, 4);
203
+ return `oklch(${round(l, 4)} ${cc} ${cc === 0 ? 0 : round(h, 2)})`;
204
+ }
205
+ function toHex(input) {
206
+ return oklchToHex(typeof input === "string" ? parseColor(input) : input);
207
+ }
208
+ function mixHue(a, b, t) {
209
+ const d = ((b - a) % 360 + 540) % 360 - 180;
210
+ return normalizeHue(a + d * t);
211
+ }
212
+ function deltaEOK(x, y) {
213
+ const a = oklchToOklab(x);
214
+ const b = oklchToOklab(y);
215
+ return Math.hypot(a.l - b.l, a.a - b.a, a.b - b.b);
216
+ }
217
+
218
+ // src/compile.ts
219
+ import {
220
+ COLOR_ROLES as COLOR_ROLES3,
221
+ COLOR_SCALE_STEPS as COLOR_SCALE_STEPS3,
222
+ COLOR_THEMES as COLOR_THEMES4
223
+ } from "@omniharmonic/openspace-schema";
224
+
225
+ // src/contrast.ts
226
+ import { COLOR_THEMES } from "@omniharmonic/openspace-schema";
227
+ function relativeLuminance(color) {
228
+ const hex = typeof color === "string" ? toHexStrict(color) : oklchToHex(color);
229
+ const { r, g, b } = srgbToLinear(hexToRgb(hex));
230
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
231
+ }
232
+ function contrastRatio(a, b) {
233
+ const la = relativeLuminance(a);
234
+ const lb = relativeLuminance(b);
235
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
236
+ return (hi + 0.05) / (lo + 0.05);
237
+ }
238
+ function toHexStrict(color) {
239
+ return /^#[0-9a-f]{6}$/i.test(color) ? color.toLowerCase() : oklchToHex(parseColor(color));
240
+ }
241
+ var CONTRAST_MINIMUMS = {
242
+ standard: { body: 4.5, large: 3 },
243
+ high: { body: 7, large: 4.5 }
244
+ };
245
+ var BACKGROUNDS = ["background", "surface", "surface-raised"];
246
+ var onAll = (fg, level) => BACKGROUNDS.map((bg) => ({ fg, bg, level }));
247
+ var CORE_CONTRAST_PAIRS = [
248
+ ...onAll("text", "body"),
249
+ ...onAll("heading", "body"),
250
+ ...onAll("text-muted", "body"),
251
+ ...onAll("link", "body"),
252
+ { fg: "accent", bg: "background", level: "large" },
253
+ { fg: "accent", bg: "surface", level: "large" },
254
+ { fg: "accent", bg: "surface-raised", level: "large" },
255
+ { fg: "accent-text", bg: "accent", level: "body" },
256
+ ...onAll("focus", "large"),
257
+ ...onAll("success", "body"),
258
+ ...onAll("warning", "body"),
259
+ ...onAll("danger", "body")
260
+ ];
261
+ var STATE_PAIRS = [
262
+ { fg: "accent-text", bg: "accent-hover", level: "body", adjust: "bg" },
263
+ { fg: "accent-text", bg: "accent-pressed", level: "body", adjust: "bg" },
264
+ { fg: "accent-hover", bg: "background", level: "large" },
265
+ { fg: "accent-pressed", bg: "background", level: "large" }
266
+ ];
267
+ var CONTRAST_PAIRS = [...CORE_CONTRAST_PAIRS, ...STATE_PAIRS];
268
+ function auditContrast(themes, options = {}) {
269
+ const mins = CONTRAST_MINIMUMS[options.contrast ?? "standard"];
270
+ const failures = [];
271
+ for (const theme of COLOR_THEMES) {
272
+ const colors = themes[theme];
273
+ if (!colors) continue;
274
+ for (const { fg, bg, level, adjust } of CONTRAST_PAIRS) {
275
+ const fgColor = colors[fg];
276
+ const bgColor = colors[bg];
277
+ if (fgColor === void 0 || bgColor === void 0) continue;
278
+ const required = mins[level];
279
+ const ratio = contrastRatio(fgColor, bgColor);
280
+ if (ratio < required) {
281
+ failures.push({
282
+ theme,
283
+ fg,
284
+ bg,
285
+ level,
286
+ fgColor,
287
+ bgColor,
288
+ ratio: Math.round(ratio * 100) / 100,
289
+ required,
290
+ suggestion: adjust === "bg" ? nearestAccessible(bgColor, fgColor, required) : nearestAccessible(fgColor, bgColor, required)
291
+ });
292
+ }
293
+ }
294
+ }
295
+ return failures;
296
+ }
297
+ var SOLVER_MARGIN = 0.03;
298
+ function withL(color, l) {
299
+ return gamutMap({ ...color, l });
300
+ }
301
+ function solveLightness(color, against, min, direction = "either") {
302
+ const bg = typeof against === "string" ? parseColor(against) : against;
303
+ const target = min + SOLVER_MARGIN;
304
+ const start = gamutMap(color);
305
+ if (contrastRatio(start, bg) >= target) return start;
306
+ let best = null;
307
+ let bestDelta = Number.POSITIVE_INFINITY;
308
+ const ends = direction === "darker" ? [0] : direction === "lighter" ? [1] : [0, 1];
309
+ for (const end of ends) {
310
+ if (contrastRatio(withL(start, end), bg) < target) continue;
311
+ let fail = start.l;
312
+ let pass = end;
313
+ for (let i = 0; i < 24; i++) {
314
+ const mid = (fail + pass) / 2;
315
+ if (contrastRatio(withL(start, mid), bg) >= target) pass = mid;
316
+ else fail = mid;
317
+ }
318
+ const delta = Math.abs(pass - start.l);
319
+ if (delta < bestDelta) {
320
+ bestDelta = delta;
321
+ best = withL(start, pass);
322
+ }
323
+ }
324
+ return best;
325
+ }
326
+ function nearestAccessible(color, against, min = 4.5) {
327
+ const fg = parseColor(color);
328
+ const solved = solveLightness(fg, against, min);
329
+ if (solved) return oklchToHex(solved);
330
+ const dark = withL(fg, 0);
331
+ const light = withL(fg, 1);
332
+ return oklchToHex(contrastRatio(dark, against) >= contrastRatio(light, against) ? dark : light);
333
+ }
334
+
335
+ // src/pairings.ts
336
+ var FALLBACK_STACKS = {
337
+ serif: ["ui-serif", "Georgia", "Cambria", '"Times New Roman"', "Times", "serif"],
338
+ "sans-serif": [
339
+ "ui-sans-serif",
340
+ "system-ui",
341
+ "-apple-system",
342
+ '"Segoe UI"',
343
+ "Roboto",
344
+ '"Helvetica Neue"',
345
+ "Arial",
346
+ "sans-serif"
347
+ ],
348
+ monospace: [
349
+ "ui-monospace",
350
+ "SFMono-Regular",
351
+ "Menlo",
352
+ "Consolas",
353
+ '"Liberation Mono"',
354
+ "monospace"
355
+ ]
356
+ };
357
+ var serif = (family, weights, extra = {}) => ({ family, category: "serif", weights, ...extra });
358
+ var sans = (family, weights, extra = {}) => ({ family, category: "sans-serif", weights, ...extra });
359
+ function pairing(p) {
360
+ return { bodyWeight: 400, strongWeight: 600, eyebrowTracking: "0.12em", ...p };
361
+ }
362
+ var TYPE_PAIRINGS = [
363
+ // --- the twelve launch-template pairings (doc 04 §7) ---
364
+ pairing({
365
+ id: "fraunces-hanken",
366
+ name: "Fraunces & Hanken Grotesk",
367
+ heading: serif("Fraunces", [400, 500, 600], { italic: true, opticalSize: true }),
368
+ body: sans("Hanken Grotesk", [400, 500, 600]),
369
+ headingWeight: 400,
370
+ headingTracking: "-0.02em",
371
+ mood: ["calm", "organic", "warm"],
372
+ template: "meadow"
373
+ }),
374
+ pairing({
375
+ id: "newsreader-public-sans",
376
+ name: "Newsreader & Public Sans",
377
+ heading: serif("Newsreader", [400, 500, 600], { italic: true, opticalSize: true }),
378
+ body: sans("Public Sans", [400, 600, 700]),
379
+ headingWeight: 500,
380
+ headingTracking: "-0.015em",
381
+ mood: ["trustworthy", "civic", "editorial"],
382
+ template: "harbor"
383
+ }),
384
+ pairing({
385
+ id: "instrument",
386
+ name: "Instrument Serif & Instrument Sans",
387
+ heading: serif("Instrument Serif", [400], { italic: true }),
388
+ body: sans("Instrument Sans", [400, 500, 600]),
389
+ headingWeight: 400,
390
+ headingTracking: "-0.01em",
391
+ mood: ["minimal", "gallery", "refined"],
392
+ template: "atelier"
393
+ }),
394
+ pairing({
395
+ id: "source-serif-plex",
396
+ name: "Source Serif 4 & IBM Plex Sans",
397
+ heading: serif("Source Serif 4", [400, 600, 700], { italic: true, opticalSize: true }),
398
+ body: sans("IBM Plex Sans", [400, 500, 600]),
399
+ headingWeight: 600,
400
+ headingTracking: "-0.015em",
401
+ mood: ["crisp", "editorial", "confident"],
402
+ template: "ledger"
403
+ }),
404
+ pairing({
405
+ id: "young-serif-work-sans",
406
+ name: "Young Serif & Work Sans",
407
+ heading: serif("Young Serif", [400]),
408
+ body: sans("Work Sans", [400, 500, 600]),
409
+ headingWeight: 400,
410
+ headingTracking: "-0.01em",
411
+ mood: ["warm", "appetizing", "friendly"],
412
+ template: "hearth"
413
+ }),
414
+ pairing({
415
+ id: "bricolage-geist",
416
+ name: "Bricolage Grotesque & Geist",
417
+ heading: sans("Bricolage Grotesque", [500, 600, 700], { opticalSize: true }),
418
+ body: sans("Geist", [400, 500, 600]),
419
+ headingWeight: 700,
420
+ headingTracking: "-0.035em",
421
+ mood: ["bold", "modern", "technical"],
422
+ template: "signal"
423
+ }),
424
+ pairing({
425
+ id: "literata-manrope",
426
+ name: "Literata & Manrope",
427
+ heading: serif("Literata", [400, 500, 600], { italic: true, opticalSize: true }),
428
+ body: sans("Manrope", [400, 500, 700]),
429
+ headingWeight: 500,
430
+ headingTracking: "-0.015em",
431
+ mood: ["literary", "reading", "calm"],
432
+ template: "field-notes"
433
+ }),
434
+ pairing({
435
+ id: "syne-dm-sans",
436
+ name: "Syne & DM Sans",
437
+ heading: sans("Syne", [600, 700, 800]),
438
+ body: sans("DM Sans", [400, 500, 700]),
439
+ headingWeight: 700,
440
+ headingTracking: "-0.02em",
441
+ mood: ["expressive", "nocturnal", "artsy"],
442
+ template: "encore"
443
+ }),
444
+ pairing({
445
+ id: "gloock-albert-sans",
446
+ name: "Gloock & Albert Sans",
447
+ heading: serif("Gloock", [400]),
448
+ body: sans("Albert Sans", [400, 500, 600]),
449
+ headingWeight: 400,
450
+ headingTracking: "-0.01em",
451
+ mood: ["tactile", "crafted", "classic"],
452
+ template: "kiln"
453
+ }),
454
+ pairing({
455
+ id: "spectral-epilogue",
456
+ name: "Spectral & Epilogue",
457
+ heading: serif("Spectral", [400, 500, 600], { italic: true }),
458
+ body: sans("Epilogue", [400, 500, 600]),
459
+ headingWeight: 500,
460
+ headingTracking: "-0.01em",
461
+ mood: ["grounded", "collective", "earnest"],
462
+ template: "commons"
463
+ }),
464
+ pairing({
465
+ id: "cormorant-jost",
466
+ name: "Cormorant Garamond & Jost",
467
+ heading: serif("Cormorant Garamond", [400, 500, 600], { italic: true }),
468
+ body: sans("Jost", [400, 500]),
469
+ headingWeight: 500,
470
+ headingTracking: "0em",
471
+ strongWeight: 500,
472
+ eyebrowTracking: "0.2em",
473
+ mood: ["romantic", "refined", "elegant"],
474
+ template: "vow"
475
+ }),
476
+ pairing({
477
+ id: "outfit",
478
+ name: "Outfit",
479
+ heading: sans("Outfit", [600, 700]),
480
+ body: sans("Outfit", [400, 500]),
481
+ headingWeight: 700,
482
+ headingTracking: "-0.025em",
483
+ strongWeight: 600,
484
+ mood: ["punchy", "mobile", "friendly"],
485
+ template: "link"
486
+ }),
487
+ // --- library pairings ---
488
+ pairing({
489
+ id: "playfair-source-sans",
490
+ name: "Playfair Display & Source Sans 3",
491
+ heading: serif("Playfair Display", [400, 600, 700], { italic: true }),
492
+ body: sans("Source Sans 3", [400, 600]),
493
+ headingWeight: 600,
494
+ headingTracking: "-0.015em",
495
+ mood: ["classic", "fashion", "editorial"]
496
+ }),
497
+ pairing({
498
+ id: "dm-serif-dm-sans",
499
+ name: "DM Serif Display & DM Sans",
500
+ heading: serif("DM Serif Display", [400], { italic: true }),
501
+ body: sans("DM Sans", [400, 500, 700]),
502
+ headingWeight: 400,
503
+ headingTracking: "-0.015em",
504
+ mood: ["confident", "warm", "modern"]
505
+ }),
506
+ pairing({
507
+ id: "lora-inter",
508
+ name: "Lora & Inter",
509
+ heading: serif("Lora", [500, 600, 700], { italic: true }),
510
+ body: sans("Inter", [400, 500, 600], { opticalSize: true }),
511
+ headingWeight: 600,
512
+ headingTracking: "-0.01em",
513
+ mood: ["approachable", "balanced", "versatile"]
514
+ }),
515
+ pairing({
516
+ id: "eb-garamond-inter",
517
+ name: "EB Garamond & Inter",
518
+ heading: serif("EB Garamond", [400, 500, 600], { italic: true }),
519
+ body: sans("Inter", [400, 500, 600], { opticalSize: true }),
520
+ headingWeight: 500,
521
+ headingTracking: "-0.01em",
522
+ mood: ["scholarly", "timeless", "quiet"]
523
+ }),
524
+ pairing({
525
+ id: "libre-baskerville-karla",
526
+ name: "Libre Baskerville & Karla",
527
+ heading: serif("Libre Baskerville", [400, 700], { italic: true }),
528
+ body: sans("Karla", [400, 500, 700]),
529
+ headingWeight: 400,
530
+ headingTracking: "-0.01em",
531
+ mood: ["bookish", "trustworthy", "warm"]
532
+ }),
533
+ pairing({
534
+ id: "space-grotesk-inter",
535
+ name: "Space Grotesk & Inter",
536
+ heading: sans("Space Grotesk", [500, 600, 700]),
537
+ body: sans("Inter", [400, 500, 600], { opticalSize: true }),
538
+ headingWeight: 600,
539
+ headingTracking: "-0.03em",
540
+ mood: ["technical", "startup", "precise"]
541
+ }),
542
+ pairing({
543
+ id: "sora-inter",
544
+ name: "Sora & Inter",
545
+ heading: sans("Sora", [500, 600, 700]),
546
+ body: sans("Inter", [400, 500, 600], { opticalSize: true }),
547
+ headingWeight: 600,
548
+ headingTracking: "-0.03em",
549
+ mood: ["clean", "product", "modern"]
550
+ }),
551
+ pairing({
552
+ id: "plus-jakarta",
553
+ name: "Plus Jakarta Sans",
554
+ heading: sans("Plus Jakarta Sans", [600, 700, 800]),
555
+ body: sans("Plus Jakarta Sans", [400, 500]),
556
+ headingWeight: 700,
557
+ headingTracking: "-0.025em",
558
+ mood: ["friendly", "product", "crisp"]
559
+ }),
560
+ pairing({
561
+ id: "manrope",
562
+ name: "Manrope",
563
+ heading: sans("Manrope", [600, 700, 800]),
564
+ body: sans("Manrope", [400, 500]),
565
+ headingWeight: 700,
566
+ headingTracking: "-0.025em",
567
+ mood: ["neutral", "geometric", "calm"]
568
+ }),
569
+ pairing({
570
+ id: "unbounded-figtree",
571
+ name: "Unbounded & Figtree",
572
+ heading: sans("Unbounded", [500, 600, 700]),
573
+ body: sans("Figtree", [400, 500, 600]),
574
+ headingWeight: 600,
575
+ headingTracking: "-0.02em",
576
+ mood: ["loud", "playful", "event"]
577
+ }),
578
+ pairing({
579
+ id: "bodoni-montserrat",
580
+ name: "Bodoni Moda & Montserrat",
581
+ heading: serif("Bodoni Moda", [400, 500, 600], { italic: true, opticalSize: true }),
582
+ body: sans("Montserrat", [400, 500, 600]),
583
+ headingWeight: 500,
584
+ headingTracking: "-0.01em",
585
+ mood: ["luxury", "fashion", "dramatic"]
586
+ }),
587
+ pairing({
588
+ id: "libre-caslon-nunito-sans",
589
+ name: "Libre Caslon Text & Nunito Sans",
590
+ heading: serif("Libre Caslon Text", [400, 700], { italic: true }),
591
+ body: sans("Nunito Sans", [400, 600, 700]),
592
+ headingWeight: 400,
593
+ headingTracking: "-0.01em",
594
+ mood: ["gentle", "heritage", "warm"]
595
+ }),
596
+ pairing({
597
+ id: "zilla-slab-inter",
598
+ name: "Zilla Slab & Inter",
599
+ heading: serif("Zilla Slab", [500, 600, 700]),
600
+ body: sans("Inter", [400, 500, 600], { opticalSize: true }),
601
+ headingWeight: 600,
602
+ headingTracking: "-0.01em",
603
+ mood: ["sturdy", "journalistic", "honest"]
604
+ }),
605
+ pairing({
606
+ id: "bitter-roboto",
607
+ name: "Bitter & Roboto",
608
+ heading: serif("Bitter", [500, 600, 700], { italic: true }),
609
+ body: sans("Roboto", [400, 500, 700]),
610
+ headingWeight: 600,
611
+ headingTracking: "-0.015em",
612
+ mood: ["practical", "clear", "service"]
613
+ }),
614
+ pairing({
615
+ id: "alegreya",
616
+ name: "Alegreya & Alegreya Sans",
617
+ heading: serif("Alegreya", [500, 600, 700], { italic: true }),
618
+ body: sans("Alegreya Sans", [400, 500, 700]),
619
+ headingWeight: 600,
620
+ headingTracking: "-0.01em",
621
+ mood: ["humanist", "literary", "warm"]
622
+ }),
623
+ pairing({
624
+ id: "josefin-crimson",
625
+ name: "Josefin Sans & Crimson Pro",
626
+ heading: sans("Josefin Sans", [400, 600, 700]),
627
+ body: serif("Crimson Pro", [400, 500, 600], { italic: true }),
628
+ headingWeight: 600,
629
+ headingTracking: "0em",
630
+ eyebrowTracking: "0.18em",
631
+ mood: ["vintage", "elegant", "art-deco"]
632
+ }),
633
+ pairing({
634
+ id: "merriweather-open-sans",
635
+ name: "Merriweather & Open Sans",
636
+ heading: serif("Merriweather", [400, 700], { italic: true }),
637
+ body: sans("Open Sans", [400, 600, 700]),
638
+ headingWeight: 700,
639
+ headingTracking: "-0.01em",
640
+ mood: ["readable", "dependable", "community"]
641
+ }),
642
+ pairing({
643
+ id: "plex-mono-plex-sans",
644
+ name: "IBM Plex Mono & IBM Plex Sans",
645
+ heading: { family: "IBM Plex Mono", category: "monospace", weights: [500, 600] },
646
+ body: sans("IBM Plex Sans", [400, 500, 600]),
647
+ headingWeight: 600,
648
+ headingTracking: "-0.02em",
649
+ mood: ["developer", "technical", "structured"]
650
+ }),
651
+ pairing({
652
+ id: "crimson-figtree",
653
+ name: "Crimson Pro & Figtree",
654
+ heading: serif("Crimson Pro", [500, 600, 700], { italic: true }),
655
+ body: sans("Figtree", [400, 500, 600]),
656
+ headingWeight: 600,
657
+ headingTracking: "-0.01em",
658
+ mood: ["soft", "personal", "story"]
659
+ }),
660
+ pairing({
661
+ id: "abril-lato",
662
+ name: "Abril Fatface & Lato",
663
+ heading: serif("Abril Fatface", [400]),
664
+ body: sans("Lato", [400, 700]),
665
+ headingWeight: 400,
666
+ headingTracking: "-0.01em",
667
+ strongWeight: 700,
668
+ mood: ["poster", "bold", "retro"]
669
+ }),
670
+ pairing({
671
+ id: "archivo",
672
+ name: "Archivo",
673
+ heading: sans("Archivo", [600, 700, 800]),
674
+ body: sans("Archivo", [400, 500]),
675
+ headingWeight: 700,
676
+ headingTracking: "-0.03em",
677
+ mood: ["industrial", "grotesque", "direct"]
678
+ })
679
+ ];
680
+ var DEFAULT_PAIRING_ID = "fraunces-hanken";
681
+ var BY_ID = new Map(TYPE_PAIRINGS.map((p) => [p.id, p]));
682
+ function getPairing(id) {
683
+ return BY_ID.get(id);
684
+ }
685
+ function fontsourceId(spec) {
686
+ return spec.fontsourceId ?? spec.family.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
687
+ }
688
+ var quoteFamily = (family) => /^[a-z-]+$/i.test(family) ? family : `"${family}"`;
689
+ function fontStack(spec) {
690
+ return [`"${spec.family}"`, ...FALLBACK_STACKS[spec.category]].map((f) => f.startsWith('"') ? f : quoteFamily(f)).join(", ");
691
+ }
692
+ function pairingFamilies(p) {
693
+ const merged = /* @__PURE__ */ new Map();
694
+ for (const spec of [p.heading, p.body]) {
695
+ const prev = merged.get(spec.family);
696
+ if (!prev) {
697
+ merged.set(spec.family, { ...spec, weights: [...spec.weights] });
698
+ continue;
699
+ }
700
+ prev.weights = [.../* @__PURE__ */ new Set([...prev.weights, ...spec.weights])].sort((a, b) => a - b);
701
+ prev.italic = prev.italic || spec.italic;
702
+ }
703
+ return [...merged.values()];
704
+ }
705
+ function googleFontsUrl(p, display = "swap") {
706
+ const families = pairingFamilies(p).map((spec) => {
707
+ const name2 = spec.family.replace(/ /g, "+");
708
+ const weights = [...spec.weights].sort((a, b) => a - b);
709
+ if (spec.italic) {
710
+ const tuples = [...weights.map((w) => `0,${w}`), ...weights.map((w) => `1,${w}`)];
711
+ return `family=${name2}:ital,wght@${tuples.join(";")}`;
712
+ }
713
+ return `family=${name2}:wght@${weights.join(";")}`;
714
+ });
715
+ return `https://fonts.googleapis.com/css2?${families.join("&")}&display=${display}`;
716
+ }
717
+ function fontsourcePackages(p) {
718
+ return pairingFamilies(p).map((spec) => `@fontsource/${fontsourceId(spec)}`);
719
+ }
720
+ function fontsourceCss(p, options = {}) {
721
+ const out = [];
722
+ for (const spec of pairingFamilies(p)) {
723
+ const pkg = `@fontsource/${fontsourceId(spec)}`;
724
+ const base = options.cdn ? `https://cdn.jsdelivr.net/npm/${pkg}@5` : pkg;
725
+ for (const w of spec.weights) {
726
+ out.push(`${base}/${w}.css`);
727
+ if (spec.italic) out.push(`${base}/${w}-italic.css`);
728
+ }
729
+ }
730
+ return out;
731
+ }
732
+
733
+ // src/custom-fonts.ts
734
+ var FORMAT = { woff2: "woff2", ttf: "truetype", otf: "opentype" };
735
+ var CATEGORY_FALLBACK = {
736
+ serif: FALLBACK_STACKS.serif,
737
+ "sans-serif": FALLBACK_STACKS["sans-serif"],
738
+ monospace: FALLBACK_STACKS.monospace,
739
+ display: FALLBACK_STACKS["sans-serif"],
740
+ handwriting: ["cursive", ...FALLBACK_STACKS.serif]
741
+ };
742
+ var cssString = (s) => `"${s.replace(/["\\\n\r]/g, "")}"`;
743
+ function customFontStack(font) {
744
+ return [cssString(font.family), ...CATEGORY_FALLBACK[font.category]].join(", ");
745
+ }
746
+ function chosenCustomFont(styles, family) {
747
+ if (!family) return void 0;
748
+ const key = family.toLowerCase();
749
+ return styles.fonts?.find((f) => f.family.toLowerCase() === key);
750
+ }
751
+ function nearestWeight(font, wanted) {
752
+ let best = font.files[0]?.weight ?? wanted;
753
+ for (const f of font.files) {
754
+ const d = Math.abs(f.weight - wanted);
755
+ const bd = Math.abs(best - wanted);
756
+ if (d < bd || d === bd && f.weight > best) best = f.weight;
757
+ }
758
+ return best;
759
+ }
760
+ function customFontFaces(styles, url, opts = {}) {
761
+ const used = new Set(
762
+ [styles.choices.headingFont, styles.choices.bodyFont].filter((x) => !!x).map((x) => x.toLowerCase())
763
+ );
764
+ const rules = [];
765
+ for (const font of styles.fonts ?? []) {
766
+ if (!opts.all && !used.has(font.family.toLowerCase())) continue;
767
+ for (const file of font.files) {
768
+ const ext = file.src.split(".").pop()?.toLowerCase() ?? "";
769
+ const format = FORMAT[ext];
770
+ if (!format) continue;
771
+ rules.push(
772
+ `@font-face{font-family:${cssString(font.family)};src:url(${cssString(url(file.src))}) format("${format}");font-weight:${file.weight};font-style:${file.style};font-display:swap}`
773
+ );
774
+ }
775
+ }
776
+ return rules.join("\n");
777
+ }
778
+ function customFontPreloads(styles, weights) {
779
+ const out = [];
780
+ for (const [family, weight] of [
781
+ [styles.choices.headingFont, weights.heading],
782
+ [styles.choices.bodyFont, weights.body]
783
+ ]) {
784
+ const font = chosenCustomFont(styles, family);
785
+ if (!font) continue;
786
+ const w = nearestWeight(font, weight);
787
+ const file = font.files.find(
788
+ (f) => f.weight === w && f.style === "normal" && f.src.endsWith(".woff2")
789
+ );
790
+ if (file && !out.includes(file.src)) out.push(file.src);
791
+ }
792
+ return out;
793
+ }
794
+
795
+ // src/type-scale.ts
796
+ var TYPE_PRESET_NAMES = [
797
+ "display",
798
+ "h1",
799
+ "h2",
800
+ "h3",
801
+ "h4",
802
+ "lead",
803
+ "body",
804
+ "small",
805
+ "eyebrow",
806
+ "button",
807
+ "nav"
808
+ ];
809
+ var FLUID_TYPE = {
810
+ minViewport: 360,
811
+ maxViewport: 1440,
812
+ /** Body size (px) at the min / max viewport. */
813
+ minBase: 16,
814
+ maxBase: 18,
815
+ minRatio: 1.2,
816
+ maxRatio: 1.333
817
+ };
818
+ var PRESET_STEP = {
819
+ display: 5.5,
820
+ h1: 4,
821
+ h2: 3,
822
+ h3: 2,
823
+ h4: 1,
824
+ lead: 0.5,
825
+ body: 0,
826
+ small: -1,
827
+ eyebrow: -1,
828
+ button: -0.5,
829
+ nav: -0.5
830
+ };
831
+ var LINE_HEIGHT = {
832
+ display: 1.05,
833
+ h1: 1.1,
834
+ h2: 1.15,
835
+ h3: 1.2,
836
+ h4: 1.3,
837
+ lead: 1.5,
838
+ body: 1.6,
839
+ small: 1.5,
840
+ eyebrow: 1.3,
841
+ button: 1.2,
842
+ nav: 1.3
843
+ };
844
+ var MIN_FONT_SIZE_PX = 14;
845
+ var HEADINGS = /* @__PURE__ */ new Set(["display", "h1", "h2", "h3", "h4"]);
846
+ var LARGE_HEADINGS = /* @__PURE__ */ new Set(["display", "h1", "h2"]);
847
+ var r4 = (v) => {
848
+ const r = Math.round(v * 1e4) / 1e4;
849
+ return Object.is(r, -0) ? 0 : r;
850
+ };
851
+ function fluidClamp(minPx, maxPx, minViewport = FLUID_TYPE.minViewport, maxViewport = FLUID_TYPE.maxViewport) {
852
+ if (Math.abs(maxPx - minPx) < 0.01) return `${r4(minPx / 16)}rem`;
853
+ const slope = (maxPx - minPx) / (maxViewport - minViewport);
854
+ const intercept = minPx - slope * minViewport;
855
+ const lo = Math.min(minPx, maxPx);
856
+ const hi = Math.max(minPx, maxPx);
857
+ return `clamp(${r4(lo / 16)}rem, ${r4(intercept / 16)}rem + ${r4(slope * 100)}vw, ${r4(hi / 16)}rem)`;
858
+ }
859
+ function stepSizes(step) {
860
+ return {
861
+ min: FLUID_TYPE.minBase * FLUID_TYPE.minRatio ** step,
862
+ max: FLUID_TYPE.maxBase * FLUID_TYPE.maxRatio ** step
863
+ };
864
+ }
865
+ function halfTracking(value) {
866
+ const m = /^(-?\d*\.?\d+)em$/.exec(value.trim());
867
+ return m?.[1] ? `${r4(Number.parseFloat(m[1]) / 2)}em` : value;
868
+ }
869
+ function typeScale(p) {
870
+ const out = {};
871
+ for (const name2 of TYPE_PRESET_NAMES) {
872
+ const sizes = stepSizes(PRESET_STEP[name2]);
873
+ const min = Math.max(MIN_FONT_SIZE_PX, sizes.min);
874
+ const max = Math.max(MIN_FONT_SIZE_PX, sizes.max);
875
+ const heading = HEADINGS.has(name2);
876
+ let letterSpacing = "0em";
877
+ if (LARGE_HEADINGS.has(name2)) letterSpacing = p.headingTracking;
878
+ else if (heading) letterSpacing = halfTracking(p.headingTracking);
879
+ else if (name2 === "eyebrow") letterSpacing = p.eyebrowTracking;
880
+ else if (name2 === "button" || name2 === "nav") letterSpacing = "0.01em";
881
+ let weight = "var(--os-font-weight-body)";
882
+ if (heading) weight = "var(--os-font-weight-heading)";
883
+ else if (name2 === "eyebrow" || name2 === "button" || name2 === "nav")
884
+ weight = "var(--os-font-weight-strong)";
885
+ out[name2] = {
886
+ "font-family": heading ? "var(--os-font-heading)" : "var(--os-font-body)",
887
+ "font-size": fluidClamp(min, max),
888
+ "line-height": String(LINE_HEIGHT[name2]),
889
+ "letter-spacing": letterSpacing,
890
+ "font-weight": weight,
891
+ "text-transform": name2 === "eyebrow" ? "uppercase" : "none"
892
+ };
893
+ }
894
+ return out;
895
+ }
896
+ function fontTokens(p) {
897
+ return {
898
+ heading: fontStack(p.heading),
899
+ body: fontStack(p.body),
900
+ "weight-heading": String(p.headingWeight),
901
+ "weight-body": String(p.bodyWeight),
902
+ "weight-strong": String(p.strongWeight)
903
+ };
904
+ }
905
+
906
+ // src/layout.ts
907
+ var rem = (px) => px === 0 ? "0" : `${Math.round(px / 16 * 1e4) / 1e4}rem`;
908
+ var SPACE_SCALES = {
909
+ compact: { "3xs": 4, "2xs": 8, xs: 12, sm: 16, md: 20, lg: 24, xl: 32, "2xl": 48, "3xl": 64 },
910
+ comfortable: {
911
+ "3xs": 4,
912
+ "2xs": 8,
913
+ xs: 12,
914
+ sm: 16,
915
+ md: 24,
916
+ lg: 32,
917
+ xl: 48,
918
+ "2xl": 64,
919
+ "3xl": 96
920
+ },
921
+ airy: { "3xs": 4, "2xs": 8, xs: 16, sm: 24, md: 32, lg: 48, xl: 64, "2xl": 96, "3xl": 128 }
922
+ };
923
+ var SECTION_RHYTHM = {
924
+ compact: { padY: [48, 80], gap: [16, 24], gutter: [20, 32] },
925
+ comfortable: { padY: [64, 120], gap: [24, 32], gutter: [24, 40] },
926
+ airy: { padY: [88, 168], gap: [32, 48], gutter: [24, 48] }
927
+ };
928
+ function spaceTokens(density) {
929
+ const out = { none: "0" };
930
+ for (const [step, px] of Object.entries(SPACE_SCALES[density])) out[step] = rem(px);
931
+ return out;
932
+ }
933
+ function sectionTokens(density) {
934
+ const r = SECTION_RHYTHM[density];
935
+ return {
936
+ "pad-y": fluidClamp(...r.padY),
937
+ "block-gap": fluidClamp(...r.gap),
938
+ gutter: fluidClamp(...r.gutter)
939
+ };
940
+ }
941
+ var WIDTH_TOKENS = { narrow: "720px", content: "1280px", wide: "1440px" };
942
+ var RADIUS_SCALES = {
943
+ sharp: { none: 0, sm: 0, md: 0, lg: 0, xl: 0, button: 0, card: 0, input: 0, image: 0 },
944
+ soft: { none: 0, sm: 4, md: 8, lg: 12, xl: 20, button: 8, card: 12, input: 6, image: 8 },
945
+ round: { none: 0, sm: 8, md: 14, lg: 22, xl: 32, button: 14, card: 22, input: 12, image: 22 }
946
+ };
947
+ function radiusTokens(shape) {
948
+ const out = {};
949
+ for (const [name2, px] of Object.entries(RADIUS_SCALES[shape]))
950
+ out[name2] = px === 0 ? "0" : `${px}px`;
951
+ out.full = "9999px";
952
+ return out;
953
+ }
954
+ var SHADOW_VALUES = {
955
+ none: "none",
956
+ sm: "0 1px 2px rgb(0 0 0 / 0.06), 0 1px 3px rgb(0 0 0 / 0.08)",
957
+ md: "0 2px 4px rgb(0 0 0 / 0.05), 0 6px 16px rgb(0 0 0 / 0.08)",
958
+ lg: "0 4px 8px rgb(0 0 0 / 0.04), 0 16px 40px rgb(0 0 0 / 0.12)"
959
+ };
960
+ var BUTTON_ROLES = ["primary", "secondary", "tertiary"];
961
+ var BUTTON_PROPS = [
962
+ "bg",
963
+ "color",
964
+ "border-color",
965
+ "border-width",
966
+ "radius",
967
+ "padding-x",
968
+ "padding-y",
969
+ "text-decoration",
970
+ "icon",
971
+ "bg-hover",
972
+ "color-hover",
973
+ "border-color-hover",
974
+ "bg-active",
975
+ "color-active",
976
+ "border-color-active"
977
+ ];
978
+ var BUTTON_SYSTEMS = {
979
+ solid: {
980
+ primary: { kind: "solid", pill: false, tone: "accent" },
981
+ secondary: { kind: "outline", pill: false, tone: "text" },
982
+ tertiary: { kind: "underline", pill: false, tone: "text" }
983
+ },
984
+ outline: {
985
+ primary: { kind: "outline", pill: false, tone: "accent" },
986
+ secondary: { kind: "outline", pill: false, tone: "text" },
987
+ tertiary: { kind: "underline", pill: false, tone: "text" }
988
+ },
989
+ underline: {
990
+ primary: { kind: "underline", pill: false, tone: "accent" },
991
+ secondary: { kind: "underline", pill: false, tone: "text" },
992
+ tertiary: { kind: "text-arrow", pill: false, tone: "text" }
993
+ },
994
+ "pill-solid": {
995
+ primary: { kind: "solid", pill: true, tone: "accent" },
996
+ secondary: { kind: "outline", pill: true, tone: "text" },
997
+ tertiary: { kind: "underline", pill: false, tone: "text" }
998
+ },
999
+ "pill-outline": {
1000
+ primary: { kind: "outline", pill: true, tone: "accent" },
1001
+ secondary: { kind: "outline", pill: true, tone: "text" },
1002
+ tertiary: { kind: "underline", pill: false, tone: "text" }
1003
+ },
1004
+ "text-arrow": {
1005
+ primary: { kind: "text-arrow", pill: false, tone: "accent" },
1006
+ secondary: { kind: "text-arrow", pill: false, tone: "text" },
1007
+ tertiary: { kind: "underline", pill: false, tone: "text" }
1008
+ }
1009
+ };
1010
+ var c = (role) => `var(--os-color-${role})`;
1011
+ function variantTokens(v) {
1012
+ const ink = v.tone === "accent" ? c("link") : c("text");
1013
+ const radius = v.pill ? "var(--os-radius-full)" : "var(--os-radius-button)";
1014
+ const padded = { "padding-x": "var(--os-space-lg)", "padding-y": "var(--os-space-xs)" };
1015
+ const bare = { "padding-x": "0", "padding-y": "var(--os-space-3xs)" };
1016
+ switch (v.kind) {
1017
+ case "solid": {
1018
+ const accent = v.tone === "accent";
1019
+ const fill = accent ? c("accent") : c("text");
1020
+ const label = accent ? c("accent-text") : c("background");
1021
+ const hover = accent ? c("accent-hover") : c("heading");
1022
+ const active = accent ? c("accent-pressed") : c("heading");
1023
+ return {
1024
+ bg: fill,
1025
+ color: label,
1026
+ "border-color": fill,
1027
+ "border-width": "1px",
1028
+ radius,
1029
+ ...padded,
1030
+ "text-decoration": "none",
1031
+ icon: "none",
1032
+ "bg-hover": hover,
1033
+ "color-hover": label,
1034
+ "border-color-hover": hover,
1035
+ "bg-active": active,
1036
+ "color-active": label,
1037
+ "border-color-active": active
1038
+ };
1039
+ }
1040
+ case "outline": {
1041
+ const line = v.tone === "accent" ? c("accent") : c("text");
1042
+ const fillHover = v.tone === "accent" ? c("accent") : c("text");
1043
+ const labelHover = v.tone === "accent" ? c("accent-text") : c("background");
1044
+ return {
1045
+ bg: "transparent",
1046
+ color: ink,
1047
+ "border-color": line,
1048
+ "border-width": "1.5px",
1049
+ radius,
1050
+ ...padded,
1051
+ "text-decoration": "none",
1052
+ icon: "none",
1053
+ "bg-hover": fillHover,
1054
+ "color-hover": labelHover,
1055
+ "border-color-hover": fillHover,
1056
+ "bg-active": v.tone === "accent" ? c("accent-pressed") : c("heading"),
1057
+ "color-active": labelHover,
1058
+ "border-color-active": v.tone === "accent" ? c("accent-pressed") : c("heading")
1059
+ };
1060
+ }
1061
+ case "underline":
1062
+ return {
1063
+ bg: "transparent",
1064
+ color: ink,
1065
+ "border-color": "transparent",
1066
+ "border-width": "0",
1067
+ radius: "0",
1068
+ ...bare,
1069
+ "text-decoration": "underline",
1070
+ icon: "none",
1071
+ "bg-hover": "transparent",
1072
+ "color-hover": c("heading"),
1073
+ "border-color-hover": "transparent",
1074
+ "bg-active": "transparent",
1075
+ "color-active": c("heading"),
1076
+ "border-color-active": "transparent"
1077
+ };
1078
+ case "text-arrow":
1079
+ return {
1080
+ bg: "transparent",
1081
+ color: ink,
1082
+ "border-color": "transparent",
1083
+ "border-width": "0",
1084
+ radius: "0",
1085
+ ...bare,
1086
+ "text-decoration": "none",
1087
+ icon: '"\u2192"',
1088
+ "bg-hover": "transparent",
1089
+ "color-hover": c("heading"),
1090
+ "border-color-hover": "transparent",
1091
+ "bg-active": "transparent",
1092
+ "color-active": c("heading"),
1093
+ "border-color-active": "transparent"
1094
+ };
1095
+ }
1096
+ }
1097
+ function buttonTokens(style) {
1098
+ const sys = BUTTON_SYSTEMS[style];
1099
+ return {
1100
+ primary: variantTokens(sys.primary),
1101
+ secondary: variantTokens(sys.secondary),
1102
+ tertiary: variantTokens(sys.tertiary)
1103
+ };
1104
+ }
1105
+ var MOTION_PROPS = [
1106
+ "enabled",
1107
+ "duration-fast",
1108
+ "duration-base",
1109
+ "duration-slow",
1110
+ "ease",
1111
+ "reveal-distance",
1112
+ "reveal-duration",
1113
+ "stagger",
1114
+ "hover-scale",
1115
+ "parallax"
1116
+ ];
1117
+ var MOTION_VALUES = {
1118
+ none: {
1119
+ enabled: "0",
1120
+ "duration-fast": "0ms",
1121
+ "duration-base": "0ms",
1122
+ "duration-slow": "0ms",
1123
+ ease: "linear",
1124
+ "reveal-distance": "0px",
1125
+ "reveal-duration": "0ms",
1126
+ stagger: "0ms",
1127
+ "hover-scale": "1",
1128
+ parallax: "0"
1129
+ },
1130
+ subtle: {
1131
+ enabled: "1",
1132
+ "duration-fast": "150ms",
1133
+ "duration-base": "250ms",
1134
+ "duration-slow": "400ms",
1135
+ ease: "cubic-bezier(0.2, 0, 0, 1)",
1136
+ "reveal-distance": "12px",
1137
+ "reveal-duration": "600ms",
1138
+ stagger: "60ms",
1139
+ "hover-scale": "1.02",
1140
+ parallax: "0"
1141
+ },
1142
+ expressive: {
1143
+ enabled: "1",
1144
+ "duration-fast": "200ms",
1145
+ "duration-base": "350ms",
1146
+ "duration-slow": "600ms",
1147
+ ease: "cubic-bezier(0.16, 1, 0.3, 1)",
1148
+ "reveal-distance": "32px",
1149
+ "reveal-duration": "900ms",
1150
+ stagger: "110ms",
1151
+ "hover-scale": "1.06",
1152
+ parallax: "0.2"
1153
+ }
1154
+ };
1155
+ var IMAGE_PROPS = [
1156
+ "radius",
1157
+ "clip-path",
1158
+ "aspect-ratio",
1159
+ "filter",
1160
+ "blend",
1161
+ "backdrop",
1162
+ "overlay",
1163
+ "overlay-blend",
1164
+ "overlay-opacity",
1165
+ "frame-padding",
1166
+ "frame-color",
1167
+ "frame-border"
1168
+ ];
1169
+ var GRAIN_SVG = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`;
1170
+ var IMAGE_BASE = {
1171
+ radius: "0",
1172
+ "clip-path": "none",
1173
+ "aspect-ratio": "auto",
1174
+ filter: "none",
1175
+ blend: "normal",
1176
+ backdrop: "transparent",
1177
+ overlay: "none",
1178
+ "overlay-blend": "normal",
1179
+ "overlay-opacity": "0",
1180
+ "frame-padding": "0",
1181
+ "frame-color": "transparent",
1182
+ "frame-border": "0 solid transparent"
1183
+ };
1184
+ var IMAGE_TREATMENT_VALUES = {
1185
+ none: IMAGE_BASE,
1186
+ rounded: { ...IMAGE_BASE, radius: "max(var(--os-radius-lg), 0.75rem)" },
1187
+ arch: { ...IMAGE_BASE, radius: "9999px 9999px 0 0" },
1188
+ circle: { ...IMAGE_BASE, radius: "50%", "aspect-ratio": "1" },
1189
+ blob: { ...IMAGE_BASE, radius: "42% 58% 63% 37% / 41% 44% 56% 59%" },
1190
+ duotone: {
1191
+ ...IMAGE_BASE,
1192
+ radius: "var(--os-radius-image)",
1193
+ filter: "grayscale(1) contrast(1.1)",
1194
+ blend: "multiply",
1195
+ backdrop: "var(--os-color-accent-200)",
1196
+ overlay: "linear-gradient(var(--os-color-accent-900), var(--os-color-accent-900))",
1197
+ "overlay-blend": "lighten",
1198
+ "overlay-opacity": "1"
1199
+ },
1200
+ grain: {
1201
+ ...IMAGE_BASE,
1202
+ radius: "var(--os-radius-image)",
1203
+ overlay: GRAIN_SVG,
1204
+ "overlay-blend": "overlay",
1205
+ "overlay-opacity": "0.22"
1206
+ },
1207
+ frame: {
1208
+ ...IMAGE_BASE,
1209
+ radius: "var(--os-radius-sm)",
1210
+ "frame-padding": "var(--os-space-xs)",
1211
+ "frame-color": "var(--os-color-surface-raised)",
1212
+ "frame-border": "1px solid var(--os-color-border)"
1213
+ }
1214
+ };
1215
+
1216
+ // src/vars.ts
1217
+ import {
1218
+ COLOR_ROLES,
1219
+ COLOR_SCALE_STEPS,
1220
+ COLOR_THEMES as COLOR_THEMES2,
1221
+ RADIUS_TOKENS,
1222
+ SHADOW_TOKENS,
1223
+ SPACE_TOKENS
1224
+ } from "@omniharmonic/openspace-schema";
1225
+
1226
+ // src/types.ts
1227
+ var STATE_ROLES = ["accent-hover", "accent-pressed"];
1228
+
1229
+ // src/vars.ts
1230
+ var VAR_PREFIX = "--os-";
1231
+ function varName(...path) {
1232
+ return VAR_PREFIX + path.map(
1233
+ (seg) => String(seg).replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
1234
+ ).join("-");
1235
+ }
1236
+ var cssVar = (...path) => `var(${varName(...path)})`;
1237
+ var colorVar = (token) => varName("color", token);
1238
+ var themeClass = (theme) => `os-theme-${theme}`;
1239
+ var imageTreatmentClass = (treatment) => `os-image-${treatment}`;
1240
+ var SPACE_STEPS = SPACE_TOKENS;
1241
+ var RADIUS_NAMES = [...RADIUS_TOKENS, "button", "card", "input", "image"];
1242
+ var SHADOW_NAMES = SHADOW_TOKENS;
1243
+ var TYPE_PROPS = [
1244
+ "font-family",
1245
+ "font-size",
1246
+ "line-height",
1247
+ "letter-spacing",
1248
+ "font-weight",
1249
+ "text-transform"
1250
+ ];
1251
+ var record = (keys, fn) => Object.fromEntries(keys.map((k) => [k, fn(k)]));
1252
+ var TOKEN_VARS = {
1253
+ /** Semantic roles plus `accent-hover` / `accent-pressed` (theme-scoped, contrast-solved). */
1254
+ color: record([...COLOR_ROLES, ...STATE_ROLES], (r) => varName("color", r)),
1255
+ /** Accent scale `--os-color-accent-50` … `-950`. */
1256
+ accent: record(COLOR_SCALE_STEPS, (s) => varName("color", "accent", s)),
1257
+ /** Neutral scale `--os-color-neutral-50` … `-950`. */
1258
+ neutral: record(COLOR_SCALE_STEPS, (s) => varName("color", "neutral", s)),
1259
+ /** The (gamut-mapped) brand color as chosen. */
1260
+ brand: varName("color", "brand"),
1261
+ font: {
1262
+ heading: varName("font", "heading"),
1263
+ body: varName("font", "body"),
1264
+ weightHeading: varName("font", "weight", "heading"),
1265
+ weightBody: varName("font", "weight", "body"),
1266
+ weightStrong: varName("font", "weight", "strong")
1267
+ },
1268
+ /** Typography presets: `--os-type-<preset>-<css-property>`. */
1269
+ type: record(
1270
+ TYPE_PRESET_NAMES,
1271
+ (preset) => record(TYPE_PROPS, (prop) => varName("type", preset, prop))
1272
+ ),
1273
+ /** Max line length for running text (~70 characters). */
1274
+ measure: varName("measure"),
1275
+ space: record(SPACE_STEPS, (s) => varName("space", s)),
1276
+ section: {
1277
+ padY: varName("section", "pad-y"),
1278
+ blockGap: varName("section", "block-gap"),
1279
+ gutter: varName("section", "gutter")
1280
+ },
1281
+ width: {
1282
+ narrow: varName("width", "narrow"),
1283
+ content: varName("width", "content"),
1284
+ wide: varName("width", "wide")
1285
+ },
1286
+ radius: record(RADIUS_NAMES, (r) => varName("radius", r)),
1287
+ shadow: record(SHADOW_NAMES, (s) => varName("shadow", s)),
1288
+ /** Buttons: `--os-button-<primary|secondary|tertiary>-<prop>`. */
1289
+ button: record(
1290
+ BUTTON_ROLES,
1291
+ (role) => record(BUTTON_PROPS, (prop) => varName("button", role, prop))
1292
+ ),
1293
+ motion: record(MOTION_PROPS, (p) => varName("motion", p)),
1294
+ image: record(IMAGE_PROPS, (p) => varName("image", p))
1295
+ };
1296
+ function allTokenVars() {
1297
+ const out = [];
1298
+ const walk = (v) => {
1299
+ if (typeof v === "string") out.push(v);
1300
+ else if (v && typeof v === "object") for (const x of Object.values(v)) walk(x);
1301
+ };
1302
+ walk(TOKEN_VARS);
1303
+ return out;
1304
+ }
1305
+ var THEME_CLASSES = COLOR_THEMES2.map(themeClass);
1306
+
1307
+ // src/overrides.ts
1308
+ var isToken = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && "$value" in v;
1309
+ function flattenDtcg(group) {
1310
+ const out = [];
1311
+ const walk = (node, path, inherited) => {
1312
+ const groupType = typeof node.$type === "string" ? node.$type : inherited;
1313
+ for (const key of Object.keys(node).sort()) {
1314
+ if (key.startsWith("$")) continue;
1315
+ const child = node[key];
1316
+ if (isToken(child)) {
1317
+ out.push({ path: [...path, key], token: child, type: child.$type ?? groupType });
1318
+ } else if (typeof child === "object" && child !== null) {
1319
+ walk(child, [...path, key], groupType);
1320
+ }
1321
+ }
1322
+ };
1323
+ if (group) walk(group, [], void 0);
1324
+ return out;
1325
+ }
1326
+ var ALIAS_RE = /\{([^{}]+)\}/g;
1327
+ var SOLE_ALIAS_RE = /^\{([^{}]+)\}$/;
1328
+ function resolveAliases(value) {
1329
+ return value.replace(ALIAS_RE, (_, ref) => cssVar(...ref.split(".")));
1330
+ }
1331
+ var UNSAFE_RE = /[;{}<>\\]|\/\*|\*\/|url\s*\(|expression\s*\(|@import|!important|javascript:/i;
1332
+ function isSafeCssValue(value) {
1333
+ return value.trim().length > 0 && !UNSAFE_RE.test(value);
1334
+ }
1335
+ var UNIT_RE = /^(px|rem|em|%|vw|vh|svh|dvh|ch|ex|ms|s|deg|fr)$/;
1336
+ var quoteFamily2 = (f) => /^[a-z-]+$/i.test(f) || f.startsWith('"') ? f : `"${f.replace(/"/g, "")}"`;
1337
+ function shadowLayer(v) {
1338
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return null;
1339
+ const parts = ["offsetX", "offsetY", "blur", "spread"].map((k) => {
1340
+ const p = v[k];
1341
+ return p === void 0 ? "0" : scalar(p, "dimension");
1342
+ });
1343
+ const color = v.color;
1344
+ if (parts.some((p) => p === null) || typeof color !== "string") return null;
1345
+ const layer = `${parts.join(" ")} ${resolveAliases(color)}`;
1346
+ return v.inset === true ? `inset ${layer}` : layer;
1347
+ }
1348
+ function scalar(v, type) {
1349
+ if (typeof v === "string") return resolveAliases(v.trim());
1350
+ if (typeof v === "number") {
1351
+ if (!Number.isFinite(v)) return null;
1352
+ if (type === "dimension") return v === 0 ? "0" : `${v}px`;
1353
+ if (type === "duration") return `${v}ms`;
1354
+ return String(v);
1355
+ }
1356
+ if (typeof v === "object" && v !== null && !Array.isArray(v)) {
1357
+ const { value, unit } = v;
1358
+ if (typeof value === "number" && typeof unit === "string" && UNIT_RE.test(unit)) {
1359
+ return `${value}${unit}`;
1360
+ }
1361
+ }
1362
+ return null;
1363
+ }
1364
+ function dtcgToCss(value, type) {
1365
+ let out = null;
1366
+ if (Array.isArray(value)) {
1367
+ if (type === "cubicBezier" && value.length === 4 && value.every((n) => typeof n === "number")) {
1368
+ out = `cubic-bezier(${value.join(", ")})`;
1369
+ } else if (type === "fontFamily" || value.every((x) => typeof x === "string")) {
1370
+ out = value.map((f) => typeof f === "string" ? quoteFamily2(f) : "").join(", ");
1371
+ } else if (type === "shadow") {
1372
+ const layers = value.map(shadowLayer);
1373
+ out = layers.every((l) => l !== null) ? layers.join(", ") : null;
1374
+ }
1375
+ } else if (type === "shadow") {
1376
+ out = shadowLayer(value);
1377
+ } else if (type === "fontFamily" && typeof value === "string") {
1378
+ out = value.includes(",") ? value : quoteFamily2(value);
1379
+ } else {
1380
+ out = scalar(value, type);
1381
+ }
1382
+ if (out === null || !isSafeCssValue(out)) return null;
1383
+ if (type === "color" && !out.startsWith("var(") && !isColor(out)) return null;
1384
+ return out;
1385
+ }
1386
+
1387
+ // src/themes.ts
1388
+ import { COLOR_ROLES as COLOR_ROLES2, COLOR_THEMES as COLOR_THEMES3 } from "@omniharmonic/openspace-schema";
1389
+
1390
+ // src/scales.ts
1391
+ import { COLOR_SCALE_STEPS as COLOR_SCALE_STEPS2 } from "@omniharmonic/openspace-schema";
1392
+ var ACCENT_LIGHTNESS = {
1393
+ 50: 0.975,
1394
+ 100: 0.94,
1395
+ 200: 0.88,
1396
+ 300: 0.8,
1397
+ 400: 0.71,
1398
+ 500: 0.625,
1399
+ 600: 0.545,
1400
+ 700: 0.465,
1401
+ 800: 0.385,
1402
+ 900: 0.305,
1403
+ 950: 0.235
1404
+ };
1405
+ var ACCENT_CHROMA = {
1406
+ 50: 0.14,
1407
+ 100: 0.28,
1408
+ 200: 0.5,
1409
+ 300: 0.74,
1410
+ 400: 0.92,
1411
+ 500: 1,
1412
+ 600: 1,
1413
+ 700: 0.92,
1414
+ 800: 0.8,
1415
+ 900: 0.66,
1416
+ 950: 0.52
1417
+ };
1418
+ var NEUTRAL_LIGHTNESS = {
1419
+ 50: 0.985,
1420
+ 100: 0.962,
1421
+ 200: 0.922,
1422
+ 300: 0.86,
1423
+ 400: 0.72,
1424
+ 500: 0.6,
1425
+ 600: 0.5,
1426
+ 700: 0.415,
1427
+ 800: 0.325,
1428
+ 900: 0.245,
1429
+ 950: 0.185
1430
+ };
1431
+ var NEUTRAL_CHROMA = {
1432
+ 50: 0.45,
1433
+ 100: 0.55,
1434
+ 200: 0.65,
1435
+ 300: 0.75,
1436
+ 400: 0.85,
1437
+ 500: 0.9,
1438
+ 600: 0.95,
1439
+ 700: 1,
1440
+ 800: 1,
1441
+ 900: 1,
1442
+ 950: 1
1443
+ };
1444
+ var TINT_HUE = { warm: 70, cool: 250 };
1445
+ var toOklch = (c2) => typeof c2 === "string" ? parseColor(c2) : c2;
1446
+ function buildScale(fn) {
1447
+ const out = {};
1448
+ for (const step of COLOR_SCALE_STEPS2) out[step] = gamutMap(fn(step));
1449
+ return out;
1450
+ }
1451
+ function accentScale(brand) {
1452
+ const b = gamutMap(toOklch(brand));
1453
+ return buildScale((step) => ({
1454
+ l: ACCENT_LIGHTNESS[step],
1455
+ c: b.c * ACCENT_CHROMA[step],
1456
+ h: b.h
1457
+ }));
1458
+ }
1459
+ function neutralScale(brand, tint) {
1460
+ if (tint === "pure") return buildScale((step) => ({ l: NEUTRAL_LIGHTNESS[step], c: 0, h: 0 }));
1461
+ const b = gamutMap(toOklch(brand));
1462
+ const target = TINT_HUE[tint];
1463
+ const h = b.c < 0.02 ? target : mixHue(target, b.h, 0.35);
1464
+ const base = Math.min(0.018, Math.max(8e-3, b.c * 0.12));
1465
+ return buildScale((step) => ({ l: NEUTRAL_LIGHTNESS[step], c: base * NEUTRAL_CHROMA[step], h }));
1466
+ }
1467
+ function nearestStep(color) {
1468
+ const { l } = toOklch(color);
1469
+ let best = 500;
1470
+ for (const step of COLOR_SCALE_STEPS2) {
1471
+ if (Math.abs(ACCENT_LIGHTNESS[step] - l) < Math.abs(ACCENT_LIGHTNESS[best] - l)) best = step;
1472
+ }
1473
+ return best;
1474
+ }
1475
+
1476
+ // src/themes.ts
1477
+ function buildPalette(brand, neutral) {
1478
+ const b = gamutMap(typeof brand === "string" ? parseColor(brand) : brand);
1479
+ return { brand: b, accent: accentScale(b), neutral: neutralScale(b, neutral) };
1480
+ }
1481
+ var WHITE = { l: 1, c: 0, h: 0 };
1482
+ var BLACK = { l: 0, c: 0, h: 0 };
1483
+ var STATUS_HUE = { success: 150, warning: 70, danger: 27 };
1484
+ var status = (l, c2 = 0.14) => ({
1485
+ success: { l, c: c2, h: STATUS_HUE.success },
1486
+ warning: { l, c: c2, h: STATUS_HUE.warning },
1487
+ danger: { l, c: c2 + 0.04, h: STATUS_HUE.danger }
1488
+ });
1489
+ function bestOn(bg, candidates) {
1490
+ let best = candidates[0];
1491
+ for (const c2 of candidates) if (contrastRatio(c2, bg) > contrastRatio(best, bg)) best = c2;
1492
+ return best;
1493
+ }
1494
+ function onColor(bg, palette) {
1495
+ return bestOn(bg, [WHITE, palette.neutral[950]]);
1496
+ }
1497
+ var shift = (c2, dl) => gamutMap({ ...c2, l: c2.l + dl });
1498
+ function themeRoles(palette) {
1499
+ const { accent: a, neutral: n, brand } = palette;
1500
+ const onAccent = (bg) => onColor(bg, palette);
1501
+ const light = {
1502
+ background: n[50],
1503
+ surface: n[100],
1504
+ "surface-raised": WHITE,
1505
+ text: n[900],
1506
+ "text-muted": n[600],
1507
+ heading: n[950],
1508
+ accent: brand,
1509
+ "accent-text": onAccent(brand),
1510
+ border: n[200],
1511
+ focus: a[600],
1512
+ link: a[700],
1513
+ ...status(0.5)
1514
+ };
1515
+ const lightAccent = {
1516
+ background: a[50],
1517
+ surface: a[100],
1518
+ "surface-raised": n[50],
1519
+ text: n[900],
1520
+ "text-muted": n[700],
1521
+ heading: n[950],
1522
+ accent: a[600],
1523
+ "accent-text": onAccent(a[600]),
1524
+ border: a[200],
1525
+ focus: a[700],
1526
+ link: a[700],
1527
+ ...status(0.48)
1528
+ };
1529
+ const dark = {
1530
+ background: n[950],
1531
+ surface: n[900],
1532
+ "surface-raised": n[800],
1533
+ text: n[100],
1534
+ "text-muted": n[400],
1535
+ heading: n[50],
1536
+ accent: a[400],
1537
+ "accent-text": onAccent(a[400]),
1538
+ border: n[800],
1539
+ focus: a[300],
1540
+ link: a[300],
1541
+ ...status(0.78, 0.13)
1542
+ };
1543
+ const darkAccent = {
1544
+ background: a[950],
1545
+ surface: a[900],
1546
+ "surface-raised": a[800],
1547
+ text: a[50],
1548
+ "text-muted": a[200],
1549
+ heading: WHITE,
1550
+ accent: a[300],
1551
+ "accent-text": onAccent(a[300]),
1552
+ border: a[800],
1553
+ focus: a[200],
1554
+ link: a[200],
1555
+ ...status(0.82, 0.12)
1556
+ };
1557
+ const brandIsLight = contrastRatio(brand, BLACK) >= contrastRatio(brand, WHITE);
1558
+ const toward = brandIsLight ? -1 : 1;
1559
+ const ink = brandIsLight ? a[950] : a[50];
1560
+ const brandTheme = {
1561
+ background: brand,
1562
+ surface: shift(brand, -0.04 * toward),
1563
+ "surface-raised": shift(brand, -0.08 * toward),
1564
+ text: ink,
1565
+ "text-muted": brandIsLight ? a[900] : a[100],
1566
+ heading: brandIsLight ? n[950] : WHITE,
1567
+ accent: brandIsLight ? n[950] : WHITE,
1568
+ "accent-text": brand,
1569
+ border: shift(brand, 0.12 * toward),
1570
+ focus: ink,
1571
+ link: ink,
1572
+ ...status(brandIsLight ? 0.32 : 0.92, 0.1)
1573
+ };
1574
+ return {
1575
+ light,
1576
+ "light-accent": lightAccent,
1577
+ dark,
1578
+ "dark-accent": darkAccent,
1579
+ brand: brandTheme
1580
+ };
1581
+ }
1582
+ var POLARITY_HEADROOM = 1.2;
1583
+ var SURFACES = ["background", "surface", "surface-raised"];
1584
+ var SURFACE_SET = new Set(SURFACES);
1585
+ var HOVER_STEP = 0.06;
1586
+ function accentStates(accent, label) {
1587
+ const sign = label.l > accent.l ? -1 : 1;
1588
+ const step = (n) => {
1589
+ const next = shift(accent, sign * HOVER_STEP * n);
1590
+ return oklchToHex(next) === oklchToHex(accent) ? shift(accent, -sign * HOVER_STEP * n) : next;
1591
+ };
1592
+ return { "accent-hover": step(1), "accent-pressed": step(2) };
1593
+ }
1594
+ function solveThemes(roles, mode = "standard") {
1595
+ const mins = CONTRAST_MINIMUMS[mode];
1596
+ const adjustments = /* @__PURE__ */ new Map();
1597
+ const themes = {};
1598
+ for (const theme of COLOR_THEMES3) {
1599
+ const set = { ...roles[theme] };
1600
+ const hex = (role) => oklchToHex(set[role]);
1601
+ const record2 = (role, against, required, next) => {
1602
+ const key = `${theme}:${role}`;
1603
+ const prev = adjustments.get(key);
1604
+ const from = prev?.from ?? hex(role);
1605
+ set[role] = next;
1606
+ adjustments.set(key, { theme, role, from, to: hex(role), against, required });
1607
+ };
1608
+ const lightText = relativeLuminance(set.text) > relativeLuminance(set.background);
1609
+ const extreme = lightText ? WHITE : BLACK;
1610
+ const away = lightText ? "lighter" : "darker";
1611
+ const need = mins.body * POLARITY_HEADROOM;
1612
+ for (const surface of SURFACES) {
1613
+ if (contrastRatio(extreme, set[surface]) >= need) continue;
1614
+ const moved = solveLightness(set[surface], extreme, need);
1615
+ if (moved) record2(surface, "text", mins.body, moved);
1616
+ }
1617
+ const ownPairsPass = (role, candidate) => CORE_CONTRAST_PAIRS.every(
1618
+ (p) => p.fg !== role || contrastRatio(candidate, set[p.bg]) >= mins[p.level]
1619
+ );
1620
+ const solvePairs = (pairs, allowBgMoves) => {
1621
+ for (let pass = 0; pass < 10; pass++) {
1622
+ let changed = false;
1623
+ for (const { fg, bg, level, adjust } of pairs) {
1624
+ const required = mins[level];
1625
+ if (contrastRatio(hex(fg), hex(bg)) >= required) continue;
1626
+ const [move, fixed] = adjust === "bg" ? [bg, fg] : [fg, bg];
1627
+ const direction = SURFACE_SET.has(fixed) ? away : "either";
1628
+ const solved = solveLightness(set[move], set[fixed], required, direction);
1629
+ if (solved) {
1630
+ record2(move, fixed, required, solved);
1631
+ changed = true;
1632
+ continue;
1633
+ }
1634
+ if (!allowBgMoves) continue;
1635
+ let best = null;
1636
+ for (const end of [WHITE, BLACK]) {
1637
+ const fgEnd = gamutMap({ ...set[fg], l: end.l });
1638
+ const movedBg = solveLightness(set[bg], fgEnd, required);
1639
+ if (!movedBg || !ownPairsPass(bg, movedBg)) continue;
1640
+ const delta = Math.abs(movedBg.l - set[bg].l);
1641
+ if (best && delta >= best.delta) continue;
1642
+ best = {
1643
+ bg: movedBg,
1644
+ fg: solveLightness(set[fg], movedBg, required) ?? fgEnd,
1645
+ delta
1646
+ };
1647
+ }
1648
+ if (!best) continue;
1649
+ record2(bg, fg, required, best.bg);
1650
+ record2(fg, bg, required, best.fg);
1651
+ changed = true;
1652
+ }
1653
+ if (!changed) break;
1654
+ }
1655
+ };
1656
+ solvePairs(CORE_CONTRAST_PAIRS, true);
1657
+ for (const surface of SURFACES) {
1658
+ if (contrastRatio(set["text-muted"], set[surface]) > contrastRatio(set.text, set[surface])) {
1659
+ record2("text-muted", "text", mins.body, set.text);
1660
+ break;
1661
+ }
1662
+ }
1663
+ Object.assign(set, accentStates(set.accent, set["accent-text"]));
1664
+ solvePairs(STATE_PAIRS, false);
1665
+ if (hex("accent-hover") === hex("accent") || hex("accent-pressed") === hex("accent")) {
1666
+ const toward = set["accent-text"].l > set.accent.l ? 1 : -1;
1667
+ const pressed = shift(set.accent, toward * HOVER_STEP * 2);
1668
+ const label = solveLightness(
1669
+ set["accent-text"],
1670
+ pressed,
1671
+ mins.body,
1672
+ toward > 0 ? "lighter" : "darker"
1673
+ );
1674
+ if (label) {
1675
+ record2("accent-text", "accent-pressed", mins.body, label);
1676
+ set["accent-hover"] = shift(set.accent, toward * HOVER_STEP);
1677
+ set["accent-pressed"] = pressed;
1678
+ solvePairs(STATE_PAIRS, false);
1679
+ }
1680
+ }
1681
+ const out = {};
1682
+ for (const role of [...COLOR_ROLES2, ...STATE_ROLES]) out[role] = hex(role);
1683
+ themes[theme] = out;
1684
+ }
1685
+ const list = [...adjustments.values()].filter((a) => a.from !== a.to);
1686
+ return { themes, adjustments: list };
1687
+ }
1688
+
1689
+ // src/compile.ts
1690
+ var FALLBACK_BRAND = "oklch(0.55 0.12 250)";
1691
+ var COLOR_THEME_SET = new Set(COLOR_THEMES4);
1692
+ var COLOR_ROLE_SET = new Set(COLOR_ROLES3);
1693
+ var ALL_THEMES = "all";
1694
+ var THEME_ROLES = [...COLOR_ROLES3, ...STATE_ROLES];
1695
+ function scaleHex(scale) {
1696
+ const out = {};
1697
+ for (const step of COLOR_SCALE_STEPS3) out[step] = oklchToHex(scale[step]);
1698
+ return out;
1699
+ }
1700
+ function overrideColor(raw, palette, roles) {
1701
+ if (typeof raw !== "string") return null;
1702
+ const alias = SOLE_ALIAS_RE.exec(raw.trim());
1703
+ if (alias?.[1]) {
1704
+ const [group, name2] = alias[1].split(".");
1705
+ if (group !== "color" || !name2) return null;
1706
+ if (name2 === "brand") return palette.brand;
1707
+ const m = /^(accent|neutral)-(\d+)$/.exec(name2);
1708
+ if (m) {
1709
+ const scale = m[1] === "accent" ? palette.accent : palette.neutral;
1710
+ return scale[Number(m[2])] ?? null;
1711
+ }
1712
+ return COLOR_ROLE_SET.has(name2) ? roles[name2] : null;
1713
+ }
1714
+ try {
1715
+ return parseColor(raw);
1716
+ } catch {
1717
+ return null;
1718
+ }
1719
+ }
1720
+ function compileStyles(styles, options = {}) {
1721
+ const { choices } = styles;
1722
+ const warnings = [];
1723
+ const mode = choices.palette.contrast ?? "standard";
1724
+ let brand;
1725
+ try {
1726
+ brand = parseColor(choices.palette.brand);
1727
+ } catch {
1728
+ warnings.push({
1729
+ code: "invalid-brand",
1730
+ path: "choices.palette.brand",
1731
+ message: `Could not read brand color "${choices.palette.brand}"; using ${FALLBACK_BRAND}.`
1732
+ });
1733
+ brand = parseColor(FALLBACK_BRAND);
1734
+ }
1735
+ const palette = buildPalette(brand, choices.palette.neutral);
1736
+ let pairing2 = getPairing(choices.pairing);
1737
+ if (!pairing2) {
1738
+ warnings.push({
1739
+ code: "unknown-pairing",
1740
+ path: "choices.pairing",
1741
+ message: `Unknown type pairing "${choices.pairing}"; using "${DEFAULT_PAIRING_ID}".`
1742
+ });
1743
+ pairing2 = getPairing(DEFAULT_PAIRING_ID);
1744
+ }
1745
+ const accentHex = scaleHex(palette.accent);
1746
+ const neutralHex = scaleHex(palette.neutral);
1747
+ const root = /* @__PURE__ */ new Map();
1748
+ root.set(varName("color", "brand"), oklchToHex(palette.brand));
1749
+ for (const step of COLOR_SCALE_STEPS3) root.set(varName("color", "accent", step), accentHex[step]);
1750
+ for (const step of COLOR_SCALE_STEPS3)
1751
+ root.set(varName("color", "neutral", step), neutralHex[step]);
1752
+ const fonts = fontTokens(pairing2);
1753
+ const headingFont = chosenCustomFont(styles, choices.headingFont);
1754
+ if (headingFont) {
1755
+ fonts.heading = customFontStack(headingFont);
1756
+ fonts["weight-heading"] = String(nearestWeight(headingFont, pairing2.headingWeight));
1757
+ }
1758
+ const bodyFont = chosenCustomFont(styles, choices.bodyFont);
1759
+ if (bodyFont) {
1760
+ fonts.body = customFontStack(bodyFont);
1761
+ fonts["weight-body"] = String(nearestWeight(bodyFont, pairing2.bodyWeight));
1762
+ fonts["weight-strong"] = String(nearestWeight(bodyFont, pairing2.strongWeight));
1763
+ }
1764
+ for (const [k, v] of Object.entries(fonts)) root.set(varName("font", k), v);
1765
+ const type = typeScale(pairing2);
1766
+ for (const preset of TYPE_PRESET_NAMES) {
1767
+ for (const [prop, v] of Object.entries(type[preset]))
1768
+ root.set(varName("type", preset, prop), v);
1769
+ }
1770
+ root.set(varName("measure"), "70ch");
1771
+ for (const [k, v] of Object.entries(spaceTokens(choices.density)))
1772
+ root.set(varName("space", k), v);
1773
+ for (const [k, v] of Object.entries(sectionTokens(choices.density)))
1774
+ root.set(varName("section", k), v);
1775
+ for (const [k, v] of Object.entries(WIDTH_TOKENS)) root.set(varName("width", k), v);
1776
+ for (const [k, v] of Object.entries(radiusTokens(choices.shape)))
1777
+ root.set(varName("radius", k), v);
1778
+ for (const [k, v] of Object.entries(SHADOW_VALUES)) root.set(varName("shadow", k), v);
1779
+ for (const [k, v] of Object.entries(MOTION_VALUES[choices.motion]))
1780
+ root.set(varName("motion", k), v);
1781
+ const themed = /* @__PURE__ */ new Map();
1782
+ const buttons = buttonTokens(choices.buttons);
1783
+ for (const role of BUTTON_ROLES) {
1784
+ for (const [prop, v] of Object.entries(buttons[role]))
1785
+ themed.set(varName("button", role, prop), v);
1786
+ }
1787
+ for (const [k, v] of Object.entries(IMAGE_TREATMENT_VALUES[choices.imageTreatment]))
1788
+ themed.set(varName("image", k), v);
1789
+ const roles = themeRoles(palette);
1790
+ const custom = /* @__PURE__ */ new Map();
1791
+ const overriddenRoles = /* @__PURE__ */ new Map();
1792
+ const roleOverrides = [];
1793
+ for (const { path, token, type: t } of flattenDtcg(styles.overrides)) {
1794
+ const dotted = path.join(".");
1795
+ const invalid = (why) => warnings.push({ code: "invalid-override", path: dotted, message: why });
1796
+ if (path[0] === "color") {
1797
+ const [, a, b] = path;
1798
+ const scope = path.length === 3 && a && (a === ALL_THEMES || COLOR_THEME_SET.has(a)) ? a : null;
1799
+ const role = scope ? b : path.length === 2 ? a : void 0;
1800
+ if (role && COLOR_ROLE_SET.has(role)) {
1801
+ const tier = scope === ALL_THEMES ? 0 : scope ? 2 : 1;
1802
+ roleOverrides.push({
1803
+ tier,
1804
+ scope: scope ?? "light",
1805
+ role,
1806
+ dotted,
1807
+ value: token.$value
1808
+ });
1809
+ continue;
1810
+ }
1811
+ }
1812
+ if (t === "typography" && typeof token.$value === "object" && token.$value !== null) {
1813
+ for (const [prop, sub] of Object.entries(token.$value).sort()) {
1814
+ const subType = prop === "fontFamily" ? "fontFamily" : prop === "fontSize" ? "dimension" : void 0;
1815
+ const css2 = dtcgToCss(sub, subType);
1816
+ if (css2 === null) invalid(`Unsupported value for ${prop}.`);
1817
+ else applyVar(varName(...path, prop), css2);
1818
+ }
1819
+ continue;
1820
+ }
1821
+ const colorPath = path[0] === "color";
1822
+ const css = dtcgToCss(token.$value, t ?? (colorPath ? "color" : void 0));
1823
+ if (css === null) {
1824
+ invalid(`Unsupported or unsafe value ${JSON.stringify(token.$value)}.`);
1825
+ continue;
1826
+ }
1827
+ applyVar(varName(...path), css);
1828
+ }
1829
+ function applyVar(name2, css) {
1830
+ if (root.has(name2)) root.set(name2, css);
1831
+ else if (themed.has(name2)) themed.set(name2, css);
1832
+ else {
1833
+ custom.set(name2, css);
1834
+ warnings.push({
1835
+ code: "custom-token",
1836
+ path: name2,
1837
+ message: `${name2} is not a built-in token; emitted as a custom property.`
1838
+ });
1839
+ }
1840
+ }
1841
+ for (const name2 of [...custom.keys()].sort()) root.set(name2, custom.get(name2));
1842
+ for (const o of roleOverrides.sort((x, y) => x.tier - y.tier)) {
1843
+ const targets = o.scope === ALL_THEMES ? COLOR_THEMES4 : [o.scope];
1844
+ for (const target of targets) {
1845
+ const color = overrideColor(o.value, palette, roles[target]);
1846
+ if (!color) {
1847
+ warnings.push({
1848
+ code: "invalid-override",
1849
+ path: o.dotted,
1850
+ message: `"${String(o.value)}" is not a color.`
1851
+ });
1852
+ break;
1853
+ }
1854
+ roles[target][o.role] = color;
1855
+ overriddenRoles.set(`${target}:${o.role}`, o.dotted);
1856
+ }
1857
+ }
1858
+ for (const theme of COLOR_THEMES4) {
1859
+ if (overriddenRoles.has(`${theme}:accent`) && !overriddenRoles.has(`${theme}:accent-text`)) {
1860
+ roles[theme]["accent-text"] = onColor(roles[theme].accent, palette);
1861
+ }
1862
+ }
1863
+ let themes;
1864
+ let adjustments = [];
1865
+ if (options.solve === false) {
1866
+ themes = {};
1867
+ for (const theme of COLOR_THEMES4) {
1868
+ const set = roles[theme];
1869
+ const all = { ...set, ...accentStates(set.accent, set["accent-text"]) };
1870
+ const out = {};
1871
+ for (const role of THEME_ROLES) out[role] = oklchToHex(all[role]);
1872
+ themes[theme] = out;
1873
+ }
1874
+ } else {
1875
+ ({ themes, adjustments } = solveThemes(roles, mode));
1876
+ }
1877
+ const failures = auditContrast(themes, { contrast: mode });
1878
+ for (const a of adjustments) {
1879
+ const path = overriddenRoles.get(`${a.theme}:${a.role}`);
1880
+ if (!path) continue;
1881
+ warnings.push({
1882
+ code: "contrast-adjusted",
1883
+ path,
1884
+ message: `Override ${a.from} adjusted to ${a.to} to reach ${a.required}:1 against ${a.against} (${a.theme}).`
1885
+ });
1886
+ }
1887
+ for (const f of failures) {
1888
+ warnings.push({
1889
+ code: "contrast-failure",
1890
+ path: `${f.theme}.${f.fg}/${f.bg}`,
1891
+ message: `${f.fg} on ${f.bg} is ${f.ratio}:1 in ${f.theme}; needs ${f.required}:1 (try ${f.suggestion}).`
1892
+ });
1893
+ }
1894
+ const themeVars = {};
1895
+ for (const theme of COLOR_THEMES4) {
1896
+ const vars = {};
1897
+ for (const role of THEME_ROLES) vars[varName("color", role)] = themes[theme][role];
1898
+ themeVars[theme] = vars;
1899
+ }
1900
+ const imageTreatments = {};
1901
+ for (const [treatment, values] of Object.entries(IMAGE_TREATMENT_VALUES)) {
1902
+ imageTreatments[treatment] = Object.fromEntries(
1903
+ Object.entries(values).map(([k, v]) => [varName("image", k), v])
1904
+ );
1905
+ }
1906
+ const reducedMotion = Object.fromEntries(
1907
+ Object.entries(MOTION_VALUES.none).filter(([k]) => k !== "ease").map(([k, v]) => [varName("motion", k), v])
1908
+ );
1909
+ const tokens = {
1910
+ choices,
1911
+ pairing: pairing2,
1912
+ palette: {
1913
+ brand: oklchToHex(palette.brand),
1914
+ brandOklch: formatOklch(palette.brand),
1915
+ accent: accentHex,
1916
+ neutral: neutralHex
1917
+ },
1918
+ themes,
1919
+ fonts: { googleFontsUrl: googleFontsUrl(pairing2), fontsource: fontsourceCss(pairing2) },
1920
+ vars: {
1921
+ root: Object.fromEntries(root),
1922
+ themes: themeVars,
1923
+ themed: Object.fromEntries(themed),
1924
+ imageTreatments,
1925
+ reducedMotion
1926
+ }
1927
+ };
1928
+ return {
1929
+ tokens,
1930
+ css: emitCss(tokens),
1931
+ contrast: { mode, adjustments, failures },
1932
+ warnings
1933
+ };
1934
+ }
1935
+ function block(selectors, decls, indent = "") {
1936
+ const lines = Object.entries(decls).map(([k, v]) => `${indent} ${k}: ${v};`);
1937
+ return `${indent}${selectors.join(`,
1938
+ ${indent}`)} {
1939
+ ${lines.join("\n")}
1940
+ ${indent}}`;
1941
+ }
1942
+ function emitCss(tokens) {
1943
+ const parts = [
1944
+ "/* OpenSpace design tokens. Generated by @omniharmonic/openspace-theme from content/styles.json; do not edit. */",
1945
+ block([":root"], tokens.vars.root)
1946
+ ];
1947
+ for (const theme of COLOR_THEMES4) {
1948
+ const scheme = relativeLuminance(tokens.themes[theme].background) > 0.18 ? "light" : "dark";
1949
+ const selectors = theme === "light" ? [":root", `.${themeClass(theme)}`] : [`.${themeClass(theme)}`];
1950
+ parts.push(block(selectors, { "color-scheme": scheme, ...tokens.vars.themes[theme] }));
1951
+ }
1952
+ parts.push(block([":root", ...THEME_CLASSES.map((c2) => `.${c2}`)], tokens.vars.themed));
1953
+ for (const [treatment, decls] of Object.entries(tokens.vars.imageTreatments)) {
1954
+ parts.push(block([`.${imageTreatmentClass(treatment)}`], decls));
1955
+ }
1956
+ parts.push(
1957
+ `@media (prefers-reduced-motion: reduce) {
1958
+ ${block([":root"], tokens.vars.reducedMotion, " ")}
1959
+ }`
1960
+ );
1961
+ return `${parts.join("\n\n")}
1962
+ `;
1963
+ }
1964
+
1965
+ // src/extract.ts
1966
+ var LINEAR = Array.from({ length: 256 }, (_, i) => srgbChannelToLinear(i / 255));
1967
+ var dist2 = (a, b) => (a.l - b.l) ** 2 + (a.a - b.a) ** 2 + (a.b - b.b) ** 2;
1968
+ function extractPalette(pixels, options = {}) {
1969
+ const channels = options.channels ?? 4;
1970
+ const k = Math.max(1, options.k ?? 6);
1971
+ const count = options.count ?? 5;
1972
+ const total = Math.floor(pixels.length / channels);
1973
+ const stride = Math.max(1, Math.ceil(total / (options.maxSamples ?? 2e4)));
1974
+ const samples = [];
1975
+ const weights = [];
1976
+ for (let i = 0; i < total; i += stride) {
1977
+ const o = i * channels;
1978
+ if (channels === 4 && (pixels[o + 3] ?? 255) < 128) continue;
1979
+ const lab = linearSrgbToOklab({
1980
+ r: LINEAR[pixels[o] ?? 0],
1981
+ g: LINEAR[pixels[o + 1] ?? 0],
1982
+ b: LINEAR[pixels[o + 2] ?? 0]
1983
+ });
1984
+ samples.push(lab);
1985
+ weights.push(1 + 12 * Math.hypot(lab.a, lab.b));
1986
+ }
1987
+ if (samples.length === 0) return [];
1988
+ let first = 0;
1989
+ for (let i = 1; i < samples.length; i++) if ((weights[i] ?? 0) > (weights[first] ?? 0)) first = i;
1990
+ const centers = [{ ...samples[first] }];
1991
+ const nearest = samples.map((s) => dist2(s, centers[0]));
1992
+ while (centers.length < Math.min(k, samples.length)) {
1993
+ let far = 0;
1994
+ for (let i = 1; i < samples.length; i++) {
1995
+ if ((nearest[i] ?? 0) * (weights[i] ?? 0) > (nearest[far] ?? 0) * (weights[far] ?? 0))
1996
+ far = i;
1997
+ }
1998
+ if ((nearest[far] ?? 0) === 0) break;
1999
+ const c2 = { ...samples[far] };
2000
+ centers.push(c2);
2001
+ for (let i = 0; i < samples.length; i++) {
2002
+ nearest[i] = Math.min(nearest[i] ?? 0, dist2(samples[i], c2));
2003
+ }
2004
+ }
2005
+ const assign = new Array(samples.length).fill(0);
2006
+ for (let iter = 0; iter < 16; iter++) {
2007
+ let moved = false;
2008
+ for (let i = 0; i < samples.length; i++) {
2009
+ let best = 0;
2010
+ let bestD = Number.POSITIVE_INFINITY;
2011
+ for (let j = 0; j < centers.length; j++) {
2012
+ const d = dist2(samples[i], centers[j]);
2013
+ if (d < bestD) {
2014
+ bestD = d;
2015
+ best = j;
2016
+ }
2017
+ }
2018
+ if (assign[i] !== best) {
2019
+ assign[i] = best;
2020
+ moved = true;
2021
+ }
2022
+ }
2023
+ const sums = centers.map(() => ({ l: 0, a: 0, b: 0, w: 0 }));
2024
+ for (let i = 0; i < samples.length; i++) {
2025
+ const s = samples[i];
2026
+ const w = weights[i] ?? 1;
2027
+ const acc = sums[assign[i] ?? 0];
2028
+ if (!acc) continue;
2029
+ acc.l += s.l * w;
2030
+ acc.a += s.a * w;
2031
+ acc.b += s.b * w;
2032
+ acc.w += w;
2033
+ }
2034
+ sums.forEach((acc, j) => {
2035
+ if (acc.w > 0) centers[j] = { l: acc.l / acc.w, a: acc.a / acc.w, b: acc.b / acc.w };
2036
+ });
2037
+ if (!moved && iter > 0) break;
2038
+ }
2039
+ const counts = centers.map(() => 0);
2040
+ for (const a of assign) counts[a] = (counts[a] ?? 0) + 1;
2041
+ const candidates = [];
2042
+ centers.forEach((center, j) => {
2043
+ const n = counts[j] ?? 0;
2044
+ if (n === 0) return;
2045
+ const lch = oklabToOklch(center);
2046
+ const population = n / samples.length;
2047
+ const chromaWeight = Math.min(1, lch.c / 0.12);
2048
+ const extreme = lch.l > 0.96 || lch.l < 0.12 ? 0.05 : 1;
2049
+ const score = population * (0.04 + chromaWeight) * extreme;
2050
+ candidates.push({
2051
+ hex: oklchToHex(lch),
2052
+ oklch: formatOklch(lch),
2053
+ l: lch.l,
2054
+ c: lch.c,
2055
+ h: lch.h,
2056
+ population,
2057
+ score
2058
+ });
2059
+ });
2060
+ candidates.sort((a, b) => b.score - a.score || a.hex.localeCompare(b.hex));
2061
+ const out = [];
2062
+ for (const c2 of candidates) if (!out.some((o) => o.hex === c2.hex)) out.push(c2);
2063
+ return out.slice(0, count);
2064
+ }
2065
+
2066
+ // src/palettes.ts
2067
+ var curatedPalettes = [
2068
+ {
2069
+ id: "sage",
2070
+ name: "Oat & Sage",
2071
+ brand: "oklch(0.62 0.07 145)",
2072
+ neutral: "warm",
2073
+ description: "Soft sage on oat paper; calm and organic.",
2074
+ templates: ["meadow"]
2075
+ },
2076
+ {
2077
+ id: "clay",
2078
+ name: "Clay",
2079
+ brand: "oklch(0.6 0.11 45)",
2080
+ neutral: "warm",
2081
+ description: "Sun-baked terracotta with bone neutrals.",
2082
+ templates: ["meadow", "kiln"]
2083
+ },
2084
+ {
2085
+ id: "harbor",
2086
+ name: "Harbor Blue",
2087
+ brand: "oklch(0.42 0.12 255)",
2088
+ neutral: "warm",
2089
+ description: "Deep civic blue on warm paper.",
2090
+ templates: ["harbor"]
2091
+ },
2092
+ {
2093
+ id: "signal-orange",
2094
+ name: "Signal Orange",
2095
+ brand: "oklch(0.68 0.19 45)",
2096
+ neutral: "warm",
2097
+ description: "A bright call-to-action orange.",
2098
+ templates: ["harbor"]
2099
+ },
2100
+ {
2101
+ id: "monochrome",
2102
+ name: "Monochrome",
2103
+ brand: "oklch(0.25 0 0)",
2104
+ neutral: "pure",
2105
+ description: "Ink and white; lets the photography lead.",
2106
+ templates: ["atelier"]
2107
+ },
2108
+ {
2109
+ id: "ledger-green",
2110
+ name: "Ledger Green",
2111
+ brand: "oklch(0.42 0.08 160)",
2112
+ neutral: "warm",
2113
+ description: "Deep green with ink and ivory.",
2114
+ templates: ["ledger"]
2115
+ },
2116
+ {
2117
+ id: "tomato",
2118
+ name: "Tomato",
2119
+ brand: "oklch(0.6 0.2 30)",
2120
+ neutral: "warm",
2121
+ description: "Ripe tomato red on cream.",
2122
+ templates: ["hearth"]
2123
+ },
2124
+ {
2125
+ id: "olive",
2126
+ name: "Olive Grove",
2127
+ brand: "oklch(0.55 0.09 115)",
2128
+ neutral: "warm",
2129
+ description: "Olive and cream, quietly appetizing.",
2130
+ templates: ["hearth"]
2131
+ },
2132
+ {
2133
+ id: "electric",
2134
+ name: "Electric Violet",
2135
+ brand: "oklch(0.62 0.25 285)",
2136
+ neutral: "cool",
2137
+ description: "Near-black chrome with an electric accent.",
2138
+ templates: ["signal"]
2139
+ },
2140
+ {
2141
+ id: "highlighter",
2142
+ name: "Highlighter",
2143
+ brand: "oklch(0.9 0.17 100)",
2144
+ neutral: "warm",
2145
+ description: "Paper and ink with a highlighter-yellow mark.",
2146
+ templates: ["field-notes"]
2147
+ },
2148
+ {
2149
+ id: "plum",
2150
+ name: "Night Plum",
2151
+ brand: "oklch(0.45 0.14 330)",
2152
+ neutral: "cool",
2153
+ description: "Dark plum with a vivid edge.",
2154
+ templates: ["encore"]
2155
+ },
2156
+ {
2157
+ id: "charcoal-clay",
2158
+ name: "Charcoal & Clay",
2159
+ brand: "oklch(0.52 0.08 50)",
2160
+ neutral: "warm",
2161
+ description: "Fired clay against charcoal and bone.",
2162
+ templates: ["kiln"]
2163
+ },
2164
+ {
2165
+ id: "earth",
2166
+ name: "Earth & Sky",
2167
+ brand: "oklch(0.5 0.1 140)",
2168
+ neutral: "cool",
2169
+ description: "Grounded greens under an open sky.",
2170
+ templates: ["commons"]
2171
+ },
2172
+ {
2173
+ id: "dusty-rose",
2174
+ name: "Dusty Rose",
2175
+ brand: "oklch(0.68 0.07 10)",
2176
+ neutral: "warm",
2177
+ description: "Linen, ink and a muted rose.",
2178
+ templates: ["vow"]
2179
+ },
2180
+ {
2181
+ id: "lagoon",
2182
+ name: "Lagoon",
2183
+ brand: "oklch(0.58 0.1 195)",
2184
+ neutral: "cool",
2185
+ description: "Clear teal water; fresh and open.",
2186
+ templates: ["link"]
2187
+ },
2188
+ {
2189
+ id: "saffron",
2190
+ name: "Saffron",
2191
+ brand: "oklch(0.78 0.15 80)",
2192
+ neutral: "warm",
2193
+ description: "Golden saffron, warm and generous.",
2194
+ templates: ["link"]
2195
+ },
2196
+ {
2197
+ id: "ink-blue",
2198
+ name: "Ink Blue",
2199
+ brand: "oklch(0.35 0.08 265)",
2200
+ neutral: "cool",
2201
+ description: "Fountain-pen blue; serious and literate.",
2202
+ templates: ["field-notes", "ledger"]
2203
+ },
2204
+ {
2205
+ id: "lavender",
2206
+ name: "Lavender",
2207
+ brand: "oklch(0.7 0.1 300)",
2208
+ neutral: "cool",
2209
+ description: "Gentle lavender for soft, restful brands."
2210
+ },
2211
+ {
2212
+ id: "forest",
2213
+ name: "Forest",
2214
+ brand: "oklch(0.4 0.07 150)",
2215
+ neutral: "warm",
2216
+ description: "Old-growth green, steady and natural.",
2217
+ templates: ["commons"]
2218
+ },
2219
+ {
2220
+ id: "coral",
2221
+ name: "Coral",
2222
+ brand: "oklch(0.7 0.15 25)",
2223
+ neutral: "warm",
2224
+ description: "Playful coral with sunny neutrals.",
2225
+ templates: ["link"]
2226
+ }
2227
+ ];
2228
+ function getCuratedPalette(id) {
2229
+ return curatedPalettes.find((p) => p.id === id);
2230
+ }
2231
+
2232
+ // src/index.ts
2233
+ var name = "@omniharmonic/openspace-theme";
2234
+ export {
2235
+ ACCENT_LIGHTNESS,
2236
+ ACHROMATIC_CHROMA,
2237
+ ALL_THEMES,
2238
+ BUTTON_PROPS,
2239
+ BUTTON_ROLES,
2240
+ CONTRAST_MINIMUMS,
2241
+ CONTRAST_PAIRS,
2242
+ CORE_CONTRAST_PAIRS,
2243
+ DEFAULT_PAIRING_ID,
2244
+ FALLBACK_STACKS,
2245
+ FLUID_TYPE,
2246
+ IMAGE_PROPS,
2247
+ IMAGE_TREATMENT_VALUES,
2248
+ MIN_FONT_SIZE_PX,
2249
+ MOTION_PROPS,
2250
+ MOTION_VALUES,
2251
+ NEUTRAL_LIGHTNESS,
2252
+ RADIUS_NAMES,
2253
+ RADIUS_SCALES,
2254
+ SHADOW_NAMES,
2255
+ SHADOW_VALUES,
2256
+ SOLE_ALIAS_RE,
2257
+ SPACE_SCALES,
2258
+ SPACE_STEPS,
2259
+ STATE_PAIRS,
2260
+ THEME_CLASSES,
2261
+ TOKEN_VARS,
2262
+ TYPE_PAIRINGS,
2263
+ TYPE_PRESET_NAMES,
2264
+ TYPE_PROPS,
2265
+ VAR_PREFIX,
2266
+ WIDTH_TOKENS,
2267
+ accentScale,
2268
+ accentStates,
2269
+ allTokenVars,
2270
+ auditContrast,
2271
+ bestOn,
2272
+ buildPalette,
2273
+ buttonTokens,
2274
+ chosenCustomFont,
2275
+ colorVar,
2276
+ compileStyles,
2277
+ contrastRatio,
2278
+ cssVar,
2279
+ curatedPalettes,
2280
+ customFontFaces,
2281
+ customFontPreloads,
2282
+ customFontStack,
2283
+ deltaEOK,
2284
+ dtcgToCss,
2285
+ emitCss,
2286
+ extractPalette,
2287
+ flattenDtcg,
2288
+ fluidClamp,
2289
+ fontStack,
2290
+ fontTokens,
2291
+ fontsourceCss,
2292
+ fontsourceId,
2293
+ fontsourcePackages,
2294
+ formatOklch,
2295
+ gamutMap,
2296
+ getCuratedPalette,
2297
+ getPairing,
2298
+ googleFontsUrl,
2299
+ hexToOklch,
2300
+ hexToRgb,
2301
+ imageTreatmentClass,
2302
+ inSrgbGamut,
2303
+ isColor,
2304
+ isSafeCssValue,
2305
+ linearChannelToSrgb,
2306
+ linearSrgbToOklab,
2307
+ linearToSrgb,
2308
+ mixHue,
2309
+ name,
2310
+ nearestAccessible,
2311
+ nearestStep,
2312
+ nearestWeight,
2313
+ neutralScale,
2314
+ normalizeHue,
2315
+ oklabToLinearSrgb,
2316
+ oklabToOklch,
2317
+ oklchToHex,
2318
+ oklchToOklab,
2319
+ oklchToSrgb,
2320
+ oklchToSrgbInGamut,
2321
+ onColor,
2322
+ pairingFamilies,
2323
+ parseColor,
2324
+ radiusTokens,
2325
+ relativeLuminance,
2326
+ resolveAliases,
2327
+ rgbToHex,
2328
+ sectionTokens,
2329
+ solveLightness,
2330
+ solveThemes,
2331
+ spaceTokens,
2332
+ srgbChannelToLinear,
2333
+ srgbToLinear,
2334
+ srgbToOklch,
2335
+ stepSizes,
2336
+ themeClass,
2337
+ themeRoles,
2338
+ toHex,
2339
+ typeScale,
2340
+ varName
2341
+ };
2342
+ //# sourceMappingURL=index.js.map