@nebutra/tokens 0.1.3 → 3.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 (41) hide show
  1. package/CHANGELOG.md +16 -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 +1 -27
  7. package/brands/linear/DESIGN.md +478 -0
  8. package/brands/linear/brand.json +2 -19
  9. package/brands/notion/brand.json +2 -28
  10. package/brands/raycast/brand.json +2 -17
  11. package/brands/stripe/brand.json +1 -16
  12. package/brands/vanta/brand.json +1 -38
  13. package/brands/vercel/brand.json +1 -18
  14. package/dist/brand-package/index.d.ts +13 -7
  15. package/dist/brand-package/index.js +2290 -34
  16. package/dist/brand-package/index.js.map +1 -1
  17. package/dist/brand-package/use-brand.d.ts +1 -1
  18. package/dist/brand-package/use-brand.js +957 -5
  19. package/dist/brand-package/use-brand.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +2417 -22
  22. package/dist/index.js.map +1 -1
  23. package/dist/{use-brand-C8d4DPqE.d.ts → use-brand-ClmvK_lC.d.ts} +19 -3
  24. package/dist/values.d.ts +1192 -0
  25. package/dist/values.js +518 -0
  26. package/dist/values.js.map +1 -0
  27. package/package.json +16 -4
  28. package/recipe.css +36 -2
  29. package/skins/cosmos.css +239 -0
  30. package/skins/gsap.css +32 -20
  31. package/skins/linear.css +54 -17
  32. package/skins/notion.css +36 -25
  33. package/skins/raycast.css +37 -19
  34. package/skins/stripe.css +34 -15
  35. package/skins/vanta.css +34 -30
  36. package/skins/vercel.css +55 -19
  37. package/styles.css +492 -149
  38. package/dist/chunk-ALJTP5HL.js +0 -881
  39. package/dist/chunk-ALJTP5HL.js.map +0 -1
  40. package/dist/chunk-RGUJQOLH.js +0 -1582
  41. package/dist/chunk-RGUJQOLH.js.map +0 -1
@@ -1,40 +1,2298 @@
1
- import {
2
- compileReferoTokens,
3
- inferRecipeFromDesignMd,
4
- validateBrandPackage
5
- } from "../chunk-RGUJQOLH.js";
6
- import {
7
- BRAND_STORAGE_KEY,
8
- BRAND_STYLE_ELEMENT_ID,
9
- applyBrandCss,
10
- applyBrandPackage,
11
- applyBrandToIframe,
12
- clearBrand,
13
- colorToHslChannels,
14
- elevationPresetToTokens,
15
- emitBrandCss,
16
- emitDarkModeSelector,
17
- emitGlobalSkinSelector,
18
- emitLightModeSelector,
19
- getActiveBrandId,
20
- hexToHslChannels,
21
- isDualModeBrand,
22
- normalizeBrandPackage,
23
- normalizeModePalette,
24
- restorePersistedBrand,
25
- rolesFromSemantic,
26
- semanticFromRoles,
27
- tryColorToHsl,
28
- tryHexToHsl,
29
- useBrand,
30
- useBrandIframePreview
31
- } from "../chunk-ALJTP5HL.js";
1
+ // src/brand-package/emit-css.ts
2
+ import { withNearestRegistryFont } from "@nebutra/fonts/registry";
3
+
4
+ // src/brand-package/hex-to-hsl.ts
5
+ function hexToHslChannels(hex) {
6
+ let h = hex.trim().replace(/^#/, "");
7
+ if (h.length === 3) {
8
+ h = h.split("").map((c) => c + c).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
+ function srgbToHslChannels(r, g, b) {
19
+ const max = Math.max(r, g, b);
20
+ const min = Math.min(r, g, b);
21
+ const l = (max + min) / 2;
22
+ let s = 0;
23
+ let hue = 0;
24
+ if (max !== min) {
25
+ const d = max - min;
26
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
27
+ switch (max) {
28
+ case r:
29
+ hue = (g - b) / d + (g < b ? 6 : 0);
30
+ break;
31
+ case g:
32
+ hue = (b - r) / d + 2;
33
+ break;
34
+ default:
35
+ hue = (r - g) / d + 4;
36
+ }
37
+ hue /= 6;
38
+ }
39
+ const H = Math.round(hue * 360);
40
+ const S = Math.round(s * 100);
41
+ const L = Math.round(l * 100);
42
+ return `${H} ${S}% ${L}%`;
43
+ }
44
+ var HSL_CHANNELS_RE = /^(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%$/;
45
+ function colorToHslChannels(color) {
46
+ const t = color.trim();
47
+ if (!t) throw new Error("Empty color");
48
+ const channels = t.match(HSL_CHANNELS_RE);
49
+ if (channels) {
50
+ return `${Math.round(Number(channels[1]))} ${Math.round(Number(channels[2]))}% ${Math.round(Number(channels[3]))}%`;
51
+ }
52
+ const hslPrefix = t.match(/^hsla?\(/i);
53
+ if (hslPrefix) {
54
+ const inner = t.slice(hslPrefix[0].length).split(")")[0] ?? "";
55
+ const nums = inner.match(/[\d.]+/g) ?? [];
56
+ if (nums.length >= 3) {
57
+ return `${Math.round(Number(nums[0]))} ${Math.round(Number(nums[1]))}% ${Math.round(Number(nums[2]))}%`;
58
+ }
59
+ }
60
+ const rgbPrefix = t.match(/^rgba?\(/i);
61
+ if (rgbPrefix) {
62
+ const inner = t.slice(rgbPrefix[0].length).split(")")[0] ?? "";
63
+ const nums = inner.match(/[\d.]+/g) ?? [];
64
+ if (nums.length >= 3) {
65
+ return srgbToHslChannels(Number(nums[0]) / 255, Number(nums[1]) / 255, Number(nums[2]) / 255);
66
+ }
67
+ }
68
+ if (t.startsWith("#") || /^[0-9a-fA-F]{3,8}$/.test(t)) {
69
+ return hexToHslChannels(t.startsWith("#") ? t : `#${t}`);
70
+ }
71
+ throw new Error(`Unsupported color: ${color}`);
72
+ }
73
+ function tryHexToHsl(hex, fallback) {
74
+ return tryColorToHsl(hex, fallback);
75
+ }
76
+ function tryColorToHsl(color, fallback) {
77
+ if (!color) return fallback;
78
+ try {
79
+ return colorToHslChannels(color);
80
+ } catch {
81
+ return fallback;
82
+ }
83
+ }
84
+
85
+ // src/brand-package/normalize.ts
86
+ var NONE = "0 0 #0000";
87
+ var KEY_SHADOW = "rgba(255, 255, 255, 0.05) 0px 1px 0px 0px inset, rgba(255, 255, 255, 0.25) 0px 0px 0px 1px, rgba(0, 0, 0, 0.2) 0px -1px 0px 0px inset";
88
+ var HAIRLINE_SHADOW = "rgba(0, 0, 0, 0.08) 0px 0px 0px 1px, rgb(250, 250, 250) 0px 0px 0px 1px";
89
+ var SOFT_CARD = "0 1px 2px 0 rgb(0 0 0 / 0.05)";
90
+ var SOFT_CONTROL = "0 1px 2px 0 rgb(0 0 0 / 0.04)";
91
+ var SOFT_RAISED = "0 4px 6px -1px rgb(0 0 0 / 0.1)";
92
+ function asChannels(value) {
93
+ if (value == null || value === "") return void 0;
94
+ const v = value.trim();
95
+ if (v.startsWith("#")) return tryHexToHsl(v, "0 0% 50%");
96
+ return v;
97
+ }
98
+ function elevationPresetToTokens(preset, cardShadow) {
99
+ switch (preset) {
100
+ case "none":
101
+ return { card: NONE, control: NONE, raised: NONE };
102
+ case "key":
103
+ return {
104
+ card: cardShadow ?? KEY_SHADOW,
105
+ control: NONE,
106
+ raised: cardShadow ?? KEY_SHADOW
107
+ };
108
+ case "hairline":
109
+ return {
110
+ card: cardShadow ?? HAIRLINE_SHADOW,
111
+ control: NONE,
112
+ raised: cardShadow ?? HAIRLINE_SHADOW
113
+ };
114
+ case "raised":
115
+ return { card: SOFT_RAISED, control: SOFT_CONTROL, raised: SOFT_RAISED };
116
+ default:
117
+ return { card: SOFT_CARD, control: SOFT_CONTROL, raised: SOFT_RAISED };
118
+ }
119
+ }
120
+ function rolesFromSemantic(s, brandMark) {
121
+ const roles = {
122
+ canvas: s.background,
123
+ canvasForeground: s.foreground,
124
+ surface: s.card,
125
+ surfaceForeground: s.cardForeground,
126
+ action: s.primary,
127
+ actionForeground: s.primaryForeground,
128
+ quiet: s.secondary,
129
+ quietForeground: s.secondaryForeground,
130
+ muted: s.muted,
131
+ mutedForeground: s.mutedForeground,
132
+ border: s.border,
133
+ ring: s.ring,
134
+ destructive: s.destructive,
135
+ destructiveForeground: s.destructiveForeground
136
+ };
137
+ if (s.input) roles.input = s.input;
138
+ const brand = asChannels(brandMark?.brand);
139
+ if (brand) roles.brand = brand;
140
+ const brandFg = asChannels(brandMark?.brandForeground);
141
+ if (brandFg) roles.brandForeground = brandFg;
142
+ if (s.success) roles.success = s.success;
143
+ if (s.successForeground) roles.successForeground = s.successForeground;
144
+ if (s.warning) roles.warning = s.warning;
145
+ if (s.warningForeground) roles.warningForeground = s.warningForeground;
146
+ if (s.info) roles.info = s.info;
147
+ if (s.infoForeground) roles.infoForeground = s.infoForeground;
148
+ return roles;
149
+ }
150
+ function semanticFromRoles(r) {
151
+ const accent = r.brand ?? r.quiet;
152
+ const accentFg = r.brandForeground ?? r.quietForeground;
153
+ const semantic = {
154
+ background: r.canvas,
155
+ foreground: r.canvasForeground,
156
+ card: r.surface,
157
+ cardForeground: r.surfaceForeground,
158
+ popover: r.surface,
159
+ popoverForeground: r.surfaceForeground,
160
+ primary: r.action,
161
+ primaryForeground: r.actionForeground,
162
+ secondary: r.quiet,
163
+ secondaryForeground: r.quietForeground,
164
+ muted: r.muted,
165
+ mutedForeground: r.mutedForeground,
166
+ accent,
167
+ accentForeground: accentFg,
168
+ destructive: r.destructive,
169
+ destructiveForeground: r.destructiveForeground,
170
+ border: r.border,
171
+ ring: r.ring
172
+ };
173
+ if (r.input) semantic.input = r.input;
174
+ if (r.success) semantic.success = r.success;
175
+ if (r.successForeground) semantic.successForeground = r.successForeground;
176
+ if (r.warning) semantic.warning = r.warning;
177
+ if (r.warningForeground) semantic.warningForeground = r.warningForeground;
178
+ if (r.info) semantic.info = r.info;
179
+ if (r.infoForeground) semantic.infoForeground = r.infoForeground;
180
+ return semantic;
181
+ }
182
+ function normalizeRadii(recipe) {
183
+ const button = recipe.radii?.button ?? recipe.buttonRadius ?? "0.375rem";
184
+ const card = recipe.radii?.card ?? recipe.cardRadius ?? "0.75rem";
185
+ const radii = {
186
+ button,
187
+ card,
188
+ badge: recipe.radii?.badge ?? recipe.badgeRadius ?? "9999px",
189
+ input: recipe.radii?.input ?? recipe.inputRadius ?? button,
190
+ pill: recipe.radii?.pill ?? "9999px"
191
+ };
192
+ return radii;
193
+ }
194
+ function normalizeElevation(recipe) {
195
+ if (recipe.elevationTokens?.card) {
196
+ const elev = { card: recipe.elevationTokens.card };
197
+ elev.control = recipe.elevationTokens.control ?? NONE;
198
+ elev.raised = recipe.elevationTokens.raised ?? recipe.elevationTokens.card;
199
+ return elev;
200
+ }
201
+ return elevationPresetToTokens(recipe.elevation, recipe.cardShadow);
202
+ }
203
+ function normalizeBadgeDefault(badge) {
204
+ if (!badge || badge === "match-primary") return "match-action";
205
+ return badge;
206
+ }
207
+ function categoryBrandMark(brand) {
208
+ return typeof brand.extensions?.categories?.brand === "string" ? brand.extensions.categories.brand : void 0;
209
+ }
210
+ function normalizeModePalette(palette, categoryBrand) {
211
+ let baseRoles;
212
+ if (palette.roles) {
213
+ baseRoles = { ...palette.roles };
214
+ } else if (palette.semantic) {
215
+ const mark = {
216
+ brandForeground: palette.semantic.primaryForeground
217
+ };
218
+ if (categoryBrand) mark.brand = categoryBrand;
219
+ baseRoles = rolesFromSemantic(palette.semantic, mark);
220
+ } else {
221
+ throw new Error("BrandModePalette requires roles or semantic");
222
+ }
223
+ const roles = {
224
+ canvas: baseRoles.canvas,
225
+ canvasForeground: baseRoles.canvasForeground,
226
+ surface: baseRoles.surface,
227
+ surfaceForeground: baseRoles.surfaceForeground,
228
+ action: baseRoles.action,
229
+ actionForeground: baseRoles.actionForeground,
230
+ quiet: baseRoles.quiet,
231
+ quietForeground: baseRoles.quietForeground,
232
+ muted: baseRoles.muted,
233
+ mutedForeground: baseRoles.mutedForeground,
234
+ border: baseRoles.border,
235
+ ring: baseRoles.ring,
236
+ destructive: baseRoles.destructive,
237
+ destructiveForeground: baseRoles.destructiveForeground
238
+ };
239
+ if (baseRoles.input) roles.input = baseRoles.input;
240
+ const brandCh = asChannels(baseRoles.brand);
241
+ if (brandCh) roles.brand = brandCh;
242
+ const brandFg = asChannels(baseRoles.brandForeground) ?? baseRoles.actionForeground;
243
+ if (brandCh) roles.brandForeground = brandFg;
244
+ if (baseRoles.success) roles.success = baseRoles.success;
245
+ if (baseRoles.successForeground) roles.successForeground = baseRoles.successForeground;
246
+ if (baseRoles.warning) roles.warning = baseRoles.warning;
247
+ if (baseRoles.warningForeground) roles.warningForeground = baseRoles.warningForeground;
248
+ if (baseRoles.info) roles.info = baseRoles.info;
249
+ if (baseRoles.infoForeground) roles.infoForeground = baseRoles.infoForeground;
250
+ return { roles, semantic: semanticFromRoles(roles) };
251
+ }
252
+ function normalizeModes(brand, categoryBrand) {
253
+ const raw = brand.modes;
254
+ if (!raw?.light && !raw?.dark) return void 0;
255
+ const modes = {};
256
+ if (raw.light) {
257
+ const n = normalizeModePalette(raw.light, categoryBrand);
258
+ modes.light = { roles: n.roles, semantic: n.semantic };
259
+ }
260
+ if (raw.dark) {
261
+ const n = normalizeModePalette(raw.dark, categoryBrand);
262
+ modes.dark = { roles: n.roles, semantic: n.semantic };
263
+ }
264
+ if (modes.light && modes.dark) return modes;
265
+ return modes;
266
+ }
267
+ function normalizeBrandPackage(brand) {
268
+ const categoryBrand = categoryBrandMark(brand);
269
+ const modes = normalizeModes(brand, categoryBrand);
270
+ const defaultModeKey = brand.darkDefault ? "dark" : "light";
271
+ const defaultMode = modes?.[defaultModeKey] ?? modes?.light ?? modes?.dark;
272
+ let primaryPalette;
273
+ if (defaultMode?.roles || defaultMode?.semantic) {
274
+ primaryPalette = {};
275
+ if (defaultMode.roles) primaryPalette.roles = defaultMode.roles;
276
+ if (defaultMode.semantic) primaryPalette.semantic = defaultMode.semantic;
277
+ } else {
278
+ primaryPalette = { semantic: brand.semantic };
279
+ if (brand.roles) primaryPalette.roles = brand.roles;
280
+ }
281
+ const { roles, semantic } = normalizeModePalette(primaryPalette, categoryBrand);
282
+ const looseRecipe = brand.recipe;
283
+ const radii = normalizeRadii(looseRecipe);
284
+ const elevationTokens = normalizeElevation(looseRecipe);
285
+ const recipe = {
286
+ buttonDefault: looseRecipe.buttonDefault,
287
+ density: looseRecipe.density ?? "comfortable",
288
+ badgeDefault: normalizeBadgeDefault(looseRecipe.badgeDefault),
289
+ radii,
290
+ elevationTokens
291
+ };
292
+ if (looseRecipe.primaryStrokeGradient) {
293
+ recipe.primaryStrokeGradient = looseRecipe.primaryStrokeGradient;
294
+ }
295
+ if (looseRecipe.outlineBorder) {
296
+ recipe.outlineBorder = looseRecipe.outlineBorder;
297
+ }
298
+ const out = {
299
+ ...brand,
300
+ roles,
301
+ semantic,
302
+ recipe
303
+ };
304
+ if (modes) out.modes = modes;
305
+ else delete out.modes;
306
+ return out;
307
+ }
308
+ function isDualModeBrand(brand) {
309
+ return Boolean(brand.modes?.light?.semantic && brand.modes?.dark?.semantic);
310
+ }
311
+
312
+ // src/brand-package/emit-css.ts
313
+ function cssFontStack(stack) {
314
+ return (withNearestRegistryFont(stack) ?? stack).replace(/'/g, '"');
315
+ }
316
+ var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
317
+ "ui-sans-serif",
318
+ "ui-serif",
319
+ "ui-monospace",
320
+ "system-ui",
321
+ "sans-serif",
322
+ "serif",
323
+ "monospace",
324
+ "-apple-system",
325
+ "BlinkMacSystemFont"
326
+ ]);
327
+ function withCjkTail(stack) {
328
+ const families = stack.split(",").map((f) => f.trim());
329
+ const cut = families.findIndex((f) => GENERIC_FAMILIES.has(f.replace(/"/g, "")));
330
+ if (cut === -1 || families.some((f) => f.includes("MiSans"))) return stack;
331
+ const cjk = ['var(--font-misans, "MiSans")', '"PingFang SC"'];
332
+ return [...families.slice(0, cut), ...cjk, ...families.slice(cut)].join(", ");
333
+ }
334
+ function motionVars(motion) {
335
+ if (!motion) return [];
336
+ const lines = [];
337
+ const curves = [
338
+ ["easeOut", "ease-out"],
339
+ ["easeInOut", "ease-in-out"],
340
+ ["easeSpring", "ease-spring"]
341
+ ];
342
+ for (const [key, name] of curves) {
343
+ const value = motion[key];
344
+ if (typeof value !== "string" || !value.trim()) continue;
345
+ lines.push(` --${name}: ${value};`);
346
+ lines.push(` --motion-${name}: ${value};`);
347
+ }
348
+ const durations = [
349
+ ["micro", "micro"],
350
+ ["flow", "flow"],
351
+ ["reveal", "reveal"],
352
+ ["cinematic", "cinematic"]
353
+ ];
354
+ for (const [key, name] of durations) {
355
+ const value = motion[key];
356
+ if (typeof value !== "number" || !Number.isFinite(value)) continue;
357
+ lines.push(` --duration-${name}: ${value}ms;`);
358
+ lines.push(` --motion-duration-${name}: ${value}ms;`);
359
+ }
360
+ return lines.length ? [``, ` /* Motion */`, ...lines] : [];
361
+ }
362
+ function spacingVars(spacing) {
363
+ if (!spacing) return [];
364
+ const lines = [];
365
+ const steps = [
366
+ ["xs", "xs"],
367
+ ["sm", "sm"],
368
+ ["md", "md"],
369
+ ["lg", "lg"],
370
+ ["xl", "xl"],
371
+ ["2xl", "2xl"]
372
+ ];
373
+ for (const [key, name] of steps) {
374
+ const value = spacing[key];
375
+ if (typeof value !== "string" || !value.trim()) continue;
376
+ lines.push(` --space-source-${name}: ${value};`);
377
+ }
378
+ return lines.length ? [``, ` /* Spacing */`, ...lines] : [];
379
+ }
380
+ function recipeVars(recipe) {
381
+ const radii = recipe.radii;
382
+ const elev = recipe.elevationTokens;
383
+ const lines = [
384
+ ` /* Shape slots */`,
385
+ ` --btn-default-radius: ${radii.button};`,
386
+ ` --radius-button: ${radii.button};`,
387
+ ` --radius-buttons: ${radii.button};`,
388
+ // A step on the size scale, not the button role.
389
+ //
390
+ // Aliasing it straight to `radii.button` is fine while a language's buttons
391
+ // are modestly rounded — five of the seven are 4–8px and never noticed. It
392
+ // breaks for the two whose buttons are pills: GSAP emitted
393
+ // `--radius-md: 100px` and Vanta 999px, and forty-two files read that step
394
+ // for panels and surfaces. The Combobox popover under GSAP came out as a
395
+ // lozenge, which is what a 100px radius does to a 260px-wide panel.
396
+ //
397
+ // `min()` keeps every language whose button is the tighter of the two
398
+ // exactly where it was, and stops a pill from escaping its role. A pill is
399
+ // a control decision; it has no business setting the radius of a surface.
400
+ ` --radius-md: min(${radii.button}, ${radii.card});`,
401
+ ` --radius-card: ${radii.card};`,
402
+ ` --radius-lg: ${radii.card};`,
403
+ ` --radius-badge: ${radii.badge ?? "9999px"};`,
404
+ ` --badge-default-radius: ${radii.badge ?? "9999px"};`,
405
+ ` --radius-inputs: ${radii.input ?? radii.button};`,
406
+ ` --input-radius: ${radii.input ?? radii.button};`,
407
+ ` --radius-pill: ${radii.pill ?? "9999px"};`,
408
+ ``,
409
+ ` /* Free elevation (carrier-provided CSS shadows) */`,
410
+ ` --elevation-card: ${elev.card};`,
411
+ ` --elevation-control: ${elev.control ?? "0 0 #0000"};`,
412
+ ` --elevation-raised: ${elev.raised ?? elev.card};`,
413
+ // --elevation-*, not --shadow-*. The theme block maps `--shadow-md` to
414
+ // `var(--elevation-md)` and inlines that into `.shadow-md`, so a skin that
415
+ // set --shadow-md was writing to the alias while the utility read the
416
+ // source. Every language's shadows were therefore byte-identical in the
417
+ // browser — measured across all seven with getComputedStyle — while the
418
+ // token files disagreed convincingly. Setting the source is what a brand
419
+ // switch needs; the alias takes care of itself.
420
+ ` --elevation-xs: ${elev.control ?? "0 0 #0000"};`,
421
+ ` --elevation-sm: ${elev.card};`,
422
+ ` --elevation-md: ${elev.raised ?? elev.card};`,
423
+ ` --elevation-lg: ${elev.raised ?? elev.card};`,
424
+ ` --btn-default-shadow: 0 0 #0000;`
425
+ ];
426
+ switch (recipe.buttonDefault) {
427
+ case "outline": {
428
+ const edge = recipe.outlineBorder ?? "hsl(var(--foreground))";
429
+ lines.push(" --btn-default-bg: transparent;");
430
+ lines.push(" --btn-default-fg: hsl(var(--foreground));");
431
+ lines.push(" --btn-default-border-width: 1px;");
432
+ lines.push(" --btn-default-border: transparent;");
433
+ lines.push(` --btn-default-stroke-gradient: linear-gradient(${edge}, ${edge});`);
434
+ lines.push(" --btn-default-hover-bg: hsl(var(--foreground) / 0.06);");
435
+ break;
436
+ }
437
+ case "gradient-stroke": {
438
+ const grad = recipe.primaryStrokeGradient ?? "linear-gradient(135deg, hsl(var(--primary)), color-mix(in srgb, hsl(var(--primary)) 55%, white))";
439
+ lines.push(" --btn-default-bg: transparent;");
440
+ lines.push(" --btn-default-fg: hsl(var(--foreground));");
441
+ lines.push(" --btn-default-border-width: 1.5px;");
442
+ lines.push(" --btn-default-border: transparent;");
443
+ lines.push(` --btn-default-stroke-gradient: ${grad};`);
444
+ lines.push(" --btn-default-hover-bg: hsl(var(--primary) / 0.08);");
445
+ break;
446
+ }
447
+ default:
448
+ lines.push(" --btn-default-bg: hsl(var(--primary));");
449
+ lines.push(" --btn-default-fg: hsl(var(--primary-foreground));");
450
+ lines.push(" --btn-default-border-width: 0px;");
451
+ lines.push(" --btn-default-border: transparent;");
452
+ lines.push(" --btn-default-stroke-gradient: linear-gradient(transparent, transparent);");
453
+ lines.push(
454
+ " --btn-default-hover-bg: color-mix(in srgb, hsl(var(--primary)) 90%, transparent);"
455
+ );
456
+ break;
457
+ }
458
+ const badgeMode = recipe.badgeDefault ?? "match-action";
459
+ if (badgeMode === "outline") {
460
+ const edge = recipe.outlineBorder ?? "hsl(var(--border))";
461
+ lines.push(" --badge-default-bg: transparent;");
462
+ lines.push(" --badge-default-fg: hsl(var(--foreground));");
463
+ lines.push(` --badge-default-border: ${edge};`);
464
+ lines.push(" --badge-default-hover-bg: hsl(var(--foreground) / 0.06);");
465
+ } else if (badgeMode === "muted") {
466
+ lines.push(" --badge-default-bg: hsl(var(--secondary));");
467
+ lines.push(" --badge-default-fg: hsl(var(--secondary-foreground));");
468
+ lines.push(" --badge-default-border: transparent;");
469
+ lines.push(" --badge-default-hover-bg: color-mix(in srgb, hsl(var(--secondary)) 90%, white);");
470
+ } else if (badgeMode === "brand") {
471
+ lines.push(" --badge-default-bg: hsl(var(--brand-mark, var(--accent)));");
472
+ lines.push(
473
+ " --badge-default-fg: hsl(var(--brand-mark-foreground, var(--accent-foreground)));"
474
+ );
475
+ lines.push(" --badge-default-border: transparent;");
476
+ lines.push(
477
+ " --badge-default-hover-bg: color-mix(in srgb, hsl(var(--brand-mark, var(--accent))) 85%, transparent);"
478
+ );
479
+ } else if (recipe.buttonDefault === "outline" || recipe.buttonDefault === "gradient-stroke") {
480
+ const edge = recipe.outlineBorder ?? "hsl(var(--foreground))";
481
+ lines.push(" --badge-default-bg: transparent;");
482
+ lines.push(" --badge-default-fg: hsl(var(--foreground));");
483
+ lines.push(` --badge-default-border: ${edge};`);
484
+ lines.push(" --badge-default-hover-bg: hsl(var(--foreground) / 0.06);");
485
+ } else {
486
+ lines.push(" --badge-default-bg: hsl(var(--primary));");
487
+ lines.push(" --badge-default-fg: hsl(var(--primary-foreground));");
488
+ lines.push(" --badge-default-border: transparent;");
489
+ lines.push(
490
+ " --badge-default-hover-bg: color-mix(in srgb, hsl(var(--primary)) 80%, transparent);"
491
+ );
492
+ }
493
+ if (recipe.density === "compact") {
494
+ lines.push(" --btn-default-padding-y: 0.5rem;");
495
+ lines.push(" --btn-default-padding-x: 0.875rem;");
496
+ lines.push(" --control-height-tiny: 1.25rem;");
497
+ lines.push(" --control-height-sm: 1.75rem;");
498
+ lines.push(" --control-height-md: 2rem;");
499
+ lines.push(" --control-height-lg: 2.5rem;");
500
+ lines.push(" --control-height-icon-sm: 1.5rem;");
501
+ lines.push(" --control-height-icon-md: 1.75rem;");
502
+ lines.push(" --control-height-icon-lg: 2rem;");
503
+ lines.push(" --control-font-size-md: 0.8125rem;");
504
+ } else if (recipe.density === "spacious") {
505
+ lines.push(" --btn-default-padding-y: 0.875rem;");
506
+ lines.push(" --btn-default-padding-x: 1.5rem;");
507
+ lines.push(" --control-height-tiny: 1.75rem;");
508
+ lines.push(" --control-height-sm: 2.25rem;");
509
+ lines.push(" --control-height-md: 2.75rem;");
510
+ lines.push(" --control-height-lg: 3.25rem;");
511
+ lines.push(" --control-height-icon-sm: 2rem;");
512
+ lines.push(" --control-height-icon-md: 2.25rem;");
513
+ lines.push(" --control-height-icon-lg: 2.5rem;");
514
+ }
515
+ return lines;
516
+ }
517
+ function isServableFaceUrl(url) {
518
+ return url.startsWith("/") && !url.startsWith("//");
519
+ }
520
+ function emitFontFaces(faces) {
521
+ const servable = faces?.filter((face) => face.src.every((s) => isServableFaceUrl(s.url)));
522
+ if (!servable?.length) return [];
523
+ const out = ["/* Brand font faces */"];
524
+ for (const face of servable) {
525
+ const src = face.src.map((s) => {
526
+ const fmt = s.format ? ` format("${s.format}")` : "";
527
+ return `url("${s.url}")${fmt}`;
528
+ }).join(", ");
529
+ out.push("@font-face {");
530
+ out.push(` font-family: "${face.family}";`);
531
+ out.push(` src: ${src};`);
532
+ if (face.weight != null) out.push(` font-weight: ${face.weight};`);
533
+ if (face.style) out.push(` font-style: ${face.style};`);
534
+ out.push(` font-display: ${face.display ?? "swap"};`);
535
+ if (face.unicodeRange) out.push(` unicode-range: ${face.unicodeRange};`);
536
+ out.push("}");
537
+ out.push("");
538
+ }
539
+ return out;
540
+ }
541
+ function emitGlobalSkinSelector(brandId, darkDefault, root = "html") {
542
+ if (darkDefault) {
543
+ return `:root,
544
+ .dark,
545
+ ${root}[data-brand="${brandId}"] {`;
546
+ }
547
+ return `:root,
548
+ ${root}[data-brand="${brandId}"] {`;
549
+ }
550
+ function emitLightModeSelector(brandId, mode, root = "html") {
551
+ if (mode === "scoped") return `${root}[data-brand="${brandId}"] {`;
552
+ return `:root,
553
+ ${root}[data-brand="${brandId}"] {`;
554
+ }
555
+ function emitDarkModeSelector(brandId, mode, root = "html") {
556
+ if (mode === "scoped") return `${root}.dark[data-brand="${brandId}"] {`;
557
+ return `.dark,
558
+ ${root}.dark[data-brand="${brandId}"] {`;
559
+ }
560
+ function neutralRamp(s, r) {
561
+ const hsl = (triple) => `hsl(${triple})`;
562
+ const anchors = [
563
+ [1, hsl(r?.canvas ?? s.background)],
564
+ [2, hsl(r?.surface ?? s.card)],
565
+ [7, hsl(r?.border ?? s.border)],
566
+ [11, hsl(r?.mutedForeground ?? s.mutedForeground)],
567
+ [12, hsl(r?.canvasForeground ?? s.foreground)]
568
+ ];
569
+ const lines = [``, ` /* \u2500\u2500 Neutral ramp, anchored on this language's roles \u2500\u2500 */`];
570
+ for (let step = 1; step <= 12; step++) {
571
+ const exact = anchors.find(([n]) => n === step);
572
+ if (exact) {
573
+ lines.push(` --neutral-${step}: ${exact[1]};`);
574
+ continue;
575
+ }
576
+ const lower = [...anchors].reverse().find(([n]) => n < step);
577
+ const upper = anchors.find(([n]) => n > step);
578
+ if (!lower || !upper) continue;
579
+ const t = Math.round((step - lower[0]) / (upper[0] - lower[0]) * 100);
580
+ lines.push(` --neutral-${step}: color-mix(in oklab, ${upper[1]} ${t}%, ${lower[1]});`);
581
+ }
582
+ return lines;
583
+ }
584
+ function emitColorVars(s, r) {
585
+ const roleLines = [
586
+ ` /* \u2500\u2500 Color roles (carrier) \u2500\u2500 */`,
587
+ ` --role-canvas: ${r?.canvas ?? s.background};`,
588
+ ` --role-canvas-fg: ${r?.canvasForeground ?? s.foreground};`,
589
+ ` --role-surface: ${r?.surface ?? s.card};`,
590
+ ` --role-surface-fg: ${r?.surfaceForeground ?? s.cardForeground};`,
591
+ ` --role-action: ${r?.action ?? s.primary};`,
592
+ ` --role-action-fg: ${r?.actionForeground ?? s.primaryForeground};`,
593
+ ` --role-quiet: ${r?.quiet ?? s.secondary};`,
594
+ ` --role-quiet-fg: ${r?.quietForeground ?? s.secondaryForeground};`,
595
+ ` --role-muted: ${r?.muted ?? s.muted};`,
596
+ ` --role-muted-fg: ${r?.mutedForeground ?? s.mutedForeground};`,
597
+ ` --role-border: ${r?.border ?? s.border};`,
598
+ ` --role-input: ${r?.input ?? s.input ?? s.border};`,
599
+ ` --role-ring: ${r?.ring ?? s.ring};`
600
+ ];
601
+ if (r?.brand) {
602
+ roleLines.push(` --role-brand: ${r.brand};`);
603
+ roleLines.push(
604
+ ` --role-brand-fg: ${r.brandForeground ?? r.actionForeground ?? s.primaryForeground};`
605
+ );
606
+ roleLines.push(` --brand-mark: ${r.brand};`);
607
+ roleLines.push(
608
+ ` --brand-mark-foreground: ${r.brandForeground ?? r.actionForeground ?? s.primaryForeground};`
609
+ );
610
+ }
611
+ const semantic = [
612
+ ``,
613
+ ` /* \u2500\u2500 shadcn bridge (primary = action CTA) \u2500\u2500 */`,
614
+ ` --background: ${s.background};`,
615
+ ` --foreground: ${s.foreground};`,
616
+ ` --card: ${s.card};`,
617
+ ` --card-foreground: ${s.cardForeground};`,
618
+ ` --popover: ${s.popover};`,
619
+ ` --popover-foreground: ${s.popoverForeground};`,
620
+ ` --primary: ${s.primary};`,
621
+ ` --primary-foreground: ${s.primaryForeground};`,
622
+ ` --secondary: ${s.secondary};`,
623
+ ` --secondary-foreground: ${s.secondaryForeground};`,
624
+ ` --muted: ${s.muted};`,
625
+ ` --muted-foreground: ${s.mutedForeground};`,
626
+ ` --accent: ${s.accent};`,
627
+ ` --accent-foreground: ${s.accentForeground};`,
628
+ ` --destructive: ${s.destructive};`,
629
+ ` --destructive-foreground: ${s.destructiveForeground};`,
630
+ ` --border: ${s.border};`,
631
+ // Field stroke, not field fill: --input reaches the DOM only through
632
+ // `border-input`. A language that omits it inherits the hairline colour,
633
+ // which is always a visible boundary — writing the surface colour here
634
+ // drew the outline in the same colour as what sits behind it.
635
+ ` --input: ${s.input ?? s.border};`,
636
+ ` --ring: ${s.ring};`
637
+ ];
638
+ if (s.success) semantic.push(` --success: ${s.success};`);
639
+ if (s.successForeground) semantic.push(` --success-foreground: ${s.successForeground};`);
640
+ if (s.warning) semantic.push(` --warning: ${s.warning};`);
641
+ if (s.warningForeground) semantic.push(` --warning-foreground: ${s.warningForeground};`);
642
+ if (s.info) semantic.push(` --info: ${s.info};`);
643
+ if (s.infoForeground) semantic.push(` --info-foreground: ${s.infoForeground};`);
644
+ semantic.push(
645
+ ` --sidebar: ${s.card};`,
646
+ ` --sidebar-foreground: ${s.foreground};`,
647
+ ` --sidebar-primary: ${s.primary};`,
648
+ ` --sidebar-primary-foreground: ${s.primaryForeground};`,
649
+ ` --sidebar-accent: ${s.accent};`,
650
+ ` --sidebar-accent-foreground: ${s.accentForeground};`,
651
+ ` --sidebar-border: ${s.border};`,
652
+ ` --sidebar-ring: ${s.ring};`,
653
+ // The identity aliases, taken over by the language.
654
+ //
655
+ // These were the one family a Brand Package did not reach: --brand-primary
656
+ // and --brand-accent are declared in styles.css as Nebutra's own #0033FE and
657
+ // #0BF1C3, and no skin overrode them. Switching to Linear moved three
658
+ // thousand semantic usages and left seventy-five sitting in Nebutra's cyan —
659
+ // a page that reads as half-switched rather than as another language.
660
+ //
661
+ // A language has exactly two vivid hues to offer, and they are `roles.brand`
662
+ // — the identity mark — and `roles.action`, the product fill. Notion is blue
663
+ // on black, Raycast coral behind near-white chrome, Stripe indigo on
664
+ // midnight. --brand-primary takes the mark, --brand-accent the action, and a
665
+ // language declaring no separate mark simply shows one hue in both, which is
666
+ // true of it rather than a gap to paper over.
667
+ //
668
+ // NOT `semantic.accent`: that is the shadcn hover-surface role, and mapping
669
+ // it here produced --brand-accent: 220 14% 96% for Linear and 70 5% 25% for
670
+ // GSAP. The glow elevations would have tinted themselves with a hover grey.
671
+ // Same slot-versus-role confusion as --radius-md and --input, one family out.
672
+ //
673
+ // --brand-tertiary falls back to the accent rather than inventing a third
674
+ // hue: two honest hues beat three where one is made up.
675
+ //
676
+ // Emitted as complete colours, not channel triples. --brand-accent is read
677
+ // inside `color-mix(in srgb, var(--brand-accent) 8%, transparent)` by the
678
+ // glow elevations, and a bare triple there voids the whole declaration —
679
+ // silently, which is the failure this codebase keeps relearning.
680
+ ` --brand-primary: hsl(${r?.brand ?? s.primary});`,
681
+ ` --brand-accent: hsl(${s.primary});`,
682
+ ` --brand-accent-foreground: hsl(${s.primaryForeground});`,
683
+ ` --brand-tertiary: hsl(${s.primary});`,
684
+ ` --brand-gradient: hsl(var(--primary));`,
685
+ ` --brand-gradient-reverse: hsl(var(--primary));`,
686
+ ` --brand-gradient-vertical: hsl(var(--primary));`,
687
+ ` --brand-gradient-radial: hsl(var(--primary));`
688
+ );
689
+ return [...roleLines, ...semantic, ...neutralRamp(s, r)];
690
+ }
691
+ function emitBrandCss(brand, options = {}) {
692
+ const mode = options.mode ?? "global";
693
+ const root = options.root ?? "html";
694
+ const b = normalizeBrandPackage(brand);
695
+ const t = b.typography;
696
+ const dual = isDualModeBrand(b);
697
+ const parts = [
698
+ `/**`,
699
+ ` * Brand carrier skin: ${b.name} (${b.id}) v${b.version}`,
700
+ ` * darkDefault=${b.darkDefault} dualMode=${dual} button=${b.recipe.buttonDefault}`,
701
+ ` * fonts=${t.faces?.length ?? 0} mode=${mode}`,
702
+ ` * Contract: roles.action \u2192 --primary; roles.brand \u2192 --brand-mark (never default CTA)`,
703
+ ` */`,
704
+ ``,
705
+ ...emitFontFaces(t.faces)
706
+ ];
707
+ const fontSans = withCjkTail(cssFontStack(t.fontSans));
708
+ const fontDisplay = withCjkTail(cssFontStack(t.fontDisplay ?? t.fontSans));
709
+ const typeLines = [
710
+ ` --font-sans: ${fontSans};`,
711
+ // The CJK-locale rule in base.css sets body's font-family from --font-cn,
712
+ // which sits above --font-sans in specificity. A skin that left it alone
713
+ // was silently overruled on every Chinese page: picking a design language
714
+ // changed nothing about the type. The tail is already in fontSans, so the
715
+ // two stacks are the same stack.
716
+ ` --font-cn: ${fontSans};`,
717
+ ` --font-heading: ${fontDisplay};`,
718
+ ` --font-display: ${fontDisplay};`
719
+ ];
720
+ if (t.fontMono) typeLines.push(` --font-mono: ${cssFontStack(t.fontMono)};`);
721
+ if (t.headingWeight != null) {
722
+ typeLines.push(` --font-weight-heading: ${t.headingWeight};`);
723
+ }
724
+ const cats = b.extensions?.categories;
725
+ const decorative = b.extensions?.decorative;
726
+ const extLines = [];
727
+ if (cats) {
728
+ for (const [key, value] of Object.entries(cats)) {
729
+ const safe = key.replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
730
+ extLines.push(` --brand-category-${safe}: ${value};`);
731
+ }
732
+ }
733
+ if (decorative) {
734
+ for (const [key, value] of Object.entries(decorative)) {
735
+ const safe = key.replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
736
+ extLines.push(` --brand-decorative-${safe}: ${value};`);
737
+ }
738
+ }
739
+ const sharedChrome = [
740
+ ``,
741
+ ` /* Recipe (action language + free elev/radii) */`,
742
+ ...recipeVars(b.recipe),
743
+ ``,
744
+ ` /* Typography */`,
745
+ ...typeLines,
746
+ ...motionVars(b.motion),
747
+ ...spacingVars(b.spacing),
748
+ ...extLines.length ? ["", " /* Taxonomy / decorative (not product CTA) */", ...extLines] : []
749
+ ];
750
+ if (dual && b.modes?.light?.semantic && b.modes?.dark?.semantic) {
751
+ parts.push(emitLightModeSelector(b.id, mode, root));
752
+ parts.push(
753
+ ...emitColorVars(b.modes.light.semantic, b.modes.light.roles),
754
+ ...sharedChrome,
755
+ `}`,
756
+ ``
757
+ );
758
+ parts.push(emitDarkModeSelector(b.id, mode, root));
759
+ parts.push(...emitColorVars(b.modes.dark.semantic, b.modes.dark.roles), `}`, ``);
760
+ } else {
761
+ const selector = mode === "scoped" ? `${root}[data-brand="${b.id}"] {` : emitGlobalSkinSelector(b.id, b.darkDefault, root);
762
+ parts.push(selector);
763
+ parts.push(...emitColorVars(b.semantic, b.roles), ...sharedChrome, `}`, ``);
764
+ }
765
+ return parts.join("\n");
766
+ }
767
+
768
+ // src/brand-package/apply-brand.ts
769
+ var BRAND_STYLE_ELEMENT_ID = "nebutra-brand-skin";
770
+ var BRAND_STORAGE_KEY = "nebutra-brand-package";
771
+ function targetDoc(doc) {
772
+ if (doc) return doc;
773
+ if (typeof document === "undefined") return null;
774
+ return document;
775
+ }
776
+ function applyBrandCss(css, brandId, options = {}) {
777
+ const d = targetDoc(options.doc);
778
+ if (!d) return;
779
+ let el = d.getElementById(BRAND_STYLE_ELEMENT_ID);
780
+ if (!el) {
781
+ el = d.createElement("style");
782
+ el.id = BRAND_STYLE_ELEMENT_ID;
783
+ el.setAttribute("data-nebutra-brand", brandId ?? "custom");
784
+ d.head.appendChild(el);
785
+ }
786
+ el.textContent = css;
787
+ if (brandId) {
788
+ d.documentElement.dataset.brand = brandId;
789
+ }
790
+ }
791
+ function applyBrandPackage(brand, options = {}) {
792
+ const css = emitBrandCss(brand);
793
+ applyBrandCss(css, brand.id, options);
794
+ if (options.persist && typeof localStorage !== "undefined") {
795
+ try {
796
+ localStorage.setItem(BRAND_STORAGE_KEY, JSON.stringify(brand));
797
+ } catch {
798
+ }
799
+ }
800
+ }
801
+ function clearBrand(options = {}) {
802
+ const d = targetDoc(options.doc);
803
+ if (!d) return;
804
+ d.getElementById(BRAND_STYLE_ELEMENT_ID)?.remove();
805
+ delete d.documentElement.dataset.brand;
806
+ if (options.persist && typeof localStorage !== "undefined") {
807
+ try {
808
+ localStorage.removeItem(BRAND_STORAGE_KEY);
809
+ } catch {
810
+ }
811
+ }
812
+ }
813
+ function restorePersistedBrand(options = {}) {
814
+ if (typeof localStorage === "undefined") return null;
815
+ try {
816
+ const raw = localStorage.getItem(BRAND_STORAGE_KEY);
817
+ if (!raw) return null;
818
+ const brand = JSON.parse(raw);
819
+ if (!brand?.id || !brand?.semantic || !brand?.recipe) return null;
820
+ applyBrandPackage(brand, { ...options, persist: false });
821
+ return brand;
822
+ } catch {
823
+ return null;
824
+ }
825
+ }
826
+ function getActiveBrandId(doc) {
827
+ const d = targetDoc(doc);
828
+ return d?.documentElement.dataset.brand ?? null;
829
+ }
830
+
831
+ // src/brand-package/compile-helpers.ts
832
+ function leafHex(tree, path) {
833
+ let cur = tree;
834
+ for (const p of path) {
835
+ if (!cur || typeof cur !== "object") return void 0;
836
+ cur = cur[p];
837
+ }
838
+ if (!cur || typeof cur !== "object") return void 0;
839
+ const v = cur.$value ?? cur.value;
840
+ return typeof v === "string" ? v : void 0;
841
+ }
842
+ function detectPreset(idHint, colors) {
843
+ const id = idHint.toLowerCase();
844
+ if (id.includes("linear")) return "linear";
845
+ if (id.includes("gsap")) return "gsap";
846
+ if (id.includes("raycast")) return "raycast";
847
+ if (id.includes("vercel")) return "vercel";
848
+ if (id.includes("vanta")) return "vanta";
849
+ if (id.includes("stripe")) return "stripe";
850
+ if (id.includes("notion")) return "notion";
851
+ if (colors["paper-white"] && colors.obsidian && colors.hairline) return "vercel";
852
+ if (colors["coral-pulse"] || colors["void-black"] && colors.mist && colors.ink) {
853
+ return "raycast";
854
+ }
855
+ if (colors["acid-lime"] || colors.void) return "linear";
856
+ if (colors["shockingly-green"] || colors["surface-cream"] || colors["just-black"]) return "gsap";
857
+ if (colors["just-black"] && colors["surface-cream"]) return "gsap";
858
+ if ((colors["notion-blue"] || colors["paper-warmth"]) && (colors["paper-warmth"] || colors["ink-black"]) && (colors["sky-tint"] || colors.marigold || colors.coral)) {
859
+ return "notion";
860
+ }
861
+ if (colors["indigo-ink"] && colors["pure-white"] && (colors.frost || colors["lavender-border"] || colors["midnight-ink"]) && !colors["paper-warmth"]) {
862
+ return "stripe";
863
+ }
864
+ if ((colors["indigo-ink"] || colors["vivid-violet"]) && (colors.parchment || colors["lavender-wash"] || colors.paper) && !colors["pure-white"]) {
865
+ return "vanta";
866
+ }
867
+ return "generic";
868
+ }
869
+ function pickUiFontFamily(font) {
870
+ const preferUi = /(inter|geist|manrope|dm sans|sans|ui)/i;
871
+ const avoidDisplay = /(reckless|serif|display|editorial|playfair|lora|source serif)/i;
872
+ const entries = Object.entries(font);
873
+ for (const [k, v] of entries) {
874
+ if (!v || typeof v !== "object") continue;
875
+ const name = String(v.$value ?? v.value ?? "");
876
+ if (!name || avoidDisplay.test(k) || avoidDisplay.test(name)) continue;
877
+ if (preferUi.test(k) || preferUi.test(name) || entries.length === 1) return name;
878
+ }
879
+ for (const [k, v] of entries) {
880
+ if (!v || typeof v !== "object") continue;
881
+ const name = String(v.$value ?? v.value ?? "");
882
+ if (name && !avoidDisplay.test(k) && !avoidDisplay.test(name)) return name;
883
+ }
884
+ return void 0;
885
+ }
886
+ function pickDisplayFontFamily(font) {
887
+ const prefer = /(reckless|serif|display|editorial|playfair|lora|source serif|mori)/i;
888
+ for (const [k, v] of Object.entries(font)) {
889
+ if (!v || typeof v !== "object") continue;
890
+ const name = String(v.$value ?? v.value ?? "");
891
+ if (name && (prefer.test(k) || prefer.test(name))) return name;
892
+ }
893
+ return void 0;
894
+ }
895
+ var COLOR_KEY_ALIASES = {
896
+ background: ["background", "canvas", "page-canvas"],
897
+ foreground: ["foreground", "ink", "ink-black"],
898
+ card: ["card", "card-surface", "surface"],
899
+ "card-foreground": ["card-foreground"],
900
+ cardForeground: ["card-foreground"],
901
+ primary: ["primary", "action"],
902
+ "primary-foreground": ["primary-foreground"],
903
+ primaryForeground: ["primary-foreground"],
904
+ secondary: ["secondary", "quiet"],
905
+ "secondary-foreground": ["secondary-foreground"],
906
+ secondaryForeground: ["secondary-foreground"],
907
+ muted: ["muted"],
908
+ "muted-foreground": ["muted-foreground", "mutedForeground", "stone"],
909
+ mutedForeground: ["muted-foreground", "stone"],
910
+ accent: ["accent"],
911
+ "accent-foreground": ["accent-foreground"],
912
+ accentForeground: ["accent-foreground"],
913
+ border: ["border", "hairline"],
914
+ input: ["input"],
915
+ ring: ["ring"],
916
+ destructive: ["destructive"],
917
+ "destructive-foreground": ["destructive-foreground"],
918
+ destructiveForeground: ["destructive-foreground"],
919
+ success: ["success"],
920
+ popover: ["popover"],
921
+ "popover-foreground": ["popover-foreground"],
922
+ popoverForeground: ["popover-foreground"]
923
+ };
924
+ function isUsableColorValue(val) {
925
+ const t = val.trim();
926
+ if (!t) return false;
927
+ if (t.startsWith("#")) return true;
928
+ if (/^\d+(\.\d+)?\s+\d+(\.\d+)?%\s+\d+(\.\d+)?%$/.test(t)) return true;
929
+ if (/^hsla?\(/i.test(t) || /^rgba?\(/i.test(t)) return true;
930
+ return false;
931
+ }
932
+ function collectColors(colorRoot) {
933
+ const raw = {};
934
+ if (!colorRoot || typeof colorRoot !== "object") return raw;
935
+ for (const [k, v] of Object.entries(colorRoot)) {
936
+ if (!v || typeof v !== "object") continue;
937
+ const val = v.$value ?? v.value;
938
+ if (typeof val === "string" && isUsableColorValue(val)) raw[k] = val.trim();
939
+ }
940
+ const out = { ...raw };
941
+ for (const [key, value] of Object.entries(raw)) {
942
+ const aliases = COLOR_KEY_ALIASES[key];
943
+ if (!aliases) continue;
944
+ for (const alias of aliases) {
945
+ if (!out[alias]) out[alias] = value;
946
+ }
947
+ }
948
+ return out;
949
+ }
950
+ function collectSurfaces(surfaceRoot) {
951
+ return collectColors(surfaceRoot);
952
+ }
953
+
954
+ // src/brand-package/infer-recipe.ts
955
+ function inferRecipeFromDesignMd(designMd) {
956
+ const t = designMd.toLowerCase();
957
+ const notes = [];
958
+ const hints = { notes };
959
+ const outlineFirst = t.includes("outlined-only") || t.includes("outline-only") || t.includes("ghost pill") || t.includes("ghost-pill") || t.includes("no filled") || t.includes("don't add filled") || t.includes("do not add filled") || t.includes("never fill") || t.includes("outlined") && t.includes("button") && !t.includes("filled cta");
960
+ const gradientStroke = t.includes("gradient-stroked") || t.includes("gradient stroke") || t.includes("gradient-stroked cta") || t.includes("border-image") || t.includes("gradient border") && (t.includes("cta") || t.includes("button"));
961
+ const neutralFilled = t.includes("no chromatic") || t.includes("deliberately neutral") || t.includes("neutral rather than chromatic") || t.includes("no chromatic action") || t.includes("don't use chromatic action") || t.includes("do not use chromatic action") || t.includes("mist") && t.includes("filled") && (t.includes("iron") || t.includes("neutral"));
962
+ const solidCta = neutralFilled || t.includes("filled") && (t.includes("primary") || t.includes("cta") || t.includes("download") || t.includes("action button") || t.includes("primary action") || t.includes("filled button"));
963
+ if (gradientStroke) {
964
+ hints.buttonDefault = "gradient-stroke";
965
+ notes.push("DESIGN.md: gradient-stroke CTA");
966
+ } else if (outlineFirst && !solidCta) {
967
+ hints.buttonDefault = "outline";
968
+ notes.push("DESIGN.md: outline-first controls");
969
+ } else if (solidCta) {
970
+ hints.buttonDefault = "solid";
971
+ if (neutralFilled) notes.push("DESIGN.md: neutral filled CTA (not chromatic)");
972
+ }
973
+ const forbidsAnyShadow = t.includes("avoids shadows") || t.includes("avoid shadows") || t.includes("avoids shadows entirely") || t.includes("no card has a box-shadow") || t.includes("no button has a shadow") || t.includes("never from box-shadow") || t.includes("never from elevation") || t.includes("depth comes from background tint") || t.includes("depth comes from background") || t.includes("no shadows, blurs") || t.includes("do not use shadows") || t.includes("don't use shadows") || t.includes("rejects drop shadows") || t.includes("reject drop shadows") || t.includes("don't drop shadows") || t.includes("do not drop shadows") || t.includes("don't apply drop shadows") || t.includes("do not apply drop shadows") || t.includes("don't add drop-shadows") || t.includes("do not add drop-shadows") || t.includes("never use drop-shadows") || t.includes("no drop shadow") || t.includes("no box-shadow") || t.includes("never via box-shadow") || t.includes("border is the elevation") || t.includes("no shadow \u2014 the border") || t.includes("no shadow - the border") || t.includes("depth is communicated only") || t.includes("no shadow") && t.includes("border") || t.includes("flat") && t.includes("1px") && t.includes("border");
974
+ const keyElev = t.includes("keyboard key") || t.includes("key shadow") || t.includes("key cap") || t.includes("inset top") && t.includes("highlight") && t.includes("shadow");
975
+ const hairlineRingElev = !forbidsAnyShadow && (t.includes("stacked box-shadow") || t.includes("double-ring") || t.includes("build depth with hairline") || t.includes("hairline") && t.includes("box-shadow") && (t.includes("never with drop-shadow") || t.includes("never use drop-shadow") || t.includes("not with drop-shadow")));
976
+ if (keyElev) {
977
+ hints.elevationPreset = "key";
978
+ notes.push("DESIGN.md: key/inset elevation");
979
+ } else if (forbidsAnyShadow) {
980
+ hints.elevationPreset = "none";
981
+ notes.push("DESIGN.md: elevation=none (no box-shadow / tint+border depth)");
982
+ } else if (hairlineRingElev) {
983
+ hints.elevationPreset = "hairline";
984
+ notes.push("DESIGN.md: hairline ring elevation");
985
+ }
986
+ if (/\*\*density:\*\*\s*comfortable|density:\s*comfortable|\bdensity\b[^\n]{0,20}comfortable/i.test(
987
+ designMd
988
+ )) {
989
+ hints.density = "comfortable";
990
+ } else if (/\*\*density:\*\*\s*spacious|density:\s*spacious/i.test(designMd)) {
991
+ hints.density = "spacious";
992
+ } else if (/\*\*density:\*\*\s*compact|density:\s*compact/i.test(designMd)) {
993
+ hints.density = "compact";
994
+ } else if ((t.includes("compact density") || t.includes("8\u201312px") || t.includes("8-12px")) && !t.includes("comfortable")) {
995
+ hints.density = "compact";
996
+ } else if (t.includes("comfortable") || t.includes("spacious")) {
997
+ hints.density = t.includes("spacious") ? "spacious" : "comfortable";
998
+ }
999
+ const tableButton = designMd.match(/\|\s*buttons\s*\|\s*(\d+px|9999?px)\s*\|/i);
1000
+ const tableCards = designMd.match(/\|\s*cards\s*\|\s*(\d+px|9999?px)\s*\|/i);
1001
+ const tableButtonRadius = tableButton?.[1];
1002
+ const tableCardsRadius = tableCards?.[1];
1003
+ const radii = {};
1004
+ if (tableButtonRadius) {
1005
+ radii.button = tableButtonRadius;
1006
+ notes.push(`DESIGN.md: table buttons radius ${tableButtonRadius}`);
1007
+ } else if (t.includes("4px border-radius on all buttons") || t.includes("use 4px border-radius on all") || t.includes("never pill") && t.includes("4px") && t.includes("button")) {
1008
+ radii.button = "4px";
1009
+ notes.push("DESIGN.md: 4px control radius (not pill)");
1010
+ } else if ((t.includes("999px") || t.includes("9999px")) && (t.includes("button") || t.includes("pill-shaped")) && !t.includes("never pill") && !t.includes("not pill") && !t.includes("pills only") || t.includes("pill-shaped") && t.includes("button")) {
1011
+ radii.button = t.includes("9999px") ? "9999px" : "999px";
1012
+ notes.push("DESIGN.md: full pill control radius");
1013
+ } else if (/\bbuttons?\b[^\n.|]{0,48}\b8px\b/.test(t) || t.includes("8px for buttons")) {
1014
+ radii.button = "8px";
1015
+ } else if (/\bbuttons?\b[^\n.|]{0,48}\b6px\b/.test(t) || t.includes("button radius to 6px")) {
1016
+ radii.button = "6px";
1017
+ } else if (t.includes("100px") && (t.includes("button") || t.includes("pill button"))) {
1018
+ radii.button = "100px";
1019
+ }
1020
+ if (tableCardsRadius) {
1021
+ radii.card = tableCardsRadius;
1022
+ notes.push(`DESIGN.md: table cards radius ${tableCardsRadius}`);
1023
+ }
1024
+ if (radii.button != null || radii.card != null) {
1025
+ hints.radii = radii;
1026
+ }
1027
+ return hints;
1028
+ }
1029
+
1030
+ // src/brand-package/presets/recipe.ts
1031
+ function buildRecipe(input) {
1032
+ const button = input.radii?.button ?? "0.375rem";
1033
+ const card = input.radii?.card ?? "0.75rem";
1034
+ const badge = input.radii?.badge ?? "9999px";
1035
+ const radii = {
1036
+ button,
1037
+ card,
1038
+ badge,
1039
+ input: input.radii?.input ?? button,
1040
+ pill: input.radii?.pill ?? "9999px"
1041
+ };
1042
+ const elevationPreset = input.elevationPreset ?? "soft";
1043
+ const elevationTokens = input.elevationTokens ?? elevationPresetToTokens(elevationPreset, input.cardShadow);
1044
+ const recipe = {
1045
+ buttonDefault: input.buttonDefault,
1046
+ density: input.density ?? "comfortable",
1047
+ radii,
1048
+ elevationTokens
1049
+ };
1050
+ if (input.badgeDefault) recipe.badgeDefault = input.badgeDefault;
1051
+ if (input.primaryStrokeGradient) recipe.primaryStrokeGradient = input.primaryStrokeGradient;
1052
+ if (input.outlineBorder) recipe.outlineBorder = input.outlineBorder;
1053
+ return recipe;
1054
+ }
1055
+
1056
+ // src/brand-package/presets/generic.ts
1057
+ function buildGeneric(ctx) {
1058
+ ctx.warnings.push(
1059
+ "Unknown brand layout \u2014 compiled with heuristic recipe. Review mapping in Create Center."
1060
+ );
1061
+ ctx.warnings.push(...ctx.recipeHints.notes);
1062
+ const entries = Object.entries(ctx.colors);
1063
+ const pick = (...keys) => {
1064
+ for (const k of keys) {
1065
+ if (ctx.colors[k]) return ctx.colors[k];
1066
+ }
1067
+ return void 0;
1068
+ };
1069
+ const bg = pick(
1070
+ "paper-warmth",
1071
+ "paper-white",
1072
+ "page-canvas",
1073
+ "parchment",
1074
+ "pure-white",
1075
+ "paper",
1076
+ "background",
1077
+ "canvas",
1078
+ "void-black",
1079
+ "void",
1080
+ "just-black",
1081
+ "off-black"
1082
+ ) ?? "#0a0a0a";
1083
+ const isLightCanvas = (() => {
1084
+ try {
1085
+ const m = tryHexToHsl(bg, "0 0% 4%").match(/(\d+)%\s*$/);
1086
+ return m ? Number(m[1]) >= 50 : false;
1087
+ } catch {
1088
+ return false;
1089
+ }
1090
+ })();
1091
+ const fg = isLightCanvas ? pick(
1092
+ "ink-black",
1093
+ "carbon",
1094
+ "charcoal",
1095
+ "obsidian",
1096
+ "foreground",
1097
+ "ink",
1098
+ "midnight-ink",
1099
+ "deep-violet",
1100
+ "slate"
1101
+ ) ?? "#181822" : pick("pure-white", "paper", "surface-cream", "bone", "mist", "foreground", "white") ?? "#ffffff";
1102
+ const primary = pick(
1103
+ "notion-blue",
1104
+ "indigo-ink",
1105
+ "vivid-violet",
1106
+ "primary",
1107
+ "acid-lime",
1108
+ "obsidian",
1109
+ "shockingly-green",
1110
+ "brand",
1111
+ "accent",
1112
+ "mid-violet",
1113
+ "amethyst-edge",
1114
+ "signal-blue"
1115
+ ) ?? entries.find(
1116
+ ([k]) => !/void|black|canvas|graphite|paper|hairline|ash|parchment|fog|lavender|steel|slate|carbon|mist|frost|smoke|white|midnight|warmth|tint|wash|marigold|coral|saffron|mocha|vermillion|sky/i.test(
1117
+ k
1118
+ )
1119
+ )?.[1] ?? (isLightCanvas ? "#171717" : "#3b82f6");
1120
+ const brandMarkHex = pick("ink-black", "coral-pulse", "brand-mark", "logo", "wordmark", "charcoal", "deep-violet") ?? void 0;
1121
+ const border = isLightCanvas ? pick("frost", "hairline", "border", "ash", "carbon", "lilac-border", "slate") ?? "#e5e5e5" : pick("hairline", "border", "slate", "graphite", "surface-25", "smoke") ?? "#333333";
1122
+ const mutedFg = pick(
1123
+ "stone",
1124
+ "charcoal",
1125
+ "graphite",
1126
+ "steel",
1127
+ "slate",
1128
+ "muted",
1129
+ "smoke",
1130
+ "ash",
1131
+ "fog",
1132
+ "surface-50"
1133
+ ) ?? "#888888";
1134
+ const card = isLightCanvas ? pick("pure-white", "paper", "card-surface", "card") ?? "#ffffff" : pick("ink", "carbon", "off-black", "obsidian", "card") ?? bg;
1135
+ const quiet = isLightCanvas ? pick("sky-tint", "periwinkle-wash", "lavender-wash", "mist", "fog", "ash", "secondary") ?? border : pick("graphite", "obsidian", "smoke", "secondary") ?? border;
1136
+ const uiFont = pickUiFontFamily(ctx.font) ?? "Inter";
1137
+ const displayFont = pickDisplayFontFamily(ctx.font);
1138
+ let elevationPreset = ctx.recipeHints.elevationPreset ?? "soft";
1139
+ if (elevationPreset === "key" && isLightCanvas) elevationPreset = "hairline";
1140
+ const brand = {
1141
+ id: ctx.id,
1142
+ name: ctx.siteName,
1143
+ darkDefault: !isLightCanvas,
1144
+ version: "0.1.0",
1145
+ semantic: isLightCanvas ? {
1146
+ background: tryHexToHsl(bg, "0 0% 98%"),
1147
+ foreground: tryHexToHsl(fg, "0 0% 9%"),
1148
+ card: tryHexToHsl(card, "0 0% 100%"),
1149
+ cardForeground: tryHexToHsl(fg, "0 0% 9%"),
1150
+ popover: tryHexToHsl(card, "0 0% 100%"),
1151
+ popoverForeground: tryHexToHsl(fg, "0 0% 9%"),
1152
+ primary: tryHexToHsl(primary, "0 0% 9%"),
1153
+ primaryForeground: tryHexToHsl(card, "0 0% 100%"),
1154
+ secondary: tryHexToHsl(quiet, "0 0% 92%"),
1155
+ secondaryForeground: tryHexToHsl(fg, "0 0% 9%"),
1156
+ muted: tryHexToHsl(quiet, "0 0% 92%"),
1157
+ mutedForeground: tryHexToHsl(mutedFg, "0 0% 40%"),
1158
+ accent: tryHexToHsl(quiet, "0 0% 92%"),
1159
+ accentForeground: tryHexToHsl(fg, "0 0% 9%"),
1160
+ destructive: "0 72% 51%",
1161
+ destructiveForeground: "0 0% 100%",
1162
+ border: tryHexToHsl(border, "0 0% 20%"),
1163
+ input: tryHexToHsl(card, "0 0% 100%"),
1164
+ ring: tryHexToHsl(primary, "0 0% 9%")
1165
+ } : {
1166
+ background: tryHexToHsl(bg, "0 0% 4%"),
1167
+ foreground: tryHexToHsl(fg, "0 0% 98%"),
1168
+ card: tryHexToHsl(card, "0 0% 8%"),
1169
+ cardForeground: tryHexToHsl(fg, "0 0% 98%"),
1170
+ popover: tryHexToHsl(card, "0 0% 8%"),
1171
+ popoverForeground: tryHexToHsl(fg, "0 0% 98%"),
1172
+ primary: tryHexToHsl(primary, "217 91% 60%"),
1173
+ primaryForeground: tryHexToHsl(bg, "0 0% 4%"),
1174
+ secondary: tryHexToHsl(border, "0 0% 20%"),
1175
+ secondaryForeground: tryHexToHsl(fg, "0 0% 98%"),
1176
+ muted: tryHexToHsl(card, "0 0% 8%"),
1177
+ mutedForeground: tryHexToHsl(mutedFg, "0 0% 53%"),
1178
+ accent: tryHexToHsl(border, "0 0% 20%"),
1179
+ accentForeground: tryHexToHsl(primary, "217 91% 60%"),
1180
+ destructive: "0 72% 51%",
1181
+ destructiveForeground: "0 0% 100%",
1182
+ border: tryHexToHsl(border, "0 0% 20%"),
1183
+ input: tryHexToHsl(border, "0 0% 20%"),
1184
+ ring: tryHexToHsl(primary, "217 91% 60%")
1185
+ },
1186
+ recipe: buildRecipe({
1187
+ buttonDefault: ctx.recipeHints.buttonDefault ?? "solid",
1188
+ radii: {
1189
+ button: ctx.recipeHints.radii?.button ?? leafHex(ctx.radius, ["buttons"]) ?? leafHex(ctx.radius, ["md"]) ?? "0.375rem",
1190
+ card: ctx.recipeHints.radii?.card ?? leafHex(ctx.radius, ["cards"]) ?? leafHex(ctx.radius, ["xl"]) ?? leafHex(ctx.radius, ["lg"]) ?? "0.75rem",
1191
+ badge: leafHex(ctx.radius, ["pills"]) ?? leafHex(ctx.radius, ["badges"]) ?? "9999px",
1192
+ input: leafHex(ctx.radius, ["inputs"]) ?? ctx.recipeHints.radii?.button ?? "0.375rem"
1193
+ },
1194
+ elevationPreset,
1195
+ density: ctx.recipeHints.density ?? "comfortable",
1196
+ outlineBorder: isLightCanvas ? border : fg,
1197
+ badgeDefault: brandMarkHex ? "muted" : "match-action"
1198
+ }),
1199
+ typography: {
1200
+ fontSans: `'${uiFont}', ui-sans-serif, system-ui, sans-serif`,
1201
+ ...displayFont ? { fontDisplay: `'${displayFont}', ui-serif, Georgia, serif` } : {},
1202
+ headingWeight: isLightCanvas ? 500 : 600
1203
+ },
1204
+ extensions: {
1205
+ ...typeof ctx.refero.url === "string" ? { sourceUrl: ctx.refero.url } : {},
1206
+ ...brandMarkHex ? { categories: { brand: brandMarkHex } } : {},
1207
+ notes: [
1208
+ "Generic compile \u2014 verify primary + buttonDefault in Create Center.",
1209
+ ...brandMarkHex ? ["Detected separate brand-mark color (categories.brand \u2192 --brand-mark)."] : []
1210
+ ]
1211
+ }
1212
+ };
1213
+ return brand;
1214
+ }
1215
+
1216
+ // src/brand-package/presets/gsap.ts
1217
+ function buildGsap(ctx) {
1218
+ const canvas = ctx.colors["just-black"] ?? ctx.colors.canvas ?? "#0e100f";
1219
+ const cream = ctx.colors["surface-cream"] ?? ctx.colors["cream-surface"] ?? "#fffce1";
1220
+ const muted = ctx.colors["surface-50"] ?? "#7c7c6f";
1221
+ const hairline = ctx.colors["surface-25"] ?? "#42433d";
1222
+ const nested = ctx.colors["off-black"] ?? ctx.colors["nested-panel"] ?? "#191919";
1223
+ const green = ctx.colors["shockingly-green"] ?? "#0ae448";
1224
+ ctx.warnings.push(
1225
+ "GSAP: shockingly-green is accent/link only \u2014 buttonDefault=outline (no solid green fill)."
1226
+ );
1227
+ const buttonDefault = ctx.recipeHints.buttonDefault ?? "gradient-stroke";
1228
+ const brand = {
1229
+ id: "gsap",
1230
+ name: "GSAP",
1231
+ darkDefault: true,
1232
+ version: "1.0.0",
1233
+ semantic: {
1234
+ // Primary for *links/accents* — filled solid CTAs are disabled by recipe
1235
+ background: tryHexToHsl(canvas, "150 8% 6%"),
1236
+ foreground: tryHexToHsl(cream, "54 100% 94%"),
1237
+ card: tryHexToHsl(nested, "0 0% 10%"),
1238
+ cardForeground: tryHexToHsl(cream, "54 100% 94%"),
1239
+ popover: tryHexToHsl(nested, "0 0% 10%"),
1240
+ popoverForeground: tryHexToHsl(cream, "54 100% 94%"),
1241
+ primary: tryHexToHsl(green, "136 91% 47%"),
1242
+ primaryForeground: tryHexToHsl(canvas, "150 8% 6%"),
1243
+ secondary: tryHexToHsl(hairline, "60 5% 25%"),
1244
+ secondaryForeground: tryHexToHsl(cream, "54 100% 94%"),
1245
+ muted: tryHexToHsl(nested, "0 0% 10%"),
1246
+ mutedForeground: tryHexToHsl(muted, "60 6% 46%"),
1247
+ accent: tryHexToHsl(hairline, "60 5% 25%"),
1248
+ accentForeground: tryHexToHsl(green, "136 91% 47%"),
1249
+ destructive: tryHexToHsl(ctx.colors["lipstick-pink"] ?? "#f100cb", "310 100% 47%"),
1250
+ destructiveForeground: tryHexToHsl(cream, "54 100% 94%"),
1251
+ border: tryHexToHsl(hairline, "60 5% 25%"),
1252
+ input: tryHexToHsl(hairline, "60 5% 25%"),
1253
+ ring: tryHexToHsl(green, "136 91% 47%"),
1254
+ info: tryHexToHsl(ctx.colors.blue ?? "#00bae2", "191 100% 44%"),
1255
+ infoForeground: tryHexToHsl(canvas, "150 8% 6%"),
1256
+ success: tryHexToHsl(green, "136 91% 47%"),
1257
+ successForeground: tryHexToHsl(canvas, "150 8% 6%")
1258
+ },
1259
+ recipe: buildRecipe({
1260
+ buttonDefault,
1261
+ radii: {
1262
+ button: ctx.recipeHints.radii?.button ?? leafHex(ctx.radius, ["full"]) ?? "100px",
1263
+ card: leafHex(ctx.radius, ["lg"]) ?? "8px"
1264
+ },
1265
+ elevationPreset: ctx.recipeHints.elevationPreset ?? "none",
1266
+ density: ctx.recipeHints.density ?? "comfortable",
1267
+ outlineBorder: cream,
1268
+ primaryStrokeGradient: "linear-gradient(114.41deg, #0ae448 20.74%, #abff84 65.5%)"
1269
+ }),
1270
+ // Performs. This is the one language where motion is the message, so the
1271
+ // ramp is long enough to be watched and the spring is allowed to overshoot
1272
+ // well past its resting state.
1273
+ motion: {
1274
+ easeOut: "cubic-bezier(0.22, 1, 0.36, 1)",
1275
+ easeInOut: "cubic-bezier(0.65, 0, 0.35, 1)",
1276
+ easeSpring: "cubic-bezier(0.68, -0.55, 0.265, 1.55)",
1277
+ micro: 120,
1278
+ flow: 250,
1279
+ reveal: 400,
1280
+ cinematic: 700
1281
+ },
1282
+ // Room for the performance to land — more air than the product-chrome trio.
1283
+ spacing: {
1284
+ xs: "0.5rem",
1285
+ sm: "0.875rem",
1286
+ md: "1.25rem",
1287
+ lg: "1.75rem",
1288
+ xl: "2.5rem",
1289
+ "2xl": "3.5rem"
1290
+ },
1291
+ typography: {
1292
+ fontSans: `'Mori', 'Inter Tight', 'DM Sans', ui-sans-serif, system-ui, sans-serif`,
1293
+ fontDisplay: `'Mori', 'Inter Tight', ui-sans-serif, system-ui, sans-serif`,
1294
+ headingWeight: 600
1295
+ },
1296
+ extensions: {
1297
+ categories: {
1298
+ gsap: green,
1299
+ scroll: ctx.colors.pink ?? "#fec5fb",
1300
+ svg: ctx.colors.orangey ?? "#ff8709",
1301
+ text: ctx.colors.lilac ?? "#9d95ff",
1302
+ ui: ctx.colors.blue ?? "#00bae2",
1303
+ other: ctx.colors["light-green"] ?? "#abff84"
1304
+ },
1305
+ displaySizePx: 224,
1306
+ sourceUrl: typeof ctx.refero.url === "string" ? ctx.refero.url : "https://gsap.com",
1307
+ notes: [
1308
+ "Outline-first product controls; category ctx.colors are marketing extensions.",
1309
+ "Replace typography.faces[].src with Create Center hosted ctx.font URLs."
1310
+ ]
1311
+ }
1312
+ };
1313
+ return brand;
1314
+ }
1315
+
1316
+ // src/brand-package/presets/linear.ts
1317
+ function linearDarkSemantic(voidC, carbon, graphite, ash, paper, lime, coral, pulse, colors) {
1318
+ return {
1319
+ background: tryHexToHsl(voidC, "210 11% 4%"),
1320
+ foreground: tryHexToHsl(paper, "0 0% 100%"),
1321
+ card: tryHexToHsl(carbon, "210 6% 6%"),
1322
+ cardForeground: tryHexToHsl(paper, "0 0% 100%"),
1323
+ popover: tryHexToHsl(colors.obsidian ?? "#161718", "210 5% 9%"),
1324
+ popoverForeground: tryHexToHsl(paper, "0 0% 100%"),
1325
+ primary: tryHexToHsl(lime, "66 89% 54%"),
1326
+ primaryForeground: tryHexToHsl(voidC, "210 11% 4%"),
1327
+ secondary: tryHexToHsl(graphite, "220 7% 15%"),
1328
+ secondaryForeground: tryHexToHsl(colors.mist ?? "#d0d6e0", "220 20% 85%"),
1329
+ muted: tryHexToHsl(colors.obsidian ?? "#161718", "210 5% 9%"),
1330
+ mutedForeground: tryHexToHsl(ash, "220 5% 41%"),
1331
+ accent: tryHexToHsl(graphite, "220 7% 15%"),
1332
+ accentForeground: tryHexToHsl(lime, "66 89% 54%"),
1333
+ destructive: tryHexToHsl(coral, "0 79% 63%"),
1334
+ destructiveForeground: tryHexToHsl(paper, "0 0% 100%"),
1335
+ border: tryHexToHsl(graphite, "220 7% 15%"),
1336
+ input: tryHexToHsl(graphite, "220 7% 15%"),
1337
+ ring: tryHexToHsl(lime, "66 89% 54%"),
1338
+ success: tryHexToHsl(pulse, "136 61% 40%"),
1339
+ successForeground: tryHexToHsl(paper, "0 0% 100%"),
1340
+ info: tryHexToHsl(colors["signal-teal"] ?? "#02b8cc", "187 98% 40%"),
1341
+ infoForeground: tryHexToHsl(paper, "0 0% 100%")
1342
+ };
1343
+ }
1344
+ function linearLightSemantic(voidC, ash, paper, lime, coral, pulse, colors) {
1345
+ return {
1346
+ background: "0 0% 100%",
1347
+ foreground: tryHexToHsl(voidC, "210 11% 4%"),
1348
+ card: "0 0% 98%",
1349
+ cardForeground: tryHexToHsl(voidC, "210 11% 4%"),
1350
+ popover: "0 0% 100%",
1351
+ popoverForeground: tryHexToHsl(voidC, "210 11% 4%"),
1352
+ primary: tryHexToHsl(lime, "66 89% 54%"),
1353
+ primaryForeground: tryHexToHsl(voidC, "210 11% 4%"),
1354
+ secondary: "220 14% 96%",
1355
+ secondaryForeground: tryHexToHsl(voidC, "210 11% 4%"),
1356
+ muted: "220 14% 96%",
1357
+ mutedForeground: tryHexToHsl(ash, "220 5% 41%"),
1358
+ accent: "220 14% 96%",
1359
+ accentForeground: tryHexToHsl(voidC, "210 11% 4%"),
1360
+ destructive: tryHexToHsl(coral, "0 79% 63%"),
1361
+ destructiveForeground: tryHexToHsl(paper, "0 0% 100%"),
1362
+ border: "220 13% 91%",
1363
+ input: "0 0% 100%",
1364
+ ring: tryHexToHsl(lime, "66 89% 54%"),
1365
+ success: tryHexToHsl(pulse, "136 61% 40%"),
1366
+ successForeground: tryHexToHsl(paper, "0 0% 100%"),
1367
+ info: tryHexToHsl(colors["signal-teal"] ?? "#02b8cc", "187 98% 40%"),
1368
+ infoForeground: tryHexToHsl(paper, "0 0% 100%")
1369
+ };
1370
+ }
1371
+ function buildLinear(ctx) {
1372
+ const voidC = ctx.colors.void ?? ctx.colors["just-black"] ?? "#08090a";
1373
+ const carbon = ctx.colors.carbon ?? "#0f1011";
1374
+ const graphite = ctx.colors.graphite ?? "#23252a";
1375
+ const ash = ctx.colors.ash ?? "#62666d";
1376
+ const paper = ctx.colors.paper ?? "#ffffff";
1377
+ const lime = ctx.colors["acid-lime"] ?? "#e4f222";
1378
+ const coral = ctx.colors["coral-red"] ?? "#eb5757";
1379
+ const pulse = ctx.colors["pulse-green"] ?? "#27a644";
1380
+ const dark = linearDarkSemantic(
1381
+ voidC,
1382
+ carbon,
1383
+ graphite,
1384
+ ash,
1385
+ paper,
1386
+ lime,
1387
+ coral,
1388
+ pulse,
1389
+ ctx.colors
1390
+ );
1391
+ const light = linearLightSemantic(voidC, ash, paper, lime, coral, pulse, ctx.colors);
1392
+ const brand = {
1393
+ id: "linear",
1394
+ name: "Linear",
1395
+ darkDefault: true,
1396
+ version: "1.0.0",
1397
+ semantic: dark,
1398
+ modes: {
1399
+ dark: { semantic: dark },
1400
+ light: { semantic: light }
1401
+ },
1402
+ recipe: buildRecipe({
1403
+ buttonDefault: "solid",
1404
+ radii: { button: "6px", card: "12px" },
1405
+ elevationPreset: "soft",
1406
+ density: "compact"
1407
+ }),
1408
+ // Resolves before the eye asks it to. Linear's interfaces answer on the
1409
+ // frame you click; the expo-out curve spends its whole budget decelerating,
1410
+ // which is what makes a 140ms move read as instant rather than abrupt. No
1411
+ // spring — nothing in this language overshoots.
1412
+ motion: {
1413
+ easeOut: "cubic-bezier(0.16, 1, 0.3, 1)",
1414
+ easeInOut: "cubic-bezier(0.4, 0, 0.2, 1)",
1415
+ micro: 80,
1416
+ flow: 140,
1417
+ reveal: 200,
1418
+ cinematic: 320
1419
+ },
1420
+ // Sits close to the content it chrome — the densest of the seven.
1421
+ spacing: {
1422
+ xs: "0.375rem",
1423
+ sm: "0.5rem",
1424
+ md: "0.75rem",
1425
+ lg: "1rem",
1426
+ xl: "1.5rem",
1427
+ "2xl": "2rem"
1428
+ },
1429
+ typography: {
1430
+ fontSans: `'Inter Variable', 'Inter', ui-sans-serif, system-ui, sans-serif`,
1431
+ fontMono: `'Berkeley Mono', 'JetBrains Mono', ui-monospace, monospace`,
1432
+ headingWeight: 510
1433
+ },
1434
+ extensions: {
1435
+ sourceUrl: typeof ctx.refero.url === "string" ? ctx.refero.url : "https://linear.app",
1436
+ notes: ["Dual-mode: dark void default + light paper; acid-lime solid CTA in both modes."]
1437
+ }
1438
+ };
1439
+ return brand;
1440
+ }
1441
+
1442
+ // src/brand-package/presets/notion.ts
1443
+ function buildNotion(ctx) {
1444
+ const paper = ctx.colors["paper-warmth"] ?? "#f6f5f4";
1445
+ const white = ctx.colors["pure-white"] ?? "#ffffff";
1446
+ const ink = ctx.colors["ink-black"] ?? "#000000";
1447
+ const charcoal = ctx.colors.charcoal ?? "#111111";
1448
+ const stone = ctx.colors.stone ?? "#757575";
1449
+ const graphite = ctx.colors.graphite ?? "#615d59";
1450
+ const blue = ctx.colors["notion-blue"] ?? "#0075de";
1451
+ const sky = ctx.colors["sky-tint"] ?? "#e6f3fe";
1452
+ const marigold = ctx.colors.marigold ?? "#ffb110";
1453
+ const coral = ctx.colors.coral ?? "#f64932";
1454
+ const midnight = ctx.colors["midnight-ink"] ?? "#02093a";
1455
+ const signal = ctx.colors["signal-blue"] ?? "#097fe8";
1456
+ ctx.warnings.push(
1457
+ "Notion: notion-blue is the only filled CTA; marigold/coral/midnight are decorative card washes (never default action)."
1458
+ );
1459
+ ctx.warnings.push(
1460
+ "Notion: content cards use 1px hairline + elev=none; soft shadows only for sticky nav / product mockups (raised slot)."
1461
+ );
1462
+ const brand = {
1463
+ id: "notion",
1464
+ name: "Notion",
1465
+ darkDefault: false,
1466
+ version: "1.0.0",
1467
+ roles: {
1468
+ canvas: tryHexToHsl(paper, "30 9% 96%"),
1469
+ canvasForeground: tryHexToHsl(ink, "0 0% 0%"),
1470
+ surface: tryHexToHsl(white, "0 0% 100%"),
1471
+ surfaceForeground: tryHexToHsl(ink, "0 0% 0%"),
1472
+ action: tryHexToHsl(blue, "208 100% 44%"),
1473
+ actionForeground: tryHexToHsl(white, "0 0% 100%"),
1474
+ // Logo / wordmark / ink hierarchy — not blue CTA
1475
+ brand: tryHexToHsl(ink, "0 0% 0%"),
1476
+ brandForeground: tryHexToHsl(white, "0 0% 100%"),
1477
+ // Ghost CTA wash
1478
+ quiet: tryHexToHsl(sky, "206 90% 95%"),
1479
+ quietForeground: tryHexToHsl(blue, "208 100% 44%"),
1480
+ muted: tryHexToHsl(sky, "206 90% 95%"),
1481
+ mutedForeground: tryHexToHsl(stone, "0 0% 46%"),
1482
+ // Approx hairline rgba(0,0,0,0.08) on warm paper
1483
+ border: "30 5% 88%",
1484
+ input: tryHexToHsl(white, "0 0% 100%"),
1485
+ ring: tryHexToHsl(blue, "208 100% 44%"),
1486
+ destructive: tryHexToHsl(coral, "6 91% 58%"),
1487
+ destructiveForeground: tryHexToHsl(white, "0 0% 100%"),
1488
+ warning: tryHexToHsl(marigold, "40 100% 53%"),
1489
+ warningForeground: tryHexToHsl(ink, "0 0% 0%"),
1490
+ info: tryHexToHsl(signal, "207 93% 47%"),
1491
+ infoForeground: tryHexToHsl(white, "0 0% 100%")
1492
+ },
1493
+ semantic: {
1494
+ background: tryHexToHsl(paper, "30 9% 96%"),
1495
+ foreground: tryHexToHsl(ink, "0 0% 0%"),
1496
+ card: tryHexToHsl(white, "0 0% 100%"),
1497
+ cardForeground: tryHexToHsl(ink, "0 0% 0%"),
1498
+ popover: tryHexToHsl(white, "0 0% 100%"),
1499
+ popoverForeground: tryHexToHsl(ink, "0 0% 0%"),
1500
+ primary: tryHexToHsl(blue, "208 100% 44%"),
1501
+ primaryForeground: tryHexToHsl(white, "0 0% 100%"),
1502
+ secondary: tryHexToHsl(sky, "206 90% 95%"),
1503
+ secondaryForeground: tryHexToHsl(blue, "208 100% 44%"),
1504
+ muted: tryHexToHsl(sky, "206 90% 95%"),
1505
+ mutedForeground: tryHexToHsl(stone, "0 0% 46%"),
1506
+ accent: tryHexToHsl(sky, "206 90% 95%"),
1507
+ accentForeground: tryHexToHsl(blue, "208 100% 44%"),
1508
+ destructive: tryHexToHsl(coral, "6 91% 58%"),
1509
+ destructiveForeground: tryHexToHsl(white, "0 0% 100%"),
1510
+ border: "30 5% 88%",
1511
+ input: tryHexToHsl(white, "0 0% 100%"),
1512
+ ring: tryHexToHsl(blue, "208 100% 44%"),
1513
+ warning: tryHexToHsl(marigold, "40 100% 53%"),
1514
+ warningForeground: tryHexToHsl(ink, "0 0% 0%"),
1515
+ info: tryHexToHsl(signal, "207 93% 47%"),
1516
+ infoForeground: tryHexToHsl(white, "0 0% 100%")
1517
+ },
1518
+ recipe: buildRecipe({
1519
+ buttonDefault: ctx.recipeHints.buttonDefault ?? "solid",
1520
+ radii: {
1521
+ button: ctx.recipeHints.radii?.button ?? leafHex(ctx.radius, ["buttons"]) ?? leafHex(ctx.radius, ["lg"]) ?? "8px",
1522
+ card: ctx.recipeHints.radii?.card ?? leafHex(ctx.radius, ["cards"]) ?? leafHex(ctx.radius, ["xl"]) ?? "12px",
1523
+ badge: leafHex(ctx.radius, ["pills"]) ?? leafHex(ctx.radius, ["full"]) ?? "9999px",
1524
+ input: leafHex(ctx.radius, ["buttons"]) ?? leafHex(ctx.radius, ["lg"]) ?? "8px"
1525
+ },
1526
+ elevationPreset: ctx.recipeHints.elevationPreset ?? "none",
1527
+ density: ctx.recipeHints.density ?? "comfortable",
1528
+ badgeDefault: "muted",
1529
+ outlineBorder: ink,
1530
+ // Sticky nav soft shadow lives in raised; cards stay flat
1531
+ elevationTokens: {
1532
+ card: "0 0 #0000",
1533
+ control: "0 0 #0000",
1534
+ raised: "0px 0.7px 1.462px 0px rgb(0 0 0 / 0.015), 0px 3px 9px 0px rgb(0 0 0 / 0.03)"
1535
+ }
1536
+ }),
1537
+ // Settles rather than snaps. A document surface is read, not operated, so
1538
+ // motion here is slow enough to follow with the eye and never competes with
1539
+ // the text it is moving.
1540
+ motion: {
1541
+ easeOut: "cubic-bezier(0.25, 0.46, 0.45, 0.94)",
1542
+ easeInOut: "cubic-bezier(0.4, 0, 0.2, 1)",
1543
+ micro: 120,
1544
+ flow: 200,
1545
+ reveal: 280,
1546
+ cinematic: 460
1547
+ },
1548
+ // Editorial — a document wants margin, not chrome density.
1549
+ spacing: {
1550
+ xs: "0.625rem",
1551
+ sm: "1rem",
1552
+ md: "1.5rem",
1553
+ lg: "2rem",
1554
+ xl: "2.75rem",
1555
+ "2xl": "3.75rem"
1556
+ },
1557
+ typography: {
1558
+ fontSans: `'NotionInter', 'Inter', ui-sans-serif, system-ui, sans-serif`,
1559
+ fontDisplay: `'NotionInter', 'Inter', ui-sans-serif, system-ui, sans-serif`,
1560
+ headingWeight: 700
1561
+ },
1562
+ extensions: {
1563
+ categories: {
1564
+ brand: ink,
1565
+ action: blue,
1566
+ ghost: sky,
1567
+ marigold,
1568
+ coral,
1569
+ midnight,
1570
+ charcoal,
1571
+ graphite
1572
+ },
1573
+ decorative: {
1574
+ marigold,
1575
+ coral,
1576
+ saffron: ctx.colors.saffron ?? "#e89d01",
1577
+ "sky-wash": ctx.colors["sky-wash"] ?? "#62aef0",
1578
+ midnight
1579
+ },
1580
+ sourceUrl: typeof ctx.refero.url === "string" ? ctx.refero.url : "https://www.notion.com",
1581
+ notes: [
1582
+ "roles.action = Notion Blue (only filled CTA). Accent hues are decorative card washes.",
1583
+ "roles.brand = Ink Black (logo / wordmark / text hierarchy via alpha).",
1584
+ "Canvas = Paper Warmth; cards = Pure White \u2014 never invert.",
1585
+ "Card elev=none + hairline border; sticky nav soft shadow \u2192 elevation raised slot.",
1586
+ "Buttons 8px, cards 12px, pills 9999px.",
1587
+ "Lyon Text is editorial accent only \u2014 not product chrome UI."
1588
+ ]
1589
+ }
1590
+ };
1591
+ return brand;
1592
+ }
1593
+
1594
+ // src/brand-package/presets/raycast.ts
1595
+ function buildRaycast(ctx) {
1596
+ const canvas = ctx.colors["void-black"] ?? ctx.colors.canvas ?? "#040506";
1597
+ const ink = ctx.colors.ink ?? ctx.colors.card ?? "#07080a";
1598
+ const obsidian = ctx.colors.obsidian ?? ctx.colors.recessed ?? "#111214";
1599
+ const graphite = ctx.colors.graphite ?? ctx.colors.badge ?? "#1b1c1e";
1600
+ const smoke = ctx.colors.smoke ?? "#6a6b6c";
1601
+ const ash = ctx.colors.ash ?? "#9c9c9d";
1602
+ const mist = ctx.colors.mist ?? "#e6e6e6";
1603
+ const iron = ctx.colors.iron ?? "#454647";
1604
+ const slate = ctx.colors.slate ?? "#2f3031";
1605
+ const paper = ctx.colors["pure-white"] ?? "#ffffff";
1606
+ const coral = ctx.colors["coral-pulse"] ?? "#ff6363";
1607
+ const success = ctx.colors["success-green"] ?? "#59d499";
1608
+ const info = ctx.colors["info-blue"] ?? "#56c2ff";
1609
+ ctx.warnings.push(
1610
+ "Raycast: coral-pulse is brand mark only \u2014 primary CTA is Mist/Iron neutral solid."
1611
+ );
1612
+ const brand = {
1613
+ id: "raycast",
1614
+ name: "Raycast",
1615
+ darkDefault: true,
1616
+ version: "1.0.0",
1617
+ semantic: {
1618
+ background: tryHexToHsl(canvas, "210 20% 2%"),
1619
+ foreground: tryHexToHsl(paper, "0 0% 100%"),
1620
+ card: tryHexToHsl(ink, "220 18% 3%"),
1621
+ cardForeground: tryHexToHsl(paper, "0 0% 100%"),
1622
+ popover: tryHexToHsl(ink, "220 18% 3%"),
1623
+ popoverForeground: tryHexToHsl(paper, "0 0% 100%"),
1624
+ // Filled CTA = Mist on dark (not coral)
1625
+ primary: tryHexToHsl(mist, "0 0% 90%"),
1626
+ primaryForeground: tryHexToHsl(iron, "210 1% 27%"),
1627
+ secondary: tryHexToHsl(graphite, "220 4% 11%"),
1628
+ secondaryForeground: tryHexToHsl(paper, "0 0% 100%"),
1629
+ muted: tryHexToHsl(obsidian, "220 6% 7%"),
1630
+ mutedForeground: tryHexToHsl(smoke, "240 1% 42%"),
1631
+ // Coral as accent for brand-adjacent UI (badges that opt-in to accent)
1632
+ accent: tryHexToHsl(coral, "0 100% 69%"),
1633
+ accentForeground: tryHexToHsl(paper, "0 0% 100%"),
1634
+ destructive: "0 72% 51%",
1635
+ destructiveForeground: tryHexToHsl(paper, "0 0% 100%"),
1636
+ border: tryHexToHsl(slate, "210 2% 19%"),
1637
+ input: tryHexToHsl(obsidian, "220 6% 7%"),
1638
+ ring: tryHexToHsl(ash, "240 1% 61%"),
1639
+ success: tryHexToHsl(success, "150 58% 59%"),
1640
+ successForeground: tryHexToHsl(canvas, "210 20% 2%"),
1641
+ info: tryHexToHsl(info, "200 100% 67%"),
1642
+ infoForeground: tryHexToHsl(canvas, "210 20% 2%")
1643
+ },
1644
+ recipe: buildRecipe({
1645
+ buttonDefault: "solid",
1646
+ radii: {
1647
+ button: ctx.recipeHints.radii?.button ?? "8px",
1648
+ card: "16px",
1649
+ badge: "6px",
1650
+ input: "8px"
1651
+ },
1652
+ elevationPreset: ctx.recipeHints.elevationPreset ?? "key",
1653
+ density: ctx.recipeHints.density ?? "comfortable",
1654
+ badgeDefault: "muted"
1655
+ }),
1656
+ // Native-feeling: quick, with a small overshoot on things that appear. The
1657
+ // spring is the point — a launcher should feel like it was already there.
1658
+ motion: {
1659
+ easeOut: "cubic-bezier(0.16, 1, 0.3, 1)",
1660
+ easeInOut: "cubic-bezier(0.4, 0, 0.2, 1)",
1661
+ easeSpring: "cubic-bezier(0.34, 1.56, 0.64, 1)",
1662
+ micro: 90,
1663
+ flow: 160,
1664
+ reveal: 220,
1665
+ cinematic: 340
1666
+ },
1667
+ // Command-palette density — compact, but not as tight as Linear.
1668
+ spacing: {
1669
+ xs: "0.375rem",
1670
+ sm: "0.5rem",
1671
+ md: "0.75rem",
1672
+ lg: "1rem",
1673
+ xl: "1.375rem",
1674
+ "2xl": "2rem"
1675
+ },
1676
+ typography: {
1677
+ fontSans: `'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif`,
1678
+ fontMono: `'Geist Mono', 'GeistMono', ui-monospace, Menlo, monospace`,
1679
+ fontDisplay: `'Inter', ui-sans-serif, system-ui, sans-serif`,
1680
+ headingWeight: 400
1681
+ },
1682
+ extensions: {
1683
+ categories: {
1684
+ brand: coral,
1685
+ ember: ctx.colors["ember-hush"] ?? "#452324",
1686
+ sky: ctx.colors["electric-sky"] ?? "#63a1ff"
1687
+ },
1688
+ sourceUrl: typeof ctx.refero.url === "string" ? ctx.refero.url : "https://raycast.com",
1689
+ notes: [
1690
+ "Primary CTA = Mist fill + Iron text (neutral solid).",
1691
+ "Coral Pulse is brand mark only \u2014 do not use for general product chrome CTAs.",
1692
+ "Cards use elevation=key (keyboard-key inset shadow stack)."
1693
+ ]
1694
+ }
1695
+ };
1696
+ return brand;
1697
+ }
1698
+
1699
+ // src/brand-package/presets/stripe.ts
1700
+ function buildStripe(ctx) {
1701
+ const white = ctx.colors["pure-white"] ?? "#ffffff";
1702
+ const mist = ctx.colors.mist ?? "#f8fafd";
1703
+ const frost = ctx.colors.frost ?? "#e5edf5";
1704
+ const midnight = ctx.colors["midnight-ink"] ?? "#061b31";
1705
+ const slate = ctx.colors.slate ?? "#64748d";
1706
+ const steel = ctx.colors.steel ?? "#50617a";
1707
+ const indigo = ctx.colors["indigo-ink"] ?? "#533afd";
1708
+ const indigoHover = ctx.colors["indigo-hover"] ?? "#7389ff";
1709
+ const lavender = ctx.colors["lavender-border"] ?? "#b9b9f9";
1710
+ const periwinkle = ctx.colors["periwinkle-wash"] ?? "#e8e9ff";
1711
+ const deep = ctx.colors["deep-violet"] ?? "#182659";
1712
+ const smoke = ctx.colors.smoke ?? "#839bc8";
1713
+ ctx.warnings.push(
1714
+ "Stripe: indigo-ink is action CTA only; midnight-ink is brand-mark/wordmark (never default CTA fill)."
1715
+ );
1716
+ ctx.warnings.push(
1717
+ "Stripe: elevation=none \u2014 depth via white\u2192mist\u2192frost tints + 1px frost rules, never box-shadow."
1718
+ );
1719
+ const brand = {
1720
+ id: "stripe",
1721
+ name: "Stripe",
1722
+ darkDefault: false,
1723
+ version: "1.0.0",
1724
+ roles: {
1725
+ canvas: tryHexToHsl(white, "0 0% 100%"),
1726
+ canvasForeground: tryHexToHsl(midnight, "208 78% 11%"),
1727
+ surface: tryHexToHsl(white, "0 0% 100%"),
1728
+ surfaceForeground: tryHexToHsl(midnight, "208 78% 11%"),
1729
+ action: tryHexToHsl(indigo, "248 98% 61%"),
1730
+ actionForeground: tryHexToHsl(white, "0 0% 100%"),
1731
+ brand: tryHexToHsl(midnight, "208 78% 11%"),
1732
+ brandForeground: tryHexToHsl(white, "0 0% 100%"),
1733
+ quiet: tryHexToHsl(periwinkle, "238 100% 95%"),
1734
+ quietForeground: tryHexToHsl(indigo, "248 98% 61%"),
1735
+ muted: tryHexToHsl(mist, "210 56% 98%"),
1736
+ mutedForeground: tryHexToHsl(slate, "215 16% 47%"),
1737
+ border: tryHexToHsl(frost, "210 36% 93%"),
1738
+ input: tryHexToHsl(white, "0 0% 100%"),
1739
+ ring: tryHexToHsl(indigo, "248 98% 61%"),
1740
+ destructive: "0 72% 51%",
1741
+ destructiveForeground: tryHexToHsl(white, "0 0% 100%"),
1742
+ info: tryHexToHsl(indigoHover, "230 100% 73%"),
1743
+ infoForeground: tryHexToHsl(midnight, "208 78% 11%")
1744
+ },
1745
+ semantic: {
1746
+ background: tryHexToHsl(white, "0 0% 100%"),
1747
+ foreground: tryHexToHsl(midnight, "208 78% 11%"),
1748
+ card: tryHexToHsl(white, "0 0% 100%"),
1749
+ cardForeground: tryHexToHsl(midnight, "208 78% 11%"),
1750
+ popover: tryHexToHsl(white, "0 0% 100%"),
1751
+ popoverForeground: tryHexToHsl(midnight, "208 78% 11%"),
1752
+ primary: tryHexToHsl(indigo, "248 98% 61%"),
1753
+ primaryForeground: tryHexToHsl(white, "0 0% 100%"),
1754
+ secondary: tryHexToHsl(periwinkle, "238 100% 95%"),
1755
+ secondaryForeground: tryHexToHsl(indigo, "248 98% 61%"),
1756
+ muted: tryHexToHsl(mist, "210 56% 98%"),
1757
+ mutedForeground: tryHexToHsl(slate, "215 16% 47%"),
1758
+ accent: tryHexToHsl(indigo, "248 98% 61%"),
1759
+ accentForeground: tryHexToHsl(white, "0 0% 100%"),
1760
+ destructive: "0 72% 51%",
1761
+ destructiveForeground: tryHexToHsl(white, "0 0% 100%"),
1762
+ border: tryHexToHsl(frost, "210 36% 93%"),
1763
+ input: tryHexToHsl(white, "0 0% 100%"),
1764
+ ring: tryHexToHsl(indigo, "248 98% 61%"),
1765
+ info: tryHexToHsl(indigoHover, "230 100% 73%"),
1766
+ infoForeground: tryHexToHsl(midnight, "208 78% 11%")
1767
+ },
1768
+ recipe: buildRecipe({
1769
+ buttonDefault: ctx.recipeHints.buttonDefault ?? "solid",
1770
+ radii: {
1771
+ button: ctx.recipeHints.radii?.button ?? "4px",
1772
+ card: "4px",
1773
+ badge: "9999px",
1774
+ input: "4px"
1775
+ },
1776
+ elevationPreset: ctx.recipeHints.elevationPreset ?? "none",
1777
+ density: ctx.recipeHints.density ?? "comfortable",
1778
+ badgeDefault: "muted",
1779
+ // Ghost outline companion uses lavender hairline, not carbon
1780
+ outlineBorder: lavender
1781
+ }),
1782
+ // Deliberate and even. Stripe's motion never draws attention to itself;
1783
+ // the curve is symmetric so a panel leaves the way it arrived.
1784
+ motion: {
1785
+ easeOut: "cubic-bezier(0.215, 0.61, 0.355, 1)",
1786
+ easeInOut: "cubic-bezier(0.645, 0.045, 0.355, 1)",
1787
+ micro: 100,
1788
+ flow: 180,
1789
+ reveal: 260,
1790
+ cinematic: 400
1791
+ },
1792
+ // Clean commerce chrome — a touch more generous than the app-shell trio.
1793
+ spacing: {
1794
+ xs: "0.5rem",
1795
+ sm: "0.75rem",
1796
+ md: "1.125rem",
1797
+ lg: "1.75rem",
1798
+ xl: "2.25rem",
1799
+ "2xl": "3.25rem"
1800
+ },
1801
+ typography: {
1802
+ fontSans: `'sohne-var', 'Inter Tight', 'Inter', ui-sans-serif, system-ui, sans-serif`,
1803
+ fontDisplay: `'sohne-var', 'Inter Tight', ui-sans-serif, system-ui, sans-serif`,
1804
+ // Whisper weight is the Stripe signature (even at 56px display)
1805
+ headingWeight: 300
1806
+ },
1807
+ extensions: {
1808
+ categories: {
1809
+ brand: midnight,
1810
+ action: indigo,
1811
+ link: indigo,
1812
+ hover: indigoHover,
1813
+ ghostBorder: lavender,
1814
+ wash: periwinkle,
1815
+ deep,
1816
+ smoke,
1817
+ steel
1818
+ },
1819
+ decorative: {
1820
+ "section-band": mist,
1821
+ frost
1822
+ },
1823
+ sourceUrl: typeof ctx.refero.url === "string" ? ctx.refero.url : "https://stripe.com",
1824
+ notes: [
1825
+ "roles.action = Indigo Ink filled CTA; roles.brand = Midnight Ink wordmark.",
1826
+ "Elevation none \u2014 tint ladder + 1px frost rules; never box-shadow.",
1827
+ "Control ctx.radius 4px (not pill); tags may stay full-pill.",
1828
+ "Typography weight 300 is the product signature (Inter Tight substitute).",
1829
+ "Pair solid CTA with ghost outline (lavender border) as secondary."
1830
+ ]
1831
+ }
1832
+ };
1833
+ return brand;
1834
+ }
1835
+
1836
+ // src/brand-package/presets/vanta.ts
1837
+ function buildVanta(ctx) {
1838
+ const parchment = ctx.colors.parchment ?? ctx.colors["page-canvas"] ?? "#f7f8fa";
1839
+ const paper = ctx.colors.paper ?? ctx.colors["card-surface"] ?? "#ffffff";
1840
+ const carbon = ctx.colors.carbon ?? "#181822";
1841
+ const graphite = ctx.colors.graphite ?? "#6d6e87";
1842
+ const steel = ctx.colors.steel ?? "#9e9fb7";
1843
+ const ash = ctx.colors.ash ?? "#dfdfe9";
1844
+ const fog = ctx.colors.fog ?? "#eaeaf1";
1845
+ const lavender = ctx.colors["lavender-wash"] ?? "#ddd6ff";
1846
+ const vivid = ctx.colors["vivid-violet"] ?? "#5e05c4";
1847
+ const indigo = ctx.colors["indigo-ink"] ?? "#260048";
1848
+ const mid = ctx.colors["mid-violet"] ?? "#8f47d5";
1849
+ const amber = ctx.colors["amber-signal"] ?? "#ffbe0f";
1850
+ ctx.warnings.push(
1851
+ "Vanta: vivid-violet is action CTA only; indigo-ink is brand-mark/logo (never default CTA fill)."
1852
+ );
1853
+ ctx.warnings.push("Vanta: elevation=none \u2014 cards use 1px carbon border, not box-shadow.");
1854
+ const brand = {
1855
+ id: "vanta",
1856
+ name: "Vanta",
1857
+ darkDefault: false,
1858
+ version: "1.0.0",
1859
+ roles: {
1860
+ canvas: tryHexToHsl(parchment, "220 23% 97%"),
1861
+ canvasForeground: tryHexToHsl(carbon, "240 14% 11%"),
1862
+ surface: tryHexToHsl(paper, "0 0% 100%"),
1863
+ surfaceForeground: tryHexToHsl(carbon, "240 14% 11%"),
1864
+ // Action = single saturated CTA moment
1865
+ action: tryHexToHsl(vivid, "268 95% 39%"),
1866
+ actionForeground: tryHexToHsl(paper, "0 0% 100%"),
1867
+ // Brand mark = logo / wordmark / decorative ink (≠ action)
1868
+ brand: tryHexToHsl(indigo, "273 100% 14%"),
1869
+ brandForeground: tryHexToHsl(paper, "0 0% 100%"),
1870
+ // Quiet = lavender informational chips (not violet fill)
1871
+ quiet: tryHexToHsl(lavender, "249 100% 92%"),
1872
+ quietForeground: tryHexToHsl(indigo, "273 100% 14%"),
1873
+ muted: tryHexToHsl(fog, "240 14% 93%"),
1874
+ mutedForeground: tryHexToHsl(graphite, "237 11% 48%"),
1875
+ border: tryHexToHsl(carbon, "240 14% 11%"),
1876
+ input: tryHexToHsl(paper, "0 0% 100%"),
1877
+ ring: tryHexToHsl(vivid, "268 95% 39%"),
1878
+ destructive: "0 72% 51%",
1879
+ destructiveForeground: tryHexToHsl(paper, "0 0% 100%"),
1880
+ warning: tryHexToHsl(amber, "44 100% 53%"),
1881
+ warningForeground: tryHexToHsl(carbon, "240 14% 11%"),
1882
+ info: tryHexToHsl(mid, "269 63% 56%"),
1883
+ infoForeground: tryHexToHsl(paper, "0 0% 100%")
1884
+ },
1885
+ // semantic filled by normalize from roles
1886
+ semantic: {
1887
+ background: tryHexToHsl(parchment, "220 23% 97%"),
1888
+ foreground: tryHexToHsl(carbon, "240 14% 11%"),
1889
+ card: tryHexToHsl(paper, "0 0% 100%"),
1890
+ cardForeground: tryHexToHsl(carbon, "240 14% 11%"),
1891
+ popover: tryHexToHsl(paper, "0 0% 100%"),
1892
+ popoverForeground: tryHexToHsl(carbon, "240 14% 11%"),
1893
+ primary: tryHexToHsl(vivid, "268 95% 39%"),
1894
+ primaryForeground: tryHexToHsl(paper, "0 0% 100%"),
1895
+ secondary: tryHexToHsl(lavender, "249 100% 92%"),
1896
+ secondaryForeground: tryHexToHsl(indigo, "273 100% 14%"),
1897
+ muted: tryHexToHsl(fog, "240 14% 93%"),
1898
+ mutedForeground: tryHexToHsl(graphite, "237 11% 48%"),
1899
+ accent: tryHexToHsl(indigo, "273 100% 14%"),
1900
+ accentForeground: tryHexToHsl(paper, "0 0% 100%"),
1901
+ destructive: "0 72% 51%",
1902
+ destructiveForeground: tryHexToHsl(paper, "0 0% 100%"),
1903
+ border: tryHexToHsl(carbon, "240 14% 11%"),
1904
+ input: tryHexToHsl(paper, "0 0% 100%"),
1905
+ ring: tryHexToHsl(vivid, "268 95% 39%"),
1906
+ warning: tryHexToHsl(amber, "44 100% 53%"),
1907
+ warningForeground: tryHexToHsl(carbon, "240 14% 11%"),
1908
+ info: tryHexToHsl(mid, "269 63% 56%"),
1909
+ infoForeground: tryHexToHsl(paper, "0 0% 100%")
1910
+ },
1911
+ recipe: buildRecipe({
1912
+ buttonDefault: ctx.recipeHints.buttonDefault ?? "solid",
1913
+ // Prefer structural ctx.radius tokens (full=pill, 2xl=cards) over free-text
1914
+ radii: {
1915
+ button: leafHex(ctx.radius, ["buttons"]) ?? leafHex(ctx.radius, ["full"]) ?? ctx.recipeHints.radii?.button ?? "999px",
1916
+ card: leafHex(ctx.radius, ["cards"]) ?? leafHex(ctx.radius, ["2xl"]) ?? "16px",
1917
+ badge: leafHex(ctx.radius, ["badges"]) ?? leafHex(ctx.radius, ["full"]) ?? ctx.recipeHints.radii?.button ?? "999px",
1918
+ input: leafHex(ctx.radius, ["inputs"]) ?? leafHex(ctx.radius, ["full"]) ?? ctx.recipeHints.radii?.button ?? "999px"
1919
+ },
1920
+ elevationPreset: ctx.recipeHints.elevationPreset ?? "none",
1921
+ density: ctx.recipeHints.density ?? "comfortable",
1922
+ // Informational chips = lavender wash + indigo (quiet), not violet CTA fill
1923
+ badgeDefault: "muted",
1924
+ outlineBorder: carbon
1925
+ }),
1926
+ // Atmospheric. Backdrops drift rather than move, so even the micro step is
1927
+ // slow and nothing snaps.
1928
+ motion: {
1929
+ easeOut: "cubic-bezier(0.33, 1, 0.68, 1)",
1930
+ easeInOut: "cubic-bezier(0.37, 0, 0.63, 1)",
1931
+ micro: 120,
1932
+ flow: 240,
1933
+ reveal: 380,
1934
+ cinematic: 620
1935
+ },
1936
+ // The most spacious of the seven — serif editorial wants the most air.
1937
+ spacing: {
1938
+ xs: "0.75rem",
1939
+ sm: "1.125rem",
1940
+ md: "1.75rem",
1941
+ lg: "2.5rem",
1942
+ xl: "3.5rem",
1943
+ "2xl": "5rem"
1944
+ },
1945
+ typography: {
1946
+ fontSans: `'Inter Variable', 'Inter', ui-sans-serif, system-ui, sans-serif`,
1947
+ fontDisplay: `'Reckless', 'Source Serif 4', 'Lora', ui-serif, Georgia, serif`,
1948
+ headingWeight: 500
1949
+ },
1950
+ extensions: {
1951
+ categories: {
1952
+ brand: indigo,
1953
+ action: vivid,
1954
+ link: mid,
1955
+ heroWash: lavender,
1956
+ warning: amber,
1957
+ steel,
1958
+ ash
1959
+ },
1960
+ decorative: {
1961
+ "hero-wash": lavender
1962
+ },
1963
+ sourceUrl: typeof ctx.refero.url === "string" ? ctx.refero.url : "https://www.vanta.com",
1964
+ notes: [
1965
+ "roles.action = Vivid Violet (filled CTA only).",
1966
+ "roles.brand = Indigo Ink (logo / wordmark / brand-mark).",
1967
+ "Elevation none \u2014 1px carbon borders frame cards; no drop-shadow.",
1968
+ "Full-pill controls (999px); cards 16px.",
1969
+ "UI = Inter Variable; marketing display = Reckless serif.",
1970
+ "Lavender wash is marketing hero surface (decorative), not product chrome fill."
1971
+ ]
1972
+ }
1973
+ };
1974
+ return brand;
1975
+ }
1976
+
1977
+ // src/brand-package/presets/vercel.ts
1978
+ function vercelLightSemantic(paper, pure, hairline, charcoal, stone, obsidian, terminal) {
1979
+ return {
1980
+ background: tryHexToHsl(paper, "0 0% 98%"),
1981
+ foreground: tryHexToHsl(obsidian, "0 0% 9%"),
1982
+ card: tryHexToHsl(pure, "0 0% 100%"),
1983
+ cardForeground: tryHexToHsl(obsidian, "0 0% 9%"),
1984
+ popover: tryHexToHsl(pure, "0 0% 100%"),
1985
+ popoverForeground: tryHexToHsl(obsidian, "0 0% 9%"),
1986
+ // Filled black button
1987
+ primary: tryHexToHsl(obsidian, "0 0% 9%"),
1988
+ primaryForeground: tryHexToHsl(pure, "0 0% 100%"),
1989
+ secondary: tryHexToHsl(hairline, "0 0% 92%"),
1990
+ secondaryForeground: tryHexToHsl(charcoal, "0 0% 30%"),
1991
+ muted: tryHexToHsl(hairline, "0 0% 92%"),
1992
+ mutedForeground: tryHexToHsl(stone, "0 0% 40%"),
1993
+ accent: tryHexToHsl(hairline, "0 0% 92%"),
1994
+ accentForeground: tryHexToHsl(obsidian, "0 0% 9%"),
1995
+ destructive: "0 72% 51%",
1996
+ destructiveForeground: tryHexToHsl(pure, "0 0% 100%"),
1997
+ border: tryHexToHsl(hairline, "0 0% 92%"),
1998
+ input: tryHexToHsl(pure, "0 0% 100%"),
1999
+ ring: tryHexToHsl(obsidian, "0 0% 9%"),
2000
+ success: tryHexToHsl(terminal, "133 49% 32%"),
2001
+ successForeground: tryHexToHsl(pure, "0 0% 100%"),
2002
+ info: tryHexToHsl(charcoal, "0 0% 30%"),
2003
+ infoForeground: tryHexToHsl(pure, "0 0% 100%")
2004
+ };
2005
+ }
2006
+ function vercelDarkSemantic(terminal) {
2007
+ return {
2008
+ background: "0 0% 0%",
2009
+ foreground: "0 0% 93%",
2010
+ card: "0 0% 4%",
2011
+ cardForeground: "0 0% 93%",
2012
+ popover: "0 0% 4%",
2013
+ popoverForeground: "0 0% 93%",
2014
+ primary: "0 0% 100%",
2015
+ primaryForeground: "0 0% 0%",
2016
+ secondary: "0 0% 12%",
2017
+ secondaryForeground: "0 0% 93%",
2018
+ muted: "0 0% 12%",
2019
+ mutedForeground: "0 0% 63%",
2020
+ accent: "0 0% 12%",
2021
+ accentForeground: "0 0% 93%",
2022
+ destructive: "0 72% 51%",
2023
+ destructiveForeground: "0 0% 100%",
2024
+ border: "0 0% 16%",
2025
+ input: "0 0% 4%",
2026
+ ring: "0 0% 100%",
2027
+ success: tryHexToHsl(terminal, "133 49% 40%"),
2028
+ successForeground: "0 0% 100%",
2029
+ info: "0 0% 70%",
2030
+ infoForeground: "0 0% 0%"
2031
+ };
2032
+ }
2033
+ function buildVercel(ctx) {
2034
+ const paper = ctx.colors["paper-white"] ?? ctx.colors["page-canvas"] ?? "#fafafa";
2035
+ const pure = ctx.colors["pure-white"] ?? ctx.colors["card-surface"] ?? "#ffffff";
2036
+ const hairline = ctx.colors.hairline ?? "#ebebeb";
2037
+ const charcoal = ctx.colors.charcoal ?? "#4d4d4d";
2038
+ const stone = ctx.colors.stone ?? "#666666";
2039
+ const obsidian = ctx.colors.obsidian ?? ctx.colors["inverted-surface"] ?? "#171717";
2040
+ const carbon = ctx.colors.carbon ?? "#000000";
2041
+ const terminal = ctx.colors["terminal-green"] ?? "#297a3a";
2042
+ ctx.warnings.push(
2043
+ "Vercel: monochrome dual-mode \u2014 no chromatic CTA; Terminal Green is support only."
2044
+ );
2045
+ const light = vercelLightSemantic(paper, pure, hairline, charcoal, stone, obsidian, terminal);
2046
+ const dark = vercelDarkSemantic(terminal);
2047
+ const brand = {
2048
+ id: "vercel",
2049
+ name: "Vercel",
2050
+ darkDefault: false,
2051
+ version: "1.0.0",
2052
+ semantic: light,
2053
+ modes: {
2054
+ light: { semantic: light },
2055
+ dark: { semantic: dark }
2056
+ },
2057
+ recipe: buildRecipe({
2058
+ buttonDefault: "solid",
2059
+ radii: {
2060
+ button: ctx.recipeHints.radii?.button ?? "6px",
2061
+ card: "6px",
2062
+ badge: "6px",
2063
+ input: "6px"
2064
+ },
2065
+ elevationPreset: ctx.recipeHints.elevationPreset === "key" ? "hairline" : ctx.recipeHints.elevationPreset ?? "hairline",
2066
+ density: ctx.recipeHints.density ?? "compact",
2067
+ badgeDefault: "muted",
2068
+ cardShadow: "rgba(0, 0, 0, 0.08) 0px 0px 0px 1px, rgb(250, 250, 250) 0px 0px 0px 1px",
2069
+ outlineBorder: hairline
2070
+ }),
2071
+ // Neutral and quick. Geist treats motion as feedback rather than
2072
+ // expression, so the ramp is short and the curve is the platform default.
2073
+ motion: {
2074
+ easeOut: "cubic-bezier(0, 0, 0.2, 1)",
2075
+ easeInOut: "cubic-bezier(0.4, 0, 0.2, 1)",
2076
+ micro: 100,
2077
+ flow: 150,
2078
+ reveal: 200,
2079
+ cinematic: 300
2080
+ },
2081
+ // Geist's default rhythm — the middle of the seven, same figures as the
2082
+ // shared fallback rail so a page with no language selected still matches it.
2083
+ spacing: {
2084
+ xs: "0.5rem",
2085
+ sm: "0.75rem",
2086
+ md: "1rem",
2087
+ lg: "1.5rem",
2088
+ xl: "2rem",
2089
+ "2xl": "3rem"
2090
+ },
2091
+ typography: {
2092
+ fontSans: `'Geist Sans', 'Geist', ui-sans-serif, system-ui, sans-serif`,
2093
+ fontMono: `'Geist Mono', ui-monospace, Menlo, monospace`,
2094
+ fontDisplay: `'Geist Sans', 'Geist', ui-sans-serif, system-ui, sans-serif`,
2095
+ headingWeight: 450
2096
+ },
2097
+ extensions: {
2098
+ categories: {
2099
+ carbon,
2100
+ terminal
2101
+ },
2102
+ sourceUrl: typeof ctx.refero.url === "string" ? ctx.refero.url : "https://vercel.com",
2103
+ notes: [
2104
+ "Dual-mode monochrome \u2014 light: Obsidian CTA; dark: white CTA on carbon.",
2105
+ "Elevation is hairline double-ring, never drop-shadow.",
2106
+ "Spectrum/solar gradients are marketing-only decorative (not product chrome)."
2107
+ ]
2108
+ }
2109
+ };
2110
+ return brand;
2111
+ }
2112
+
2113
+ // src/brand-package/presets/index.ts
2114
+ var PRESET_BUILDERS = {
2115
+ linear: buildLinear,
2116
+ gsap: buildGsap,
2117
+ raycast: buildRaycast,
2118
+ vercel: buildVercel,
2119
+ notion: buildNotion,
2120
+ stripe: buildStripe,
2121
+ vanta: buildVanta
2122
+ };
2123
+ function buildPresetBrand(preset, ctx) {
2124
+ if (preset === "generic") return buildGeneric(ctx);
2125
+ return PRESET_BUILDERS[preset](ctx);
2126
+ }
2127
+
2128
+ // src/brand-package/compile-refero.ts
2129
+ function finish(brand, warnings) {
2130
+ const b = normalizeBrandPackage(brand);
2131
+ return { brand: b, css: emitBrandCss(b), warnings };
2132
+ }
2133
+ function compileReferoTokens(input) {
2134
+ const warnings = [];
2135
+ const color = input.tokens.color ?? {};
2136
+ const surface = input.tokens.surface ?? {};
2137
+ const font = input.tokens.font ?? {};
2138
+ const radius = input.tokens.radius ?? {};
2139
+ const ext = input.tokens.$extensions ?? {};
2140
+ const refero = ext["com.refero.extraction"] ?? {};
2141
+ const colors = { ...collectColors(color), ...collectSurfaces(surface) };
2142
+ const siteName = typeof refero.siteName === "string" && refero.siteName || input.name || "Custom Brand";
2143
+ const id = input.id || siteName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "custom";
2144
+ const preset = detectPreset(id, colors);
2145
+ const recipeHints = inferRecipeFromDesignMd(input.designMd ?? "");
2146
+ const brand = buildPresetBrand(preset, {
2147
+ colors,
2148
+ font,
2149
+ radius,
2150
+ refero,
2151
+ recipeHints,
2152
+ warnings,
2153
+ id,
2154
+ siteName
2155
+ });
2156
+ return finish(brand, warnings);
2157
+ }
2158
+
2159
+ // src/brand-package/validate.ts
2160
+ var BUTTON_STYLES = /* @__PURE__ */ new Set(["solid", "outline", "gradient-stroke"]);
2161
+ function validateBrandPackage(brand) {
2162
+ const errors = [];
2163
+ const warnings = [];
2164
+ if (!brand || typeof brand !== "object") {
2165
+ return { ok: false, errors: ["Brand package must be an object"], warnings };
2166
+ }
2167
+ let b;
2168
+ try {
2169
+ b = normalizeBrandPackage(brand);
2170
+ } catch (e) {
2171
+ return { ok: false, errors: [`normalize failed: ${e.message}`], warnings };
2172
+ }
2173
+ if (!b.id || typeof b.id !== "string") errors.push("id is required");
2174
+ if (!b.name || typeof b.name !== "string") errors.push("name is required");
2175
+ if (!b.version || typeof b.version !== "string") errors.push("version is required");
2176
+ if (!b.roles) {
2177
+ errors.push("roles missing after normalize");
2178
+ } else {
2179
+ for (const key of ["canvas", "action", "actionForeground", "border"]) {
2180
+ if (!b.roles[key]) errors.push(`roles.${key} is required`);
2181
+ }
2182
+ if (b.roles.brand && b.roles.brand === b.roles.action) {
2183
+ warnings.push(
2184
+ "roles.brand equals roles.action \u2014 brand mark is not separated from CTA (often intentional)"
2185
+ );
2186
+ }
2187
+ }
2188
+ if (!b.semantic || typeof b.semantic !== "object") {
2189
+ errors.push("semantic is required");
2190
+ } else {
2191
+ for (const key of [
2192
+ "background",
2193
+ "foreground",
2194
+ "primary",
2195
+ "primaryForeground",
2196
+ "border",
2197
+ "ring"
2198
+ ]) {
2199
+ if (!b.semantic[key]) errors.push(`semantic.${key} is required`);
2200
+ }
2201
+ if (b.roles && b.semantic.primary !== b.roles.action) {
2202
+ errors.push("semantic.primary must equal roles.action (CTA bridge)");
2203
+ }
2204
+ }
2205
+ if (!b.recipe || typeof b.recipe !== "object") {
2206
+ errors.push("recipe is required");
2207
+ } else {
2208
+ if (!BUTTON_STYLES.has(b.recipe.buttonDefault)) {
2209
+ errors.push(`recipe.buttonDefault must be one of ${[...BUTTON_STYLES].join(", ")}`);
2210
+ }
2211
+ if (!b.recipe.radii?.button) errors.push("recipe.radii.button is required");
2212
+ if (!b.recipe.radii?.card) errors.push("recipe.radii.card is required");
2213
+ if (!b.recipe.elevationTokens?.card) {
2214
+ errors.push("recipe.elevationTokens.card is required (free CSS box-shadow)");
2215
+ }
2216
+ if (b.recipe.buttonDefault === "gradient-stroke" && !b.recipe.primaryStrokeGradient) {
2217
+ warnings.push(
2218
+ "gradient-stroke without primaryStrokeGradient \u2014 border falls back to solid primary"
2219
+ );
2220
+ }
2221
+ }
2222
+ if (!b.typography?.fontSans) errors.push("typography.fontSans is required");
2223
+ if (b.typography?.faces) {
2224
+ b.typography.faces.forEach((face, i) => {
2225
+ if (!face.family) errors.push(`typography.faces[${i}].family is required`);
2226
+ if (!face.src?.length) errors.push(`typography.faces[${i}].src must be non-empty`);
2227
+ else {
2228
+ for (const [j, src] of face.src.entries()) {
2229
+ if (!src.url) errors.push(`typography.faces[${i}].src[${j}].url is required`);
2230
+ }
2231
+ }
2232
+ });
2233
+ }
2234
+ for (const [key, value] of Object.entries(b.recipe?.elevationTokens ?? {})) {
2235
+ if (typeof value === "string" && /var\(\s*--(?:shadow|elevation)-/.test(value)) {
2236
+ errors.push(
2237
+ `recipe.elevationTokens.${key} references a shadow/elevation variable \u2014 that is a cycle once the skin sets the source. Inline the value.`
2238
+ );
2239
+ }
2240
+ }
2241
+ if (b.motion) {
2242
+ for (const key of ["micro", "flow", "reveal", "cinematic"]) {
2243
+ const value = b.motion[key];
2244
+ if (value == null) continue;
2245
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
2246
+ errors.push(`motion.${key} must be a non-negative number of milliseconds`);
2247
+ } else if (value > 2e3) {
2248
+ warnings.push(`motion.${key} is ${value}ms \u2014 long enough to read as a stall`);
2249
+ }
2250
+ }
2251
+ for (const key of ["easeOut", "easeInOut", "easeSpring"]) {
2252
+ const value = b.motion[key];
2253
+ if (value == null) continue;
2254
+ if (typeof value !== "string" || !value.trim()) {
2255
+ errors.push(`motion.${key} must be a non-empty timing function`);
2256
+ }
2257
+ }
2258
+ const { micro, flow, reveal, cinematic } = b.motion;
2259
+ const ramp = [micro, flow, reveal, cinematic].filter((v) => typeof v === "number");
2260
+ if (ramp.length > 1 && ramp.some((v, i) => i > 0 && v < (ramp[i - 1] ?? v))) {
2261
+ warnings.push(
2262
+ "motion durations are not ascending \u2014 micro should be the shortest and cinematic the longest"
2263
+ );
2264
+ }
2265
+ }
2266
+ if (b.spacing) {
2267
+ const UNIT = /^-?\d*\.?\d+(rem|em|px|%|vh|vw|ch)$/;
2268
+ const order = [];
2269
+ for (const key of ["xs", "sm", "md", "lg", "xl", "2xl"]) {
2270
+ const value = b.spacing[key];
2271
+ if (value == null) continue;
2272
+ if (typeof value !== "string" || !UNIT.test(value.trim())) {
2273
+ errors.push(
2274
+ `spacing.${key} must be a CSS length with a unit (e.g. "1rem"), got ${JSON.stringify(value)}`
2275
+ );
2276
+ continue;
2277
+ }
2278
+ order.push([key, Number.parseFloat(value)]);
2279
+ }
2280
+ if (order.length > 1 && order.some(([, v], i) => i > 0 && v < (order[i - 1]?.[1] ?? v))) {
2281
+ warnings.push(
2282
+ "spacing steps are not ascending \u2014 xs should be the smallest and 2xl the largest"
2283
+ );
2284
+ }
2285
+ }
2286
+ if (b.recipe?.buttonDefault === "solid" && b.semantic?.primary === b.semantic?.primaryForeground) {
2287
+ warnings.push("primary and primaryForeground are identical \u2014 check contrast");
2288
+ }
2289
+ return { ok: errors.length === 0, errors, warnings };
2290
+ }
32
2291
  export {
33
2292
  BRAND_STORAGE_KEY,
34
2293
  BRAND_STYLE_ELEMENT_ID,
35
2294
  applyBrandCss,
36
2295
  applyBrandPackage,
37
- applyBrandToIframe,
38
2296
  clearBrand,
39
2297
  colorToHslChannels,
40
2298
  compileReferoTokens,
@@ -54,8 +2312,6 @@ export {
54
2312
  semanticFromRoles,
55
2313
  tryColorToHsl,
56
2314
  tryHexToHsl,
57
- useBrand,
58
- useBrandIframePreview,
59
2315
  validateBrandPackage
60
2316
  };
61
2317
  //# sourceMappingURL=index.js.map