@nebutra/tokens 0.1.2 → 2.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/brands/README.md +66 -0
  3. package/brands/cosmos/DESIGN.md +356 -0
  4. package/brands/cosmos/brand.json +209 -0
  5. package/brands/cosmos/refero-tokens.json +312 -0
  6. package/brands/gsap/brand.json +18 -91
  7. package/brands/linear/DESIGN.md +478 -0
  8. package/brands/linear/brand.json +20 -76
  9. package/brands/notion/brand.json +20 -90
  10. package/brands/raycast/brand.json +19 -81
  11. package/brands/stripe/brand.json +17 -88
  12. package/brands/vanta/brand.json +17 -101
  13. package/brands/vercel/brand.json +17 -68
  14. package/dist/brand-package/index.d.ts +123 -0
  15. package/dist/brand-package/index.js +2316 -0
  16. package/dist/brand-package/index.js.map +1 -0
  17. package/dist/brand-package/use-brand.d.ts +2 -0
  18. package/dist/brand-package/use-brand.js +963 -0
  19. package/dist/brand-package/use-brand.js.map +1 -0
  20. package/dist/index.d.ts +81 -0
  21. package/dist/index.js +2581 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/use-brand-ClmvK_lC.d.ts +387 -0
  24. package/package.json +41 -10
  25. package/recipe.css +330 -1
  26. package/skins/README.md +2 -4
  27. package/skins/cosmos.css +239 -0
  28. package/skins/gsap.css +62 -138
  29. package/skins/linear.css +85 -132
  30. package/skins/notion.css +62 -141
  31. package/skins/raycast.css +68 -137
  32. package/skins/stripe.css +60 -143
  33. package/skins/vanta.css +60 -151
  34. package/skins/vercel.css +83 -128
  35. package/styles.css +477 -115
  36. package/.turbo/turbo-build.log +0 -8
  37. package/.turbo/turbo-typecheck.log +0 -4
  38. package/AGENTS.md +0 -45
  39. package/DESIGN.md +0 -357
  40. package/scripts/compile-brand-run.ts +0 -79
  41. package/scripts/compile-brand.mjs +0 -25
  42. package/scripts/emit-skins-run.ts +0 -62
  43. package/scripts/emit-skins.mjs +0 -31
  44. package/scripts/sync-styles.mjs +0 -29
  45. package/src/brand-package/__tests__/compile-refero.test.ts +0 -427
  46. package/src/brand-package/__tests__/emit-css.test.ts +0 -130
  47. package/src/brand-package/apply-brand.ts +0 -92
  48. package/src/brand-package/compile-helpers.ts +0 -170
  49. package/src/brand-package/compile-refero.ts +0 -69
  50. package/src/brand-package/emit-css.ts +0 -468
  51. package/src/brand-package/hex-to-hsl.ts +0 -100
  52. package/src/brand-package/index.ts +0 -71
  53. package/src/brand-package/infer-recipe.ts +0 -198
  54. package/src/brand-package/normalize.ts +0 -295
  55. package/src/brand-package/presets/context.ts +0 -17
  56. package/src/brand-package/presets/generic.ts +0 -214
  57. package/src/brand-package/presets/gsap.ts +0 -146
  58. package/src/brand-package/presets/index.ts +0 -31
  59. package/src/brand-package/presets/linear.ts +0 -183
  60. package/src/brand-package/presets/notion.ts +0 -208
  61. package/src/brand-package/presets/raycast.ts +0 -138
  62. package/src/brand-package/presets/recipe.ts +0 -61
  63. package/src/brand-package/presets/stripe.ts +0 -207
  64. package/src/brand-package/presets/vanta.ts +0 -220
  65. package/src/brand-package/presets/vercel.ts +0 -187
  66. package/src/brand-package/types.ts +0 -285
  67. package/src/brand-package/use-brand.ts +0 -207
  68. package/src/brand-package/validate.ts +0 -108
  69. package/src/index.ts +0 -67
  70. package/src/theme-provider.tsx +0 -246
  71. package/tsconfig.json +0 -11
  72. package/turbo.json +0 -15
@@ -1,468 +0,0 @@
1
- /**
2
- * Emit carrier CSS from a normalized Brand Package.
3
- * Components bind: --primary (= action), --brand-mark, --elevation-*, --radius-*.
4
- */
5
-
6
- import { isDualModeBrand, normalizeBrandPackage } from "./normalize";
7
- import type {
8
- BrandColorRoles,
9
- BrandFontFace,
10
- BrandPackage,
11
- BrandRecipe,
12
- BrandSemanticColors,
13
- BrandTypeStep,
14
- BrandZones,
15
- BrandZoneTypography,
16
- } from "./types";
17
-
18
- /** Biome CSS formatter prefers double-quoted font families over single quotes. */
19
- function cssFontStack(stack: string): string {
20
- return stack.replace(/'/g, '"');
21
- }
22
-
23
- function recipeVars(recipe: BrandRecipe): string[] {
24
- const radii = recipe.radii;
25
- const elev = recipe.elevationTokens;
26
-
27
- const lines: string[] = [
28
- ` /* Shape slots */`,
29
- ` --btn-default-radius: ${radii.button};`,
30
- ` --radius-button: ${radii.button};`,
31
- ` --radius-buttons: ${radii.button};`,
32
- ` --radius-md: ${radii.button};`,
33
- ` --radius-card: ${radii.card};`,
34
- ` --radius-lg: ${radii.card};`,
35
- ` --radius-badge: ${radii.badge ?? "9999px"};`,
36
- ` --badge-default-radius: ${radii.badge ?? "9999px"};`,
37
- ` --radius-inputs: ${radii.input ?? radii.button};`,
38
- ` --input-radius: ${radii.input ?? radii.button};`,
39
- ` --radius-pill: ${radii.pill ?? "9999px"};`,
40
- ``,
41
- ` /* Free elevation (carrier-provided CSS shadows) */`,
42
- ` --elevation-card: ${elev.card};`,
43
- ` --elevation-control: ${elev.control ?? "0 0 #0000"};`,
44
- ` --elevation-raised: ${elev.raised ?? elev.card};`,
45
- // Kill Tailwind drop defaults when brand uses custom elev stacks
46
- ` --shadow-xs: ${elev.control ?? "0 0 #0000"};`,
47
- ` --shadow-sm: ${elev.card};`,
48
- ` --shadow-md: ${elev.raised ?? elev.card};`,
49
- ` --shadow-lg: ${elev.raised ?? elev.card};`,
50
- ` --btn-default-shadow: 0 0 #0000;`,
51
- ];
52
-
53
- switch (recipe.buttonDefault) {
54
- case "outline": {
55
- const edge = recipe.outlineBorder ?? "hsl(var(--foreground))";
56
- lines.push(" --btn-default-bg: transparent;");
57
- lines.push(" --btn-default-fg: hsl(var(--foreground));");
58
- lines.push(" --btn-default-border-width: 1px;");
59
- lines.push(" --btn-default-border: transparent;");
60
- lines.push(` --btn-default-stroke-gradient: linear-gradient(${edge}, ${edge});`);
61
- lines.push(" --btn-default-hover-bg: hsl(var(--foreground) / 0.06);");
62
- break;
63
- }
64
- case "gradient-stroke": {
65
- const grad =
66
- recipe.primaryStrokeGradient ??
67
- "linear-gradient(135deg, hsl(var(--primary)), color-mix(in srgb, hsl(var(--primary)) 55%, white))";
68
- lines.push(" --btn-default-bg: transparent;");
69
- lines.push(" --btn-default-fg: hsl(var(--foreground));");
70
- lines.push(" --btn-default-border-width: 1.5px;");
71
- lines.push(" --btn-default-border: transparent;");
72
- lines.push(` --btn-default-stroke-gradient: ${grad};`);
73
- lines.push(" --btn-default-hover-bg: hsl(var(--primary) / 0.08);");
74
- break;
75
- }
76
- default:
77
- lines.push(" --btn-default-bg: hsl(var(--primary));");
78
- lines.push(" --btn-default-fg: hsl(var(--primary-foreground));");
79
- lines.push(" --btn-default-border-width: 0px;");
80
- lines.push(" --btn-default-border: transparent;");
81
- lines.push(" --btn-default-stroke-gradient: transparent;");
82
- lines.push(
83
- " --btn-default-hover-bg: color-mix(in srgb, hsl(var(--primary)) 90%, transparent);",
84
- );
85
- break;
86
- }
87
-
88
- const badgeMode = recipe.badgeDefault ?? "match-action";
89
- if (badgeMode === "outline") {
90
- const edge = recipe.outlineBorder ?? "hsl(var(--border))";
91
- lines.push(" --badge-default-bg: transparent;");
92
- lines.push(" --badge-default-fg: hsl(var(--foreground));");
93
- lines.push(` --badge-default-border: ${edge};`);
94
- lines.push(" --badge-default-hover-bg: hsl(var(--foreground) / 0.06);");
95
- } else if (badgeMode === "muted") {
96
- lines.push(" --badge-default-bg: hsl(var(--secondary));");
97
- lines.push(" --badge-default-fg: hsl(var(--secondary-foreground));");
98
- lines.push(" --badge-default-border: transparent;");
99
- lines.push(" --badge-default-hover-bg: color-mix(in srgb, hsl(var(--secondary)) 90%, white);");
100
- } else if (badgeMode === "brand") {
101
- lines.push(" --badge-default-bg: hsl(var(--brand-mark, var(--accent)));");
102
- lines.push(
103
- " --badge-default-fg: hsl(var(--brand-mark-foreground, var(--accent-foreground)));",
104
- );
105
- lines.push(" --badge-default-border: transparent;");
106
- lines.push(
107
- " --badge-default-hover-bg: color-mix(in srgb, hsl(var(--brand-mark, var(--accent))) 85%, transparent);",
108
- );
109
- } else if (recipe.buttonDefault === "outline" || recipe.buttonDefault === "gradient-stroke") {
110
- const edge = recipe.outlineBorder ?? "hsl(var(--foreground))";
111
- lines.push(" --badge-default-bg: transparent;");
112
- lines.push(" --badge-default-fg: hsl(var(--foreground));");
113
- lines.push(` --badge-default-border: ${edge};`);
114
- lines.push(" --badge-default-hover-bg: hsl(var(--foreground) / 0.06);");
115
- } else {
116
- // match-action
117
- lines.push(" --badge-default-bg: hsl(var(--primary));");
118
- lines.push(" --badge-default-fg: hsl(var(--primary-foreground));");
119
- lines.push(" --badge-default-border: transparent;");
120
- lines.push(
121
- " --badge-default-hover-bg: color-mix(in srgb, hsl(var(--primary)) 80%, transparent);",
122
- );
123
- }
124
-
125
- if (recipe.density === "compact") {
126
- lines.push(" --btn-default-padding-y: 0.5rem;");
127
- lines.push(" --btn-default-padding-x: 0.875rem;");
128
- lines.push(" --control-height-tiny: 1.25rem;");
129
- lines.push(" --control-height-sm: 1.75rem;");
130
- lines.push(" --control-height-md: 2rem;");
131
- lines.push(" --control-height-lg: 2.5rem;");
132
- lines.push(" --control-font-size-md: 0.8125rem;");
133
- } else if (recipe.density === "spacious") {
134
- lines.push(" --btn-default-padding-y: 0.875rem;");
135
- lines.push(" --btn-default-padding-x: 1.5rem;");
136
- lines.push(" --control-height-tiny: 1.75rem;");
137
- lines.push(" --control-height-sm: 2.25rem;");
138
- lines.push(" --control-height-md: 2.75rem;");
139
- lines.push(" --control-height-lg: 3.25rem;");
140
- }
141
-
142
- return lines;
143
- }
144
-
145
- function emitFontFaces(faces: BrandFontFace[] | undefined): string[] {
146
- if (!faces?.length) return [];
147
- const out: string[] = ["/* Brand font faces */"];
148
- for (const face of faces) {
149
- const src = face.src
150
- .map((s) => {
151
- const fmt = s.format ? ` format("${s.format}")` : "";
152
- return `url("${s.url}")${fmt}`;
153
- })
154
- .join(", ");
155
- out.push("@font-face {");
156
- out.push(` font-family: "${face.family}";`);
157
- out.push(` src: ${src};`);
158
- if (face.weight != null) out.push(` font-weight: ${face.weight};`);
159
- if (face.style) out.push(` font-style: ${face.style};`);
160
- out.push(` font-display: ${face.display ?? "swap"};`);
161
- if (face.unicodeRange) out.push(` unicode-range: ${face.unicodeRange};`);
162
- out.push("}");
163
- out.push("");
164
- }
165
- return out;
166
- }
167
-
168
- function stepVars(prefix: string, step: BrandTypeStep | undefined, role: string): string[] {
169
- if (!step) return [];
170
- const lines = [` --${prefix}-${role}-size: ${step.fontSize};`];
171
- if (step.lineHeight != null) lines.push(` --${prefix}-${role}-leading: ${step.lineHeight};`);
172
- if (step.letterSpacing != null) {
173
- lines.push(` --${prefix}-${role}-tracking: ${step.letterSpacing};`);
174
- }
175
- if (step.fontWeight != null) lines.push(` --${prefix}-${role}-weight: ${step.fontWeight};`);
176
- return lines;
177
- }
178
-
179
- function zoneScaleVars(
180
- zone: "product" | "marketing",
181
- scale: BrandZoneTypography | undefined,
182
- ): string[] {
183
- if (!scale) return [];
184
- const p = `zone-${zone}`;
185
- return [
186
- ...stepVars(p, scale.caption, "caption"),
187
- ...stepVars(p, scale.bodySm, "body-sm"),
188
- ...stepVars(p, scale.body, "body"),
189
- ...stepVars(p, scale.bodyLg, "body-lg"),
190
- ...stepVars(p, scale.subheading, "subheading"),
191
- ...stepVars(p, scale.headingSm, "heading-sm"),
192
- ...stepVars(p, scale.heading, "heading"),
193
- ...stepVars(p, scale.headingLg, "heading-lg"),
194
- ...stepVars(p, scale.display, "display"),
195
- ];
196
- }
197
-
198
- function zoneConsumerBlock(
199
- zone: "product" | "marketing",
200
- scale: BrandZoneTypography | undefined,
201
- ): string[] {
202
- if (!scale) return [];
203
- const p = `zone-${zone}`;
204
- const body = scale.body;
205
- const lines = [
206
- ``,
207
- `/* Zone: ${zone} — product never inherits marketing display */`,
208
- // Multi-line selector list matches Biome CSS formatter output (CI governance).
209
- `[data-zone="${zone}"],`,
210
- `.zone-${zone} {`,
211
- ];
212
- if (body?.fontSize) lines.push(` font-size: var(--${p}-body-size, ${body.fontSize});`);
213
- if (body?.lineHeight != null) {
214
- lines.push(` line-height: var(--${p}-body-leading, ${body.lineHeight});`);
215
- }
216
- if (body?.fontWeight != null) {
217
- lines.push(` font-weight: var(--${p}-body-weight, ${body.fontWeight});`);
218
- }
219
- if (body?.letterSpacing != null) {
220
- lines.push(` letter-spacing: var(--${p}-body-tracking, ${body.letterSpacing});`);
221
- }
222
- const roles = [
223
- "caption",
224
- "body-sm",
225
- "body",
226
- "body-lg",
227
- "subheading",
228
- "heading-sm",
229
- "heading",
230
- "heading-lg",
231
- "display",
232
- ] as const;
233
- for (const role of roles) {
234
- // Product zone hard-cap: display falls back to heading if product display is unset.
235
- // Emit once — a second --text-display trips biome noDuplicateCustomProperties.
236
- if (zone === "product" && role === "display") {
237
- lines.push(` --text-display: var(--${p}-display-size, var(--${p}-heading-size, inherit));`);
238
- } else {
239
- lines.push(` --text-${role}: var(--${p}-${role}-size, inherit);`);
240
- }
241
- lines.push(` --leading-${role}: var(--${p}-${role}-leading, inherit);`);
242
- lines.push(` --tracking-${role}: var(--${p}-${role}-tracking, inherit);`);
243
- }
244
- lines.push(`}`);
245
- return lines;
246
- }
247
-
248
- function emitZones(zones: BrandZones | undefined): string[] {
249
- if (!zones) return [];
250
- return [
251
- ``,
252
- ` /* Zone type scales */`,
253
- ...zoneScaleVars("product", zones.product),
254
- ...zoneScaleVars("marketing", zones.marketing),
255
- ];
256
- }
257
-
258
- export type EmitBrandCssMode =
259
- /** Single-skin import / Create Center inject — also binds :root (global swap) */
260
- | "global"
261
- /** Multi-language catalog — only activates under html[data-brand] */
262
- | "scoped";
263
-
264
- export interface EmitBrandCssOptions {
265
- /**
266
- * `global` (default): `:root` + `html[data-brand]` — one import recolors the app.
267
- * Single-mode dark packs also include `.dark`.
268
- * Dual-mode packs (`modes.light` + `modes.dark`) emit separate light/dark color blocks.
269
- * `scoped`: only `html[data-brand]` (+ `html.dark[data-brand]` when dual).
270
- */
271
- mode?: EmitBrandCssMode;
272
- }
273
-
274
- /** Global single-skin selector list — single-mode packs only. */
275
- export function emitGlobalSkinSelector(brandId: string, darkDefault: boolean): string {
276
- if (darkDefault) {
277
- return `:root,\n.dark,\nhtml[data-brand="${brandId}"] {`;
278
- }
279
- return `:root,\nhtml[data-brand="${brandId}"] {`;
280
- }
281
-
282
- /** Light mode selector (dual-mode). */
283
- export function emitLightModeSelector(brandId: string, mode: EmitBrandCssMode): string {
284
- if (mode === "scoped") return `html[data-brand="${brandId}"] {`;
285
- return `:root,\nhtml[data-brand="${brandId}"] {`;
286
- }
287
-
288
- /** Dark mode selector (dual-mode) — never paints light colors under .dark. */
289
- export function emitDarkModeSelector(brandId: string, mode: EmitBrandCssMode): string {
290
- if (mode === "scoped") return `html.dark[data-brand="${brandId}"] {`;
291
- return `.dark,\nhtml.dark[data-brand="${brandId}"] {`;
292
- }
293
-
294
- function emitColorVars(s: BrandSemanticColors, r: BrandColorRoles | undefined): string[] {
295
- const roleLines: string[] = [
296
- ` /* ── Color roles (carrier) ── */`,
297
- ` --role-canvas: ${r?.canvas ?? s.background};`,
298
- ` --role-canvas-fg: ${r?.canvasForeground ?? s.foreground};`,
299
- ` --role-surface: ${r?.surface ?? s.card};`,
300
- ` --role-surface-fg: ${r?.surfaceForeground ?? s.cardForeground};`,
301
- ` --role-action: ${r?.action ?? s.primary};`,
302
- ` --role-action-fg: ${r?.actionForeground ?? s.primaryForeground};`,
303
- ` --role-quiet: ${r?.quiet ?? s.secondary};`,
304
- ` --role-quiet-fg: ${r?.quietForeground ?? s.secondaryForeground};`,
305
- ` --role-muted: ${r?.muted ?? s.muted};`,
306
- ` --role-muted-fg: ${r?.mutedForeground ?? s.mutedForeground};`,
307
- ` --role-border: ${r?.border ?? s.border};`,
308
- ` --role-input: ${r?.input ?? s.input};`,
309
- ` --role-ring: ${r?.ring ?? s.ring};`,
310
- ];
311
- if (r?.brand) {
312
- roleLines.push(` --role-brand: ${r.brand};`);
313
- roleLines.push(
314
- ` --role-brand-fg: ${r.brandForeground ?? r.actionForeground ?? s.primaryForeground};`,
315
- );
316
- roleLines.push(` --brand-mark: ${r.brand};`);
317
- roleLines.push(
318
- ` --brand-mark-foreground: ${r.brandForeground ?? r.actionForeground ?? s.primaryForeground};`,
319
- );
320
- }
321
-
322
- const semantic = [
323
- ``,
324
- ` /* ── shadcn bridge (primary = action CTA) ── */`,
325
- ` --background: ${s.background};`,
326
- ` --foreground: ${s.foreground};`,
327
- ` --card: ${s.card};`,
328
- ` --card-foreground: ${s.cardForeground};`,
329
- ` --popover: ${s.popover};`,
330
- ` --popover-foreground: ${s.popoverForeground};`,
331
- ` --primary: ${s.primary};`,
332
- ` --primary-foreground: ${s.primaryForeground};`,
333
- ` --secondary: ${s.secondary};`,
334
- ` --secondary-foreground: ${s.secondaryForeground};`,
335
- ` --muted: ${s.muted};`,
336
- ` --muted-foreground: ${s.mutedForeground};`,
337
- ` --accent: ${s.accent};`,
338
- ` --accent-foreground: ${s.accentForeground};`,
339
- ` --destructive: ${s.destructive};`,
340
- ` --destructive-foreground: ${s.destructiveForeground};`,
341
- ` --border: ${s.border};`,
342
- ` --input: ${s.input};`,
343
- ` --ring: ${s.ring};`,
344
- ];
345
-
346
- if (s.success) semantic.push(` --success: ${s.success};`);
347
- if (s.successForeground) semantic.push(` --success-foreground: ${s.successForeground};`);
348
- if (s.warning) semantic.push(` --warning: ${s.warning};`);
349
- if (s.warningForeground) semantic.push(` --warning-foreground: ${s.warningForeground};`);
350
- if (s.info) semantic.push(` --info: ${s.info};`);
351
- if (s.infoForeground) semantic.push(` --info-foreground: ${s.infoForeground};`);
352
-
353
- semantic.push(
354
- ` --sidebar: ${s.card};`,
355
- ` --sidebar-foreground: ${s.foreground};`,
356
- ` --sidebar-primary: ${s.primary};`,
357
- ` --sidebar-primary-foreground: ${s.primaryForeground};`,
358
- ` --sidebar-accent: ${s.accent};`,
359
- ` --sidebar-accent-foreground: ${s.accentForeground};`,
360
- ` --sidebar-border: ${s.border};`,
361
- ` --sidebar-ring: ${s.ring};`,
362
- ` --brand-gradient: hsl(var(--primary));`,
363
- ` --brand-gradient-reverse: hsl(var(--primary));`,
364
- ` --brand-gradient-vertical: hsl(var(--primary));`,
365
- ` --brand-gradient-radial: hsl(var(--primary));`,
366
- );
367
-
368
- return [...roleLines, ...semantic];
369
- }
370
-
371
- /**
372
- * Emit a single opt-in skin CSS file from a Brand Package.
373
- */
374
- export function emitBrandCss(brand: BrandPackage, options: EmitBrandCssOptions = {}): string {
375
- const mode = options.mode ?? "global";
376
- const b = normalizeBrandPackage(brand);
377
- const t = b.typography;
378
- const dual = isDualModeBrand(b);
379
-
380
- const parts: string[] = [
381
- `/**`,
382
- ` * Brand carrier skin: ${b.name} (${b.id}) v${b.version}`,
383
- ` * darkDefault=${b.darkDefault} dualMode=${dual} button=${b.recipe.buttonDefault}`,
384
- ` * fonts=${t.faces?.length ?? 0} zones=${b.zones ? "yes" : "no"} mode=${mode}`,
385
- ` * Contract: roles.action → --primary; roles.brand → --brand-mark (never default CTA)`,
386
- ` */`,
387
- ``,
388
- ...emitFontFaces(t.faces),
389
- ];
390
-
391
- // Biome CSS formatter prefers double-quoted font families.
392
- const fontSans = cssFontStack(t.fontSans);
393
- const fontDisplay = cssFontStack(t.fontDisplay ?? t.fontSans);
394
- const typeLines = [
395
- ` --font-sans: ${fontSans};`,
396
- ` --font-heading: ${fontDisplay};`,
397
- ` --font-display: ${fontDisplay};`,
398
- ];
399
- if (t.fontMono) typeLines.push(` --font-mono: ${cssFontStack(t.fontMono)};`);
400
- if (t.headingWeight != null) {
401
- typeLines.push(` --font-weight-heading: ${t.headingWeight};`);
402
- }
403
-
404
- const cats = b.extensions?.categories;
405
- const decorative = b.extensions?.decorative;
406
- const extLines: string[] = [];
407
- if (cats) {
408
- for (const [key, value] of Object.entries(cats)) {
409
- const safe = key.replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
410
- extLines.push(` --brand-category-${safe}: ${value};`);
411
- }
412
- }
413
- if (decorative) {
414
- for (const [key, value] of Object.entries(decorative)) {
415
- const safe = key.replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
416
- extLines.push(` --brand-decorative-${safe}: ${value};`);
417
- }
418
- }
419
-
420
- const sharedChrome = [
421
- ``,
422
- ` /* Recipe (action language + free elev/radii) */`,
423
- ...recipeVars(b.recipe),
424
- ``,
425
- ` /* Typography */`,
426
- ...typeLines,
427
- ...emitZones(b.zones),
428
- ...(extLines.length
429
- ? ["", " /* Taxonomy / decorative (not product CTA) */", ...extLines]
430
- : []),
431
- ];
432
-
433
- if (dual && b.modes?.light?.semantic && b.modes?.dark?.semantic) {
434
- // Light block: colors + shared chrome
435
- parts.push(emitLightModeSelector(b.id, mode));
436
- parts.push(
437
- ...emitColorVars(b.modes.light.semantic, b.modes.light.roles),
438
- ...sharedChrome,
439
- `}`,
440
- ``,
441
- );
442
- // Dark block: colors only (chrome inherits)
443
- parts.push(emitDarkModeSelector(b.id, mode));
444
- parts.push(...emitColorVars(b.modes.dark.semantic, b.modes.dark.roles), `}`, ``);
445
- } else {
446
- // Single-mode: one selector + full block
447
- const selector =
448
- mode === "scoped"
449
- ? `html[data-brand="${b.id}"] {`
450
- : emitGlobalSkinSelector(b.id, b.darkDefault);
451
- parts.push(selector);
452
- parts.push(...emitColorVars(b.semantic, b.roles), ...sharedChrome, `}`, ``);
453
- }
454
-
455
- parts.push(
456
- ...zoneConsumerBlock("product", b.zones?.product),
457
- ...zoneConsumerBlock("marketing", b.zones?.marketing),
458
- ``,
459
- `/* Product chrome must not use decorative gradients as action fills */`,
460
- `[data-zone="product"] .btn-brand-default,`,
461
- `.zone-product .btn-brand-default {`,
462
- ` /* intentional no-op: documents contract; action uses --primary only */`,
463
- `}`,
464
- ``,
465
- );
466
-
467
- return parts.join("\n");
468
- }
@@ -1,100 +0,0 @@
1
- /** Convert #rgb / #rrggbb to HSL channel triple "H S% L%" for shadcn-style vars. */
2
- export function hexToHslChannels(hex: string): string {
3
- let h = hex.trim().replace(/^#/, "");
4
- if (h.length === 3) {
5
- h = h
6
- .split("")
7
- .map((c) => c + c)
8
- .join("");
9
- }
10
- if (h.length !== 6 || !/^[0-9a-fA-F]+$/.test(h)) {
11
- throw new Error(`Invalid hex color: ${hex}`);
12
- }
13
- const r = Number.parseInt(h.slice(0, 2), 16) / 255;
14
- const g = Number.parseInt(h.slice(2, 4), 16) / 255;
15
- const b = Number.parseInt(h.slice(4, 6), 16) / 255;
16
- return srgbToHslChannels(r, g, b);
17
- }
18
-
19
- function srgbToHslChannels(r: number, g: number, b: number): string {
20
- const max = Math.max(r, g, b);
21
- const min = Math.min(r, g, b);
22
- const l = (max + min) / 2;
23
- let s = 0;
24
- let hue = 0;
25
- if (max !== min) {
26
- const d = max - min;
27
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
28
- switch (max) {
29
- case r:
30
- hue = (g - b) / d + (g < b ? 6 : 0);
31
- break;
32
- case g:
33
- hue = (b - r) / d + 2;
34
- break;
35
- default:
36
- hue = (r - g) / d + 4;
37
- }
38
- hue /= 6;
39
- }
40
- const H = Math.round(hue * 360);
41
- const S = Math.round(s * 100);
42
- const L = Math.round(l * 100);
43
- return `${H} ${S}% ${L}%`;
44
- }
45
-
46
- /** Already a shadcn-style channel triple? e.g. "228 85% 56%" */
47
- const HSL_CHANNELS_RE = /^(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%$/;
48
-
49
- /**
50
- * Normalize any common color input to HSL channel triple "H S% L%".
51
- * Accepts: #hex, "H S% L%", hsl()/hsla(), rgb()/rgba().
52
- */
53
- export function colorToHslChannels(color: string): string {
54
- const t = color.trim();
55
- if (!t) throw new Error("Empty color");
56
-
57
- const channels = t.match(HSL_CHANNELS_RE);
58
- if (channels) {
59
- return `${Math.round(Number(channels[1]))} ${Math.round(Number(channels[2]))}% ${Math.round(Number(channels[3]))}%`;
60
- }
61
-
62
- // Parse hsl()/rgb() without open-ended \s* quantifiers (ReDoS-safe).
63
- const hslPrefix = t.match(/^hsla?\(/i);
64
- if (hslPrefix) {
65
- const inner = t.slice(hslPrefix[0].length).split(")")[0] ?? "";
66
- const nums = inner.match(/[\d.]+/g) ?? [];
67
- if (nums.length >= 3) {
68
- return `${Math.round(Number(nums[0]))} ${Math.round(Number(nums[1]))}% ${Math.round(Number(nums[2]))}%`;
69
- }
70
- }
71
-
72
- const rgbPrefix = t.match(/^rgba?\(/i);
73
- if (rgbPrefix) {
74
- const inner = t.slice(rgbPrefix[0].length).split(")")[0] ?? "";
75
- const nums = inner.match(/[\d.]+/g) ?? [];
76
- if (nums.length >= 3) {
77
- return srgbToHslChannels(Number(nums[0]) / 255, Number(nums[1]) / 255, Number(nums[2]) / 255);
78
- }
79
- }
80
-
81
- if (t.startsWith("#") || /^[0-9a-fA-F]{3,8}$/.test(t)) {
82
- return hexToHslChannels(t.startsWith("#") ? t : `#${t}`);
83
- }
84
-
85
- throw new Error(`Unsupported color: ${color}`);
86
- }
87
-
88
- /** Prefer {@link colorToHslChannels}; hex-only name kept for call sites. */
89
- export function tryHexToHsl(hex: string | undefined, fallback: string): string {
90
- return tryColorToHsl(hex, fallback);
91
- }
92
-
93
- export function tryColorToHsl(color: string | undefined, fallback: string): string {
94
- if (!color) return fallback;
95
- try {
96
- return colorToHslChannels(color);
97
- } catch {
98
- return fallback;
99
- }
100
- }
@@ -1,71 +0,0 @@
1
- export {
2
- type ApplyBrandOptions,
3
- applyBrandCss,
4
- applyBrandPackage,
5
- BRAND_STORAGE_KEY,
6
- BRAND_STYLE_ELEMENT_ID,
7
- clearBrand,
8
- getActiveBrandId,
9
- restorePersistedBrand,
10
- } from "./apply-brand";
11
- export { compileReferoTokens } from "./compile-refero";
12
- export {
13
- type EmitBrandCssMode,
14
- type EmitBrandCssOptions,
15
- emitBrandCss,
16
- emitDarkModeSelector,
17
- emitGlobalSkinSelector,
18
- emitLightModeSelector,
19
- } from "./emit-css";
20
- export {
21
- colorToHslChannels,
22
- hexToHslChannels,
23
- tryColorToHsl,
24
- tryHexToHsl,
25
- } from "./hex-to-hsl";
26
- export { type InferredRecipeHints, inferRecipeFromDesignMd } from "./infer-recipe";
27
- export {
28
- elevationPresetToTokens,
29
- isDualModeBrand,
30
- normalizeBrandPackage,
31
- normalizeModePalette,
32
- rolesFromSemantic,
33
- semanticFromRoles,
34
- } from "./normalize";
35
- export type {
36
- BadgeDefaultStyle,
37
- BrandColorRoles,
38
- BrandElevationTokens,
39
- BrandExtensions,
40
- BrandFontFace,
41
- BrandFontSource,
42
- BrandModePalette,
43
- BrandModes,
44
- BrandPackage,
45
- BrandPackageInput,
46
- BrandRadii,
47
- BrandRecipe,
48
- BrandRecipeInput,
49
- BrandSemanticColors,
50
- BrandTypeStep,
51
- BrandTypography,
52
- BrandZoneId,
53
- BrandZones,
54
- BrandZoneTypography,
55
- ButtonDefaultStyle,
56
- CompileResult,
57
- CssColor,
58
- Density,
59
- ElevationStyle,
60
- HslChannels,
61
- } from "./types";
62
- export {
63
- applyBrandToIframe,
64
- type BrandIframePreviewOptions,
65
- type UseBrandIframePreviewResult,
66
- type UseBrandOptions,
67
- type UseBrandResult,
68
- useBrand,
69
- useBrandIframePreview,
70
- } from "./use-brand";
71
- export { type ValidationResult, validateBrandPackage } from "./validate";