@rojaostudio/ds-core 1.0.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.
@@ -0,0 +1,898 @@
1
+ import { primitives } from './chunk-MJA5ZGMI.js';
2
+ import { __spreadValues, __spreadProps } from './chunk-ORMEWXMH.js';
3
+
4
+ // generate/scale.ts
5
+ var SCALE_STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
6
+ var L_T = {
7
+ 50: 0.985,
8
+ 100: 0.945,
9
+ 200: 0.875,
10
+ 300: 0.785,
11
+ 400: 0.675,
12
+ 500: 0.565,
13
+ 600: 0.455,
14
+ 700: 0.355,
15
+ 800: 0.255,
16
+ 900: 0.16
17
+ };
18
+ var C_MUL = {
19
+ 50: 0.42,
20
+ 100: 0.58,
21
+ 200: 0.78,
22
+ 300: 0.9,
23
+ 400: 0.97,
24
+ 500: 1,
25
+ 600: 1,
26
+ 700: 1,
27
+ 800: 0.96,
28
+ 900: 0.9
29
+ };
30
+ function hexToHsl(hex) {
31
+ const m = hex.replace("#", "");
32
+ const r = parseInt(m.slice(0, 2), 16) / 255;
33
+ const g = parseInt(m.slice(2, 4), 16) / 255;
34
+ const b = parseInt(m.slice(4, 6), 16) / 255;
35
+ const max = Math.max(r, g, b);
36
+ const min = Math.min(r, g, b);
37
+ const l = (max + min) / 2;
38
+ if (max === min) return [0, 0, Math.round(l * 100)];
39
+ const d = max - min;
40
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
41
+ let h = 0;
42
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
43
+ else if (max === g) h = ((b - r) / d + 2) / 6;
44
+ else h = ((r - g) / d + 4) / 6;
45
+ return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
46
+ }
47
+ function hslToHex(h, s, l) {
48
+ const sn = s / 100;
49
+ const ln = l / 100;
50
+ const a = sn * Math.min(ln, 1 - ln);
51
+ const f = (n) => {
52
+ const k = (n + h / 30) % 12;
53
+ const c = ln - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
54
+ return Math.round(255 * c).toString(16).padStart(2, "0");
55
+ };
56
+ return `#${f(0)}${f(8)}${f(4)}`;
57
+ }
58
+ var DEG = 180 / Math.PI;
59
+ var RAD = Math.PI / 180;
60
+ var toLin = (c) => {
61
+ const s = c / 255;
62
+ return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
63
+ };
64
+ var toGam = (c) => {
65
+ const s = c <= 31308e-7 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
66
+ return Math.round(Math.max(0, Math.min(1, s)) * 255);
67
+ };
68
+ function hexToOklch(hex) {
69
+ const m = hex.replace("#", "");
70
+ const r = toLin(parseInt(m.slice(0, 2), 16));
71
+ const g = toLin(parseInt(m.slice(2, 4), 16));
72
+ const b = toLin(parseInt(m.slice(4, 6), 16));
73
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
74
+ const mm = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
75
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
76
+ const L = 0.2104542553 * l + 0.793617785 * mm - 0.0040720468 * s;
77
+ const A = 1.9779984951 * l - 2.428592205 * mm + 0.4505937099 * s;
78
+ const B = 0.0259040371 * l + 0.7827717662 * mm - 0.808675766 * s;
79
+ return [L, Math.hypot(A, B), Math.atan2(B, A)];
80
+ }
81
+ function oklchToLinRgb(L, C, h) {
82
+ const A = C * Math.cos(h), B = C * Math.sin(h);
83
+ const l = (L + 0.3963377774 * A + 0.2158037573 * B) ** 3;
84
+ const m = (L - 0.1055613458 * A - 0.0638541728 * B) ** 3;
85
+ const s = (L - 0.0894841775 * A - 1.291485548 * B) ** 3;
86
+ return [
87
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
88
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
89
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
90
+ ];
91
+ }
92
+ var inGamut = (rgb) => rgb.every((c) => c >= -1e-3 && c <= 1.001);
93
+ function oklchToHex(L, C, h) {
94
+ if (!inGamut(oklchToLinRgb(L, C, h))) {
95
+ let lo = 0, hi = C;
96
+ for (let i = 0; i < 18; i++) {
97
+ const mid = (lo + hi) / 2;
98
+ if (inGamut(oklchToLinRgb(L, mid, h))) lo = mid;
99
+ else hi = mid;
100
+ }
101
+ C = lo;
102
+ }
103
+ const [r, g, b] = oklchToLinRgb(L, C, h);
104
+ return `#${[toGam(r), toGam(g), toGam(b)].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
105
+ }
106
+ function warmShiftDeg(hDeg, L, Lanchor) {
107
+ if (hDeg < 60 || hDeg > 115 || L >= Lanchor) return hDeg;
108
+ const t = Math.min(1, (Lanchor - L) / (Lanchor - 0.35));
109
+ return hDeg - t * (hDeg - 55) * 0.9;
110
+ }
111
+ function warmFloor(hDeg) {
112
+ if (hDeg >= 95 && hDeg <= 115) return 0.62;
113
+ if (hDeg >= 45 && hDeg < 95) return 0.255 + (hDeg - 45) / 50 * (0.62 - 0.255);
114
+ return 0.255;
115
+ }
116
+ function nearestStep(lOklch) {
117
+ let best = SCALE_STEPS[0];
118
+ let bestDiff = Infinity;
119
+ for (const step2 of SCALE_STEPS) {
120
+ const diff = Math.abs(L_T[step2] - lOklch);
121
+ if (diff < bestDiff) {
122
+ bestDiff = diff;
123
+ best = step2;
124
+ }
125
+ }
126
+ return best;
127
+ }
128
+ function buildScale(inputHex) {
129
+ const [Li, Ci, hRad] = hexToOklch(inputHex);
130
+ const hDeg = (hRad * DEG % 360 + 360) % 360;
131
+ const floor = Ci < 0.08 ? 0.16 : warmFloor(hDeg);
132
+ const lAt = (s) => floor + (L_T[s] - 0.16) / 0.825 * (0.985 - floor);
133
+ let anchor = SCALE_STEPS[0], bd = Infinity;
134
+ for (const s of SCALE_STEPS) {
135
+ const d = Math.abs(lAt(s) - Li);
136
+ if (d < bd) {
137
+ bd = d;
138
+ anchor = s;
139
+ }
140
+ }
141
+ const isNeutral = Ci < 0.02;
142
+ const out = {};
143
+ for (const step2 of SCALE_STEPS) {
144
+ if (step2 === anchor) {
145
+ out[step2] = inputHex.toLowerCase();
146
+ continue;
147
+ }
148
+ const L = lAt(step2);
149
+ const h = warmShiftDeg(hDeg, L, Li) * RAD;
150
+ const cMul = isNeutral && step2 <= 200 ? 0 : C_MUL[step2];
151
+ out[step2] = oklchToHex(L, Ci * cMul, h);
152
+ }
153
+ return out;
154
+ }
155
+ function relativeLuminance(hex) {
156
+ const m = hex.replace("#", "");
157
+ const channel = (v) => {
158
+ const sr = v / 255;
159
+ return sr <= 0.03928 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
160
+ };
161
+ const r = channel(parseInt(m.slice(0, 2), 16));
162
+ const g = channel(parseInt(m.slice(2, 4), 16));
163
+ const b = channel(parseInt(m.slice(4, 6), 16));
164
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
165
+ }
166
+ function contrastRatio(a, b) {
167
+ const la = relativeLuminance(a);
168
+ const lb = relativeLuminance(b);
169
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
170
+ return (hi + 0.05) / (lo + 0.05);
171
+ }
172
+ function onColor(bgHex, scale) {
173
+ const candidates = ["#ffffff", "#000000", scale == null ? void 0 : scale[50], scale == null ? void 0 : scale[900]].filter(
174
+ (c) => !!c
175
+ );
176
+ let best = candidates[0];
177
+ let bestRatio = 0;
178
+ for (const c of candidates) {
179
+ const ratio = contrastRatio(bgHex, c);
180
+ if (ratio > bestRatio) {
181
+ bestRatio = ratio;
182
+ best = c;
183
+ }
184
+ }
185
+ return best;
186
+ }
187
+ var HEX_RE = /^#[0-9a-fA-F]{6}$/;
188
+ var isHex = (c) => HEX_RE.test(c);
189
+ var PALETTE_STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
190
+ function parseRef(c) {
191
+ const m = c.match(/^([a-zA-Z][a-zA-Z-]*?)-(\d{2,3})$/);
192
+ if (!m) return null;
193
+ return { palette: m[1], step: Number(m[2]) };
194
+ }
195
+ function refToHex(c) {
196
+ var _a;
197
+ if (isHex(c)) return c;
198
+ if (c === "white") return "#ffffff";
199
+ if (c === "black") return "#000000";
200
+ const p = parseRef(c);
201
+ if (!p) return null;
202
+ const hex = (_a = primitives.color[p.palette]) == null ? void 0 : _a[p.step];
203
+ return typeof hex === "string" && HEX_RE.test(hex) ? hex : null;
204
+ }
205
+ function onColorRef(c) {
206
+ const hex = refToHex(c);
207
+ if (!hex) return null;
208
+ return onColor(hex) === "#000000" ? "black" : "white";
209
+ }
210
+ function hoverRef(c) {
211
+ const p = parseRef(c);
212
+ if (!p) return null;
213
+ const i = PALETTE_STEPS.indexOf(p.step);
214
+ if (i < 0) return null;
215
+ const j = i < PALETTE_STEPS.length - 1 ? i + 1 : i - 1;
216
+ return `${p.palette}-${PALETTE_STEPS[j]}`;
217
+ }
218
+ function brandTones(hex) {
219
+ const scale = buildScale(hex);
220
+ const pinned = SCALE_STEPS.findIndex((s) => scale[s].toLowerCase() === hex.toLowerCase());
221
+ const i = pinned >= 0 ? pinned : SCALE_STEPS.indexOf(nearestStep(hexToOklch(hex)[0]));
222
+ const hoverStep = i < SCALE_STEPS.length - 1 ? SCALE_STEPS[i + 1] : SCALE_STEPS[i - 1];
223
+ return { hover: scale[hoverStep], on: onColor(hex, scale) };
224
+ }
225
+ var NEUTRAL_SAT_MAX = 20;
226
+ function brandHex(c, palettes) {
227
+ var _a, _b;
228
+ if (isHex(c)) return c;
229
+ if (c === "white") return "#ffffff";
230
+ if (c === "black") return "#000000";
231
+ const p = parseRef(c);
232
+ if (p && palettes) {
233
+ const raw = palettes[p.palette];
234
+ if (typeof raw === "string") return (_a = buildScale(raw)[p.step]) != null ? _a : null;
235
+ if (raw && typeof raw === "object") return (_b = raw[String(p.step)]) != null ? _b : null;
236
+ }
237
+ return refToHex(c);
238
+ }
239
+ function isNeutralBrand(def) {
240
+ if (def.archetype === "neutral") return true;
241
+ if (def.archetype === "chromatic") return false;
242
+ const hex = brandHex(def.brand.primary, def.palettes);
243
+ if (!hex) return false;
244
+ const [, s] = hexToHsl(hex);
245
+ return s <= NEUTRAL_SAT_MAX;
246
+ }
247
+
248
+ // generate/generateTheme.ts
249
+ var step = (p, n) => `var(--color-${p}-${n})`;
250
+ function ref(c, allowLiteral = false) {
251
+ const mix = c.match(/^mix\((.+)\)$/);
252
+ if (mix) {
253
+ const [aPart, bPart] = mix[1].split(",").map((s) => s.trim());
254
+ const a = aPart.match(/^(.+?)\s+(\d+%)$/);
255
+ const aStr = a ? `${ref(a[1], allowLiteral)} ${a[2]}` : ref(aPart, allowLiteral);
256
+ const bStr = bPart === "transparent" ? "transparent" : ref(bPart, allowLiteral);
257
+ return `color-mix(in srgb, ${aStr}, ${bStr})`;
258
+ }
259
+ if (c.startsWith("#")) return c;
260
+ return `var(--color-${c})`;
261
+ }
262
+ var alpha = (pct) => `color-mix(in srgb, var(--color-white) ${pct}%, transparent)`;
263
+ var SURF = { light: "#ffffff", dark: "#0a0a0a" };
264
+ function surfaceSafeHex(hex, mode, min = 3) {
265
+ const bg = SURF[mode];
266
+ if (contrastRatio(hex, bg) >= min) return hex;
267
+ const scale = buildScale(hex);
268
+ const order = mode === "light" ? [600, 700, 800, 900] : [300, 200, 100, 50];
269
+ for (const s of order) if (contrastRatio(scale[s], bg) >= min) return scale[s];
270
+ return mode === "light" ? scale[900] : scale[50];
271
+ }
272
+ var FILL_AFFORDANCE_MIN = 2;
273
+ function primaryFillBorder(fillRef, surfaceRef) {
274
+ const fill = isHex(fillRef) ? fillRef : refToHex(fillRef);
275
+ const surf = refToHex(surfaceRef);
276
+ if (!fill || !surf) return null;
277
+ if (contrastRatio(fill, surf) >= FILL_AFFORDANCE_MIN) return null;
278
+ return "color-mix(in srgb, var(--brand-on-primary) 60%, transparent)";
279
+ }
280
+ var isSerif = (name) => !!name && /serif/.test(name);
281
+ var familyFb = (name, kind) => kind === "serif" || isSerif(name) ? "Georgia, serif" : "system-ui, sans-serif";
282
+ function fonts(def) {
283
+ var _a;
284
+ const f = def.fonts;
285
+ const out = { "--font-family": `var(--font-${f.body}), system-ui, sans-serif` };
286
+ const disp = (_a = f.display) != null ? _a : f.body;
287
+ out["--font-family-display"] = `var(--font-${disp}), ${familyFb(disp, f.displayKind)}`;
288
+ if (f.editorial) out["--font-family-editorial"] = `var(--font-${f.editorial}), ${familyFb(f.editorial)}`;
289
+ if (f.mono) out["--font-family-mono"] = `var(--font-${f.mono}), 'Courier New', monospace`;
290
+ return out;
291
+ }
292
+ function onOf(explicit, color, lit) {
293
+ var _a;
294
+ if (explicit) return ref(explicit, lit);
295
+ return ref((_a = onColorRef(color)) != null ? _a : "white", lit);
296
+ }
297
+ var SECONDARY_DOTS = "radial-gradient(circle at 1px 1px, color-mix(in srgb, var(--brand-on-secondary) 14%, transparent) 1px, transparent 0)";
298
+ var SECONDARY_TEXTURE_SIZE = "18px 18px";
299
+ var TEXTURE_SAT_MIN = 20;
300
+ var TEXTURE_LUM_MIN = 0.03;
301
+ var TEXTURE_LUM_MAX = 0.85;
302
+ function isChromaticSurface(secRef) {
303
+ const hex = secRef ? refToHex(secRef) : null;
304
+ if (!hex) return true;
305
+ const [, s] = hexToHsl(hex);
306
+ const l = relativeLuminance(hex);
307
+ return s > TEXTURE_SAT_MIN && l >= TEXTURE_LUM_MIN && l <= TEXTURE_LUM_MAX;
308
+ }
309
+ function secondaryTexture(secRef) {
310
+ return {
311
+ "--brand-secondary-texture": isChromaticSurface(secRef) ? SECONDARY_DOTS : "none",
312
+ "--brand-secondary-texture-size": SECONDARY_TEXTURE_SIZE
313
+ };
314
+ }
315
+ function brandScale(primary) {
316
+ const o = {};
317
+ if (isHex(primary)) {
318
+ const scale = buildScale(primary);
319
+ for (const s of SCALE_STEPS) o[`--brand-primary-${s}`] = scale[s];
320
+ return o;
321
+ }
322
+ const m = primary.match(/^([a-zA-Z]+)-\d+$/);
323
+ if (m) for (const s of SCALE_STEPS) o[`--brand-primary-${s}`] = `var(--color-${m[1]}-${s})`;
324
+ return o;
325
+ }
326
+ function deriveHarmony(primaryHex) {
327
+ const [h, s, l] = hexToHsl(primaryHex);
328
+ const clampS = (x) => Math.min(72, Math.max(35, x));
329
+ const clampL = (x) => Math.min(60, Math.max(42, x));
330
+ return {
331
+ secondary: hslToHex((h + 28) % 360, clampS(s * 0.9), clampL(l)),
332
+ accent: hslToHex((h + 170) % 360, clampS(s), clampL(l))
333
+ };
334
+ }
335
+ function brandLight(def, lit) {
336
+ var _a;
337
+ const b = def.brand;
338
+ const o = {};
339
+ Object.assign(o, brandScale(b.primary));
340
+ const primary = b.primary;
341
+ o["--brand-primary"] = ref(primary, lit);
342
+ const tones = isHex(primary) ? brandTones(primary) : null;
343
+ if (b.hover) o["--brand-hover"] = ref(b.hover, lit);
344
+ else if (tones) o["--brand-hover"] = tones.hover;
345
+ else {
346
+ const h = hoverRef(b.primary);
347
+ if (h) o["--brand-hover"] = ref(h, lit);
348
+ }
349
+ o["--brand-on-primary"] = b.onPrimary ? ref(b.onPrimary, lit) : tones ? tones.on : ref((_a = onColorRef(primary)) != null ? _a : "white", lit);
350
+ if (b.secondary) {
351
+ o["--brand-secondary"] = ref(b.secondary, lit);
352
+ o["--brand-on-secondary"] = onOf(b.onSecondary, b.secondary, lit);
353
+ o["--brand-secondary-surface"] = "var(--brand-secondary)";
354
+ o["--brand-secondary-band"] = "var(--brand-secondary)";
355
+ Object.assign(o, secondaryTexture(b.secondary));
356
+ }
357
+ if (b.accent) {
358
+ o["--brand-accent"] = ref(b.accent, lit);
359
+ if (b.accentLight) o["--brand-accent-light"] = ref(b.accentLight, lit);
360
+ o["--brand-on-accent"] = onOf(b.onAccent, b.accent, lit);
361
+ }
362
+ return o;
363
+ }
364
+ function light(def) {
365
+ var _a, _b;
366
+ const S = def.surface, T = def.text, b = def.brand, lit = !!def.allowLiteral;
367
+ const ink = (_b = (_a = def.dark) == null ? void 0 : _a.surface) != null ? _b : T;
368
+ const o = __spreadValues(__spreadValues({}, fonts(def)), brandLight(def, lit));
369
+ const tintPct = def.surfaceTint ? Math.min(def.surfaceTint, 10) : void 0;
370
+ const tint = (base) => tintPct ? `color-mix(in srgb, ${ref(b.primary, lit)} ${tintPct}%, ${base})` : base;
371
+ o["--surface-page"] = tint(step(S, 50));
372
+ o["--surface-default"] = tint("var(--color-white)");
373
+ o["--surface-raised"] = tint(step(S, 100));
374
+ o["--surface-overlay"] = step(ink, 900);
375
+ o["--surface-invert"] = step(ink, 900);
376
+ o["--border-subtle"] = S !== T ? step(S, 200) : step(T, 100);
377
+ o["--border-default"] = step(T, 200);
378
+ o["--border-strong"] = step(T, 300);
379
+ o["--border-control"] = step(T, 500);
380
+ o["--border-focus"] = b.borderFocus ? ref(b.borderFocus, lit) : isNeutralBrand(def) ? step(T, 900) : isHex(b.primary) ? surfaceSafeHex(b.primary, "light") : ref(b.primary, lit);
381
+ o["--brand-text"] = isNeutralBrand(def) ? step(T, 900) : isHex(b.primary) ? surfaceSafeHex(b.primary, "light") : ref(b.primary, lit);
382
+ const pb = primaryFillBorder(b.primary, `${S}-50`);
383
+ if (pb) o["--brand-primary-border"] = pb;
384
+ o["--text-primary"] = step(ink, 900);
385
+ o["--text-secondary"] = step(T, 700);
386
+ o["--text-muted"] = step(T, 600);
387
+ o["--text-disabled"] = step(T, 400);
388
+ o["--text-inverse"] = ink !== T ? step(T, 100) : "var(--color-white)";
389
+ o["--text-placeholder"] = step(T, 600);
390
+ o["--icon-default"] = step(T, 800);
391
+ return o;
392
+ }
393
+ function darkFull(def) {
394
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q;
395
+ const Sd = (_b = (_a = def.dark) == null ? void 0 : _a.surface) != null ? _b : def.surface;
396
+ const Td = (_d = (_c = def.dark) == null ? void 0 : _c.text) != null ? _d : def.text;
397
+ const ink = (_f = (_e = def.dark) == null ? void 0 : _e.surface) != null ? _f : def.text;
398
+ const lit = !!def.allowLiteral;
399
+ const o = {};
400
+ o["--surface-page"] = step(Sd, 900);
401
+ o["--surface-default"] = step(Sd, 800);
402
+ o["--surface-raised"] = step(Sd, 700);
403
+ o["--surface-overlay"] = step(Sd, 200);
404
+ o["--surface-invert"] = step(Td, 50);
405
+ o["--border-subtle"] = alpha(8);
406
+ o["--border-default"] = alpha(14);
407
+ o["--border-strong"] = alpha(22);
408
+ o["--border-control"] = alpha(40);
409
+ o["--text-primary"] = step(Td, 50);
410
+ o["--text-secondary"] = step(Td, 200);
411
+ o["--text-muted"] = step(Td, 300);
412
+ o["--text-disabled"] = step(Td, 600);
413
+ o["--text-inverse"] = step(ink, 900);
414
+ o["--text-placeholder"] = step(Td, 300);
415
+ o["--icon-default"] = step(Td, 400);
416
+ const db = (_g = def.dark) == null ? void 0 : _g.brand;
417
+ if (isNeutralBrand(def) && !db) {
418
+ o["--brand-primary"] = "var(--color-white)";
419
+ o["--brand-hover"] = step(Td, 100);
420
+ o["--brand-on-primary"] = step(Td, 900);
421
+ o["--brand-secondary"] = step(Td, 900);
422
+ o["--brand-on-secondary"] = "var(--color-white)";
423
+ o["--border-focus"] = step(Td, 400);
424
+ o["--brand-text"] = step(Td, 50);
425
+ Object.assign(o, secondaryTexture(`${Td}-900`));
426
+ } else if (db) {
427
+ if (db.primary) {
428
+ const dpHex = isHex(db.primary);
429
+ const dp = ref(db.primary, lit);
430
+ o["--brand-primary"] = dp;
431
+ if (dpHex) {
432
+ const t = brandTones(db.primary);
433
+ if (!db.hover) o["--brand-hover"] = t.hover;
434
+ if (!db.onPrimary) o["--brand-on-primary"] = t.on;
435
+ }
436
+ }
437
+ if (db.hover) o["--brand-hover"] = ref(db.hover, lit);
438
+ if (db.onPrimary) o["--brand-on-primary"] = ref(db.onPrimary, lit);
439
+ o["--brand-secondary"] = db.secondary ? ref(db.secondary, lit) : `color-mix(in srgb, ${ref((_h = db.primary) != null ? _h : def.brand.primary, lit)} 15%, transparent)`;
440
+ if (db.onSecondary) o["--brand-on-secondary"] = ref(db.onSecondary, lit);
441
+ Object.assign(o, db.secondary ? secondaryTexture(db.secondary) : { "--brand-secondary-texture": "none", "--brand-secondary-texture-size": SECONDARY_TEXTURE_SIZE });
442
+ if (db.accent) {
443
+ o["--brand-accent"] = ref(db.accent, lit);
444
+ o["--brand-accent-light"] = db.accentLight ? ref(db.accentLight, lit) : `color-mix(in srgb, ${ref(db.accent, lit)} 15%, transparent)`;
445
+ }
446
+ if (db.onAccent) o["--brand-on-accent"] = ref(db.onAccent, lit);
447
+ o["--border-focus"] = ref((_j = (_i = db.borderFocus) != null ? _i : db.primary) != null ? _j : def.brand.primary, lit);
448
+ const pbd = primaryFillBorder((_k = db.primary) != null ? _k : def.brand.primary, `${Sd}-900`);
449
+ if (pbd) o["--brand-primary-border"] = pbd;
450
+ } else if (isHex(def.brand.primary)) {
451
+ const dp = surfaceSafeHex(def.brand.primary, "dark");
452
+ const t = brandTones(dp);
453
+ o["--brand-primary"] = dp;
454
+ o["--brand-hover"] = t.hover;
455
+ o["--brand-on-primary"] = t.on;
456
+ o["--border-focus"] = dp;
457
+ }
458
+ if (!("--brand-text" in o)) {
459
+ o["--brand-text"] = isHex(def.brand.primary) ? surfaceSafeHex(((_m = (_l = def.dark) == null ? void 0 : _l.brand) == null ? void 0 : _m.primary) && isHex(def.dark.brand.primary) ? def.dark.brand.primary : def.brand.primary, "dark") : ((_o = (_n = def.dark) == null ? void 0 : _n.brand) == null ? void 0 : _o.primary) ? ref(def.dark.brand.primary, lit) : step(Td, 50);
460
+ }
461
+ const hasSecondary = !!db || isNeutralBrand(def) || !!def.brand.secondary;
462
+ if (hasSecondary) {
463
+ const translucentSecondary = !!db && !db.secondary;
464
+ const darkSecRef = (_q = db == null ? void 0 : db.secondary) != null ? _q : isNeutralBrand(def) && !db ? `${Td}-900` : (_p = def.brand.secondary) != null ? _p : null;
465
+ const chromatic = !translucentSecondary && isChromaticSurface(darkSecRef);
466
+ o["--brand-secondary-surface"] = chromatic ? "var(--brand-secondary)" : "var(--surface-page)";
467
+ o["--brand-secondary-band"] = chromatic ? "var(--brand-secondary)" : "transparent";
468
+ }
469
+ return o;
470
+ }
471
+ function mixTheme(def) {
472
+ const A = step(def.surface, 900);
473
+ const W = "var(--color-white)";
474
+ const onA = (p) => `color-mix(in srgb, ${W} ${p}%, ${A})`;
475
+ const lift = (p) => `color-mix(in srgb, ${W} ${p}%, transparent)`;
476
+ const o = __spreadValues(__spreadValues({}, fonts(def)), brandLight(def, !!def.allowLiteral));
477
+ o["--surface-page"] = A;
478
+ o["--surface-default"] = onA(5);
479
+ o["--surface-raised"] = onA(9);
480
+ o["--surface-overlay"] = W;
481
+ o["--surface-invert"] = W;
482
+ o["--border-subtle"] = lift(8);
483
+ o["--border-default"] = lift(14);
484
+ o["--border-strong"] = lift(26);
485
+ o["--border-control"] = lift(40);
486
+ o["--border-focus"] = W;
487
+ o["--border-on-invert"] = `color-mix(in srgb, ${A} 16%, transparent)`;
488
+ o["--text-primary"] = W;
489
+ o["--text-secondary"] = onA(68);
490
+ o["--text-muted"] = onA(46);
491
+ o["--text-disabled"] = onA(30);
492
+ o["--text-inverse"] = A;
493
+ o["--text-placeholder"] = onA(36);
494
+ o["--icon-default"] = onA(68);
495
+ o["--brand-text"] = W;
496
+ return o;
497
+ }
498
+ function resolveExtra(v) {
499
+ if (v === "transparent") return "transparent";
500
+ if (/^#/.test(v) || /^mix\(/.test(v) || /^[a-zA-Z]+-\d+$/.test(v) || v === "white" || v === "black")
501
+ return ref(v, true);
502
+ return v;
503
+ }
504
+ function applyExtras(l, dark, def) {
505
+ if (!def.extras) return;
506
+ for (const [tok, val] of Object.entries(def.extras)) {
507
+ if (val.light !== void 0) l[tok] = resolveExtra(val.light);
508
+ if (val.dark !== void 0) dark[tok] = resolveExtra(val.dark);
509
+ }
510
+ }
511
+ var FONT_SCALE = {
512
+ xs: "12px",
513
+ sm: "14px",
514
+ base: "16px",
515
+ lg: "18px",
516
+ xl: "20px",
517
+ "2xl": "24px",
518
+ "3xl": "30px",
519
+ "4xl": "36px"
520
+ };
521
+ function applyPalettes(l, def) {
522
+ if (!def.palettes) return;
523
+ for (const [name, val] of Object.entries(def.palettes)) {
524
+ if (typeof val === "string") {
525
+ if (!isHex(val)) continue;
526
+ const scale = buildScale(val);
527
+ for (const step2 of SCALE_STEPS) l[`--color-${name}-${step2}`] = scale[step2];
528
+ } else {
529
+ for (const [step2, hex] of Object.entries(val)) l[`--color-${name}-${step2}`] = hex;
530
+ }
531
+ }
532
+ }
533
+ function applyType(l, def) {
534
+ if (!def.type) return;
535
+ for (const [name, style] of Object.entries(def.type)) {
536
+ const px = (style == null ? void 0 : style.token) ? FONT_SCALE[style.token] : void 0;
537
+ if (px) l[`--font-size-${name}`] = px;
538
+ }
539
+ }
540
+ var RADIUS_LEVEL = {
541
+ square: "0",
542
+ sharp: "0.25rem",
543
+ // 4px
544
+ soft: "0.5rem",
545
+ // 8px (= base)
546
+ round: "1rem",
547
+ // 16px
548
+ pill: "9999px"
549
+ };
550
+ function applyRadius(l, def) {
551
+ const r = def.radius;
552
+ if (!r) return;
553
+ const roles = typeof r === "string" ? { control: r, card: r, avatar: r } : r;
554
+ const set = (lvl, token) => {
555
+ if (lvl && lvl !== "soft") l[token] = RADIUS_LEVEL[lvl];
556
+ };
557
+ set(roles.control, "--radius-control");
558
+ set(roles.card, "--radius-card");
559
+ set(roles.avatar, "--radius-avatar");
560
+ if (roles.control && roles.control !== "soft") {
561
+ l["--radius-tooltip"] = RADIUS_LEVEL[roles.control];
562
+ l["--radius-toast"] = RADIUS_LEVEL[roles.control === "pill" ? "round" : roles.control];
563
+ }
564
+ }
565
+ function applyDisplay(l, def) {
566
+ const d = def.display;
567
+ if (!d) return;
568
+ if (d.transform) l["--display-transform"] = d.transform;
569
+ if (d.tracking === "wide") l["--display-tracking"] = "0.08em";
570
+ }
571
+ function applyGradient(l, def) {
572
+ const g = def.surfaceGradient;
573
+ if (!g) return;
574
+ const pct = Math.min(g, 40);
575
+ l["--surface-gradient"] = `radial-gradient(ellipse at 50% -10%, color-mix(in srgb, var(--brand-primary) ${pct}%, transparent), transparent 68%)`;
576
+ }
577
+ var SURFACE_TEXTURE = {
578
+ dots: {
579
+ image: "radial-gradient(color-mix(in srgb, var(--text-primary) 8%, transparent) 1px, transparent 1.5px)",
580
+ size: "16px 16px"
581
+ },
582
+ grain: {
583
+ image: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.045'/%3E%3C/svg%3E")`,
584
+ size: "120px 120px"
585
+ },
586
+ rays: {
587
+ image: "repeating-conic-gradient(from 0deg at 50% 0%, color-mix(in srgb, var(--brand-primary) 6%, transparent) 0deg 4deg, transparent 4deg 12deg)",
588
+ size: "auto"
589
+ },
590
+ "soft-blobs": {
591
+ image: "radial-gradient(40% 40% at 15% 10%, color-mix(in srgb, var(--brand-primary) 7%, transparent), transparent 70%), radial-gradient(45% 45% at 90% 18%, color-mix(in srgb, var(--brand-accent, var(--brand-primary)) 6%, transparent), transparent 70%)",
592
+ size: "auto"
593
+ }
594
+ };
595
+ function applySurfacePattern(l, def) {
596
+ const p = def.surfacePattern;
597
+ if (!p || p === "none") return;
598
+ const t = SURFACE_TEXTURE[p];
599
+ l["--surface-texture"] = t.image;
600
+ l["--surface-texture-size"] = t.size;
601
+ }
602
+ var CARD_SHADOW = {
603
+ flat: "none",
604
+ sm: "0 1px 2px color-mix(in srgb, var(--text-primary) 8%, transparent)",
605
+ md: "0 4px 14px color-mix(in srgb, var(--text-primary) 12%, transparent)"
606
+ };
607
+ var CARD_RATIO = {
608
+ square: "1 / 1",
609
+ portrait: "3 / 4",
610
+ landscape: "4 / 3"
611
+ };
612
+ function applyCard(l, def) {
613
+ const c = def.card;
614
+ if (!c) return;
615
+ if (c.elevation) l["--card-shadow"] = CARD_SHADOW[c.elevation];
616
+ if (c.imageRatio) l["--card-image-ratio"] = CARD_RATIO[c.imageRatio];
617
+ if (c.overlay) l["--card-overlay"] = "1";
618
+ }
619
+ function applyHero(l, def) {
620
+ const h = def.hero;
621
+ if (!h || h.overlay === void 0) return;
622
+ const pct = Math.min(Math.max(h.overlay, 0), 90);
623
+ l["--hero-overlay"] = `linear-gradient(to top, color-mix(in srgb, #000 ${pct}%, transparent), transparent 62%)`;
624
+ }
625
+ function withDerivedHarmony(def) {
626
+ var _a, _b, _c, _d, _e;
627
+ let next = def;
628
+ const b = def.brand;
629
+ if (isHex(b.primary) && (!b.secondary || !b.accent)) {
630
+ const h = deriveHarmony(b.primary);
631
+ next = __spreadProps(__spreadValues({}, next), { brand: __spreadProps(__spreadValues({}, b), { secondary: (_a = b.secondary) != null ? _a : h.secondary, accent: (_b = b.accent) != null ? _b : h.accent }) });
632
+ }
633
+ const db = (_c = def.dark) == null ? void 0 : _c.brand;
634
+ const dp = db == null ? void 0 : db.primary;
635
+ if (db && dp && isHex(dp) && (!db.secondary || !db.accent)) {
636
+ const h = deriveHarmony(dp);
637
+ next = __spreadProps(__spreadValues({}, next), { dark: __spreadProps(__spreadValues({}, next.dark), { brand: __spreadProps(__spreadValues({}, db), { secondary: (_d = db.secondary) != null ? _d : h.secondary, accent: (_e = db.accent) != null ? _e : h.accent }) }) });
638
+ }
639
+ return next;
640
+ }
641
+ function generateTheme(def) {
642
+ def = withDerivedHarmony(def);
643
+ if (def.scaleMode === "mix") {
644
+ const l2 = mixTheme(def);
645
+ const dark2 = {};
646
+ applyPalettes(l2, def);
647
+ applyExtras(l2, dark2, def);
648
+ applyType(l2, def);
649
+ applyRadius(l2, def);
650
+ applyDisplay(l2, def);
651
+ applyGradient(l2, def);
652
+ applySurfacePattern(l2, def);
653
+ applyCard(l2, def);
654
+ applyHero(l2, def);
655
+ return { supported: true, light: l2, dark: dark2 };
656
+ }
657
+ const l = light(def);
658
+ const full = darkFull(def);
659
+ const dark = {};
660
+ for (const [k, v] of Object.entries(full)) if (l[k] !== v) dark[k] = v;
661
+ applyPalettes(l, def);
662
+ applyExtras(l, dark, def);
663
+ applyType(l, def);
664
+ applyRadius(l, def);
665
+ applyDisplay(l, def);
666
+ applyGradient(l, def);
667
+ applySurfacePattern(l, def);
668
+ applyCard(l, def);
669
+ applyHero(l, def);
670
+ return { supported: true, light: l, dark };
671
+ }
672
+ function resolveTheme(def, opts) {
673
+ const { light: light2, dark } = generateTheme(def);
674
+ const isDark = (opts == null ? void 0 : opts.mode) === "dark";
675
+ return { tokens: isDark ? __spreadValues(__spreadValues({}, light2), dark) : light2, isDark };
676
+ }
677
+
678
+ // generate/emitCss.ts
679
+ var SECTIONS = [
680
+ { label: "Tipografia", test: (k) => k.startsWith("--font-") },
681
+ { label: "Brand", test: (k) => k.startsWith("--brand-") },
682
+ { label: "Surface", test: (k) => k.startsWith("--surface-") },
683
+ { label: "Border", test: (k) => k.startsWith("--border-") },
684
+ { label: "Text", test: (k) => k.startsWith("--text-") },
685
+ { label: "Icon", test: (k) => k.startsWith("--icon-") },
686
+ { label: "Radius", test: (k) => k.startsWith("--radius-") }
687
+ ];
688
+ function block(map, indent = " ") {
689
+ const keys = Object.keys(map);
690
+ const pad = Math.max(...keys.map((k) => k.length));
691
+ const lines = [];
692
+ const seen = /* @__PURE__ */ new Set();
693
+ for (const s of SECTIONS) {
694
+ const inSec = keys.filter(s.test);
695
+ if (!inSec.length) continue;
696
+ lines.push(`${indent}/* \u2500\u2500 ${s.label} \u2500\u2500 */`);
697
+ for (const k of inSec) {
698
+ lines.push(`${indent}${k.padEnd(pad)}: ${map[k]};`);
699
+ seen.add(k);
700
+ }
701
+ lines.push("");
702
+ }
703
+ const rest = keys.filter((k) => !seen.has(k));
704
+ if (rest.length) {
705
+ lines.push(`${indent}/* \u2500\u2500 Extras (decis\xF5es do tema) \u2500\u2500 */`);
706
+ for (const k of rest) lines.push(`${indent}${k.padEnd(pad)}: ${map[k]};`);
707
+ lines.push("");
708
+ }
709
+ return lines.join("\n").trimEnd();
710
+ }
711
+ function emitCss(def) {
712
+ const { supported, light: light2, dark, note } = generateTheme(def);
713
+ if (!supported) throw new Error(`emitCss: ${def.name} n\xE3o suportado \u2014 ${note}`);
714
+ const Name = def.name[0].toUpperCase() + def.name.slice(1);
715
+ const header = [
716
+ "/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
717
+ ` ROJAO DS \u2014 ${Name} theme \xB7 GERADO (n\xE3o editar \xE0 m\xE3o)`,
718
+ ...def.description ? [` ${def.description}`] : [],
719
+ "",
720
+ ` Fonte: recipes/index.ts (${def.name}) \u2192 generate/emitCss.ts`,
721
+ " Regerar: pnpm build:themes",
722
+ "",
723
+ ` <html class="theme-${def.name}"> \u2192 light`,
724
+ ` <html class="theme-${def.name} dark"> \u2192 dark`,
725
+ "",
726
+ // Propriedade do OUTPUT (#98). O gerador é MIT, mas o que sai dele é da marca de quem
727
+ // gerou — e isso precisa estar escrito NO arquivo, não só na página: quem recebe o
728
+ // theme.css de um colega meses depois não viu a página nenhuma.
729
+ " Este arquivo \xE9 seu. Gerado a partir da sua marca, sem amarra de licen\xE7a:",
730
+ " use, edite e redistribua como quiser. O gerador \xE9 MIT; o resultado \xE9 seu.",
731
+ " \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */"
732
+ ].join("\n");
733
+ const lightBlock = `.theme-${def.name} {
734
+ ${block(light2)}
735
+ }`;
736
+ const darkBlock = Object.keys(dark).length ? `
737
+
738
+ .theme-${def.name}.dark,
739
+ :is(.dark).theme-${def.name} {
740
+ ${block(dark)}
741
+ }` : "";
742
+ return `${header}
743
+
744
+ ${lightBlock}${darkBlock}
745
+ `;
746
+ }
747
+
748
+ // generate/emitClaudeMd.ts
749
+ function resolveColor(v) {
750
+ var _a, _b;
751
+ if (!v) return v;
752
+ if (v.startsWith("color-mix")) {
753
+ const mix = v.match(/var\(--color-([a-zA-Z]+)(?:-(\d+))?\)\s+(\d+)%/);
754
+ return mix ? `${mix[1]}${mix[2] ? "-" + mix[2] : ""} a ${mix[3]}%` : v;
755
+ }
756
+ const scale = v.match(/var\(--color-([a-zA-Z]+)-(\d+)\)/);
757
+ if (scale) {
758
+ const pal = primitives.color[scale[1]];
759
+ return (_a = pal == null ? void 0 : pal[scale[2]]) != null ? _a : v;
760
+ }
761
+ const flat = v.match(/var\(--color-(white|black)\)/);
762
+ if (flat) return (_b = primitives.color[flat[1]]) != null ? _b : v;
763
+ return v;
764
+ }
765
+ var TOKEN_DOCS = [
766
+ { key: "--brand-primary", use: "bot\xE3o prim\xE1rio, fills de marca" },
767
+ { key: "--brand-on-primary", use: "texto/\xEDcone sobre o prim\xE1rio" },
768
+ { key: "--brand-hover", use: "hover do prim\xE1rio" },
769
+ { key: "--brand-secondary", use: "2\xAA cor de marca \u2014 S\xD3 como fill (com --brand-on-secondary); nunca borda/texto sobre superf\xEDcie" },
770
+ { key: "--brand-on-secondary", use: "texto sobre o fill secund\xE1rio" },
771
+ { key: "--brand-accent", use: "destaque, links, sucesso (use com parcim\xF4nia)" },
772
+ { key: "--brand-accent-light", use: "fundo tingido do accent" },
773
+ { key: "--brand-on-accent", use: "texto sobre o accent" },
774
+ { key: "--surface-page", use: "fundo da p\xE1gina" },
775
+ { key: "--surface-default", use: "cards, pain\xE9is" },
776
+ { key: "--surface-raised", use: "superf\xEDcie elevada/hover" },
777
+ { key: "--border-subtle", use: "divis\xF3rias fracas" },
778
+ { key: "--border-default", use: "bordas de inputs e cards" },
779
+ { key: "--border-strong", use: "bordas enfatizadas" },
780
+ { key: "--border-focus", use: "anel de foco" },
781
+ { key: "--text-primary", use: "t\xEDtulos, corpo" },
782
+ { key: "--text-secondary", use: "texto de apoio" },
783
+ { key: "--text-muted", use: "legendas, metadados" },
784
+ { key: "--text-inverse", use: "texto sobre superf\xEDcies invertidas" },
785
+ { key: "--icon-default", use: "cor padr\xE3o de \xEDcone" }
786
+ ];
787
+ function tokenTable(light2, dark) {
788
+ const rows = TOKEN_DOCS.filter((t) => t.key in light2).map((t) => {
789
+ const l = resolveColor(light2[t.key]);
790
+ const d = t.key in dark ? resolveColor(dark[t.key]) : "\u2014";
791
+ return `| \`${t.key}\` | ${t.use} | \`${l}\` | \`${d}\` |`;
792
+ });
793
+ return [
794
+ "| Token | Use para | Light | Dark |",
795
+ "|---|---|---|---|",
796
+ ...rows
797
+ ].join("\n");
798
+ }
799
+ function scaleLine(obj, unit = "px") {
800
+ return Object.entries(obj).map(([k, v]) => `\`${k}\`=${v}${v === 9999 ? "" : unit}`).join(" \xB7 ");
801
+ }
802
+ function fileLabel(target) {
803
+ return target === "cursor" ? ".cursorrules" : target === "agents" ? "AGENTS.md" : "CLAUDE.md";
804
+ }
805
+ function emitClaudeMd(def, opts = {}) {
806
+ var _a, _b, _c;
807
+ const { supported, light: light2, dark, note } = generateTheme(def);
808
+ if (!supported) throw new Error(`emitClaudeMd: ${def.name} n\xE3o suportado \u2014 ${note}`);
809
+ const Name = def.name[0].toUpperCase() + def.name.slice(1);
810
+ const cssUrl = (_a = opts.cssUrl) != null ? _a : null;
811
+ const bodyFont = typeof ((_b = def.fonts) == null ? void 0 : _b.body) === "string" ? def.fonts.body : "inter";
812
+ const displayFont = typeof ((_c = def.fonts) == null ? void 0 : _c.display) === "string" ? def.fonts.display : bodyFont;
813
+ const fontTitle = (s) => s.split("-").map((w) => {
814
+ var _a2;
815
+ return ((_a2 = w[0]) == null ? void 0 : _a2.toUpperCase()) + w.slice(1);
816
+ }).join(" ");
817
+ const neutral = primitives.color.neutral;
818
+ return `# Design System \u2014 ${Name} (brand: \`${def.name}\`)
819
+
820
+ > This project uses the **${Name}** design system. When you build any UI \u2014
821
+ > components, pages, screens \u2014 follow the rules below so every screen looks like the
822
+ > same product. Do not invent colors, fonts, spacing, or radii.
823
+
824
+ ## Brand identity
825
+ - **Name:** ${Name}
826
+ - **Look:** ${isNeutralBrand(def) ? "neutral \u2014 black & white lead, the accent does the talking" : "chromatic \u2014 the brand color leads"}${def.description ? ` \u2014 ${def.description}` : ""}
827
+ - **Body font:** ${fontTitle(bodyFont)} \xB7 **Display font:** ${fontTitle(displayFont)} (load from Google Fonts; never substitute)
828
+ - **Default radius:** \`${primitives.radius.md}px\` (\`rounded-lg\`). Pills/avatars fully round.
829
+ - **Light AND dark are first-class** \u2014 every screen must work in both.
830
+
831
+ ## How to apply
832
+ 1. ${cssUrl ? `Import the brand stylesheet once at the app root:
833
+ \`\`\`html
834
+ <link rel="stylesheet" href="${cssUrl}" />
835
+ \`\`\`` : `Put \`${def.name}.css\` (downloaded with this file) in your project and import it
836
+ once at the app root:
837
+ \`\`\`css
838
+ @import "./${def.name}.css";
839
+ \`\`\`
840
+ The theme is a file **you own** \u2014 commit it. Nothing here depends on an external host.`}
841
+ 2. Put the theme class on \`<html>\`:
842
+ \`\`\`html
843
+ <html class="theme-${def.name}"> <!-- light -->
844
+ <html class="theme-${def.name} dark"> <!-- dark -->
845
+ \`\`\`
846
+ > One sheet can hold **several themes** \u2014 each as its own \`theme-<name>\` class.
847
+ > Switch with the class (one product per app, or many on one page).${cssUrl ? `
848
+ > Edit the theme at the source and this same URL updates **all your apps at once**.` : ""}
849
+ 3. Style everything with the semantic tokens below, as CSS variables \u2014 they already
850
+ flip between light and dark. You never write a hex value in a component.
851
+ \`\`\`css
852
+ .card { background: var(--surface-default); color: var(--text-primary);
853
+ border: 1px solid var(--border-default); border-radius: ${primitives.radius.md}px; }
854
+ \`\`\`
855
+
856
+ ## Semantic color tokens \u2014 ALWAYS use these, NEVER hardcode a color
857
+ ${tokenTable(light2, dark)}
858
+
859
+ ## Spacing scale (px) \u2014 use ONLY these steps for padding, margin, gap
860
+ ${scaleLine(primitives.space)}
861
+
862
+ ## Type scale (font-size, px)
863
+ ${scaleLine(primitives.fontSize)}
864
+
865
+ **Font weights:** ${scaleLine(primitives.fontWeight, "")}
866
+ **Line heights:** ${Object.entries(primitives.lineHeight).map(([k, v]) => `\`${k}\`=${v}`).join(" \xB7 ")}
867
+
868
+ ## Radius (px)
869
+ ${scaleLine(primitives.radius)}
870
+
871
+ ## Rules (do / don't)
872
+ - \u2705 Primary button = \`--brand-primary\` bg + \`--brand-on-primary\` text; hover \u2192 \`--brand-hover\`.
873
+ - \u2705 Low-emphasis / secondary action = NEUTRAL outline: \`--border-default\` border + \`--text-secondary\` text (legible by rule, light AND dark). A brand color belongs to the PRIMARY (filled) action, not a secondary outline.
874
+ - \u2705 Page wrapper uses \`--surface-page\`; cards use \`--surface-default\`.
875
+ - \u2705 Body text is \`--text-primary\`; never pure black/white text directly.
876
+ - \u2705 \`--brand-accent\` is the ONLY saturated color \u2014 use it sparingly (links, success, highlights).
877
+ - \u2705 Spacing, font-size and radius come ONLY from the scales above.
878
+ - \u274C Never write a raw hex (\`#000\`, \`bg-black\`) or a Tailwind palette color (\`bg-emerald-500\`, \`text-zinc-700\`) in a component \u2014 map it to a token above.
879
+ - \u274C Never use a brand color (incl. \`--brand-secondary\`) as a border/ring or as text directly on a surface. The engine guarantees contrast ONLY for a fill + its \`--brand-on-*\` text. As a stroke (WCAG 1.4.11, 3:1) or label (1.4.3, 4.5:1) on the surface it can fail \u2014 and in one mode but not the other. Brand colors = fills.
880
+ - \u274C Never introduce a second accent color. Don't hardcode dark-mode colors \u2014 tokens already flip.
881
+
882
+ ## Use with shadcn/ui
883
+ You are NOT replacing shadcn. Use shadcn/ui components and apply THIS brand as the theme:
884
+ map the tokens above onto shadcn's CSS variables (\`--background\` \u2192 \`--surface-page\`,
885
+ \`--foreground\` \u2192 \`--text-primary\`, \`--primary\` \u2192 \`--brand-primary\`, \`--border\` \u2192
886
+ \`--border-default\`, \`--ring\` \u2192 \`--border-focus\`, \`--radius\` \u2192 \`${primitives.radius.md}px\`).
887
+ Build with shadcn's component shapes; paint them with these tokens. Don't restyle components from scratch.
888
+
889
+ ## Primitive palette (reference \u2014 prefer the semantic tokens above)
890
+ \`\`\`
891
+ neutral 50 ${neutral[50]} \xB7 500 ${neutral[500]} \xB7 900 ${neutral[900]}
892
+ \`\`\`
893
+
894
+ <!-- Generated by Roj\xE3o DS \u2014 file: ${fileLabel("claude")} / ${fileLabel("cursor")} / ${fileLabel("agents")}. Drop it in your project root; Claude Code & Cursor read it automatically. -->
895
+ `;
896
+ }
897
+
898
+ export { SCALE_STEPS, brandTones, buildScale, contrastRatio, emitClaudeMd, emitCss, generateTheme, hexToHsl, hslToHex, isHex, isNeutralBrand, onColor, refToHex, relativeLuminance, resolveTheme };