@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,9 +1,961 @@
1
1
  "use client";
2
- import {
3
- applyBrandToIframe,
4
- useBrand,
5
- useBrandIframePreview
6
- } from "../chunk-ALJTP5HL.js";
2
+
3
+ // src/brand-package/use-brand.ts
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+
6
+ // src/brand-package/emit-css.ts
7
+ import { withNearestRegistryFont } from "@nebutra/fonts/registry";
8
+
9
+ // src/brand-package/hex-to-hsl.ts
10
+ function hexToHslChannels(hex) {
11
+ let h = hex.trim().replace(/^#/, "");
12
+ if (h.length === 3) {
13
+ h = h.split("").map((c) => c + c).join("");
14
+ }
15
+ if (h.length !== 6 || !/^[0-9a-fA-F]+$/.test(h)) {
16
+ throw new Error(`Invalid hex color: ${hex}`);
17
+ }
18
+ const r = Number.parseInt(h.slice(0, 2), 16) / 255;
19
+ const g = Number.parseInt(h.slice(2, 4), 16) / 255;
20
+ const b = Number.parseInt(h.slice(4, 6), 16) / 255;
21
+ return srgbToHslChannels(r, g, b);
22
+ }
23
+ function srgbToHslChannels(r, g, b) {
24
+ const max = Math.max(r, g, b);
25
+ const min = Math.min(r, g, b);
26
+ const l = (max + min) / 2;
27
+ let s = 0;
28
+ let hue = 0;
29
+ if (max !== min) {
30
+ const d = max - min;
31
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
32
+ switch (max) {
33
+ case r:
34
+ hue = (g - b) / d + (g < b ? 6 : 0);
35
+ break;
36
+ case g:
37
+ hue = (b - r) / d + 2;
38
+ break;
39
+ default:
40
+ hue = (r - g) / d + 4;
41
+ }
42
+ hue /= 6;
43
+ }
44
+ const H = Math.round(hue * 360);
45
+ const S = Math.round(s * 100);
46
+ const L = Math.round(l * 100);
47
+ return `${H} ${S}% ${L}%`;
48
+ }
49
+ var HSL_CHANNELS_RE = /^(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%$/;
50
+ function colorToHslChannels(color) {
51
+ const t = color.trim();
52
+ if (!t) throw new Error("Empty color");
53
+ const channels = t.match(HSL_CHANNELS_RE);
54
+ if (channels) {
55
+ return `${Math.round(Number(channels[1]))} ${Math.round(Number(channels[2]))}% ${Math.round(Number(channels[3]))}%`;
56
+ }
57
+ const hslPrefix = t.match(/^hsla?\(/i);
58
+ if (hslPrefix) {
59
+ const inner = t.slice(hslPrefix[0].length).split(")")[0] ?? "";
60
+ const nums = inner.match(/[\d.]+/g) ?? [];
61
+ if (nums.length >= 3) {
62
+ return `${Math.round(Number(nums[0]))} ${Math.round(Number(nums[1]))}% ${Math.round(Number(nums[2]))}%`;
63
+ }
64
+ }
65
+ const rgbPrefix = t.match(/^rgba?\(/i);
66
+ if (rgbPrefix) {
67
+ const inner = t.slice(rgbPrefix[0].length).split(")")[0] ?? "";
68
+ const nums = inner.match(/[\d.]+/g) ?? [];
69
+ if (nums.length >= 3) {
70
+ return srgbToHslChannels(Number(nums[0]) / 255, Number(nums[1]) / 255, Number(nums[2]) / 255);
71
+ }
72
+ }
73
+ if (t.startsWith("#") || /^[0-9a-fA-F]{3,8}$/.test(t)) {
74
+ return hexToHslChannels(t.startsWith("#") ? t : `#${t}`);
75
+ }
76
+ throw new Error(`Unsupported color: ${color}`);
77
+ }
78
+ function tryHexToHsl(hex, fallback) {
79
+ return tryColorToHsl(hex, fallback);
80
+ }
81
+ function tryColorToHsl(color, fallback) {
82
+ if (!color) return fallback;
83
+ try {
84
+ return colorToHslChannels(color);
85
+ } catch {
86
+ return fallback;
87
+ }
88
+ }
89
+
90
+ // src/brand-package/normalize.ts
91
+ var NONE = "0 0 #0000";
92
+ 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";
93
+ var HAIRLINE_SHADOW = "rgba(0, 0, 0, 0.08) 0px 0px 0px 1px, rgb(250, 250, 250) 0px 0px 0px 1px";
94
+ var SOFT_CARD = "0 1px 2px 0 rgb(0 0 0 / 0.05)";
95
+ var SOFT_CONTROL = "0 1px 2px 0 rgb(0 0 0 / 0.04)";
96
+ var SOFT_RAISED = "0 4px 6px -1px rgb(0 0 0 / 0.1)";
97
+ function asChannels(value) {
98
+ if (value == null || value === "") return void 0;
99
+ const v = value.trim();
100
+ if (v.startsWith("#")) return tryHexToHsl(v, "0 0% 50%");
101
+ return v;
102
+ }
103
+ function elevationPresetToTokens(preset, cardShadow) {
104
+ switch (preset) {
105
+ case "none":
106
+ return { card: NONE, control: NONE, raised: NONE };
107
+ case "key":
108
+ return {
109
+ card: cardShadow ?? KEY_SHADOW,
110
+ control: NONE,
111
+ raised: cardShadow ?? KEY_SHADOW
112
+ };
113
+ case "hairline":
114
+ return {
115
+ card: cardShadow ?? HAIRLINE_SHADOW,
116
+ control: NONE,
117
+ raised: cardShadow ?? HAIRLINE_SHADOW
118
+ };
119
+ case "raised":
120
+ return { card: SOFT_RAISED, control: SOFT_CONTROL, raised: SOFT_RAISED };
121
+ default:
122
+ return { card: SOFT_CARD, control: SOFT_CONTROL, raised: SOFT_RAISED };
123
+ }
124
+ }
125
+ function rolesFromSemantic(s, brandMark) {
126
+ const roles = {
127
+ canvas: s.background,
128
+ canvasForeground: s.foreground,
129
+ surface: s.card,
130
+ surfaceForeground: s.cardForeground,
131
+ action: s.primary,
132
+ actionForeground: s.primaryForeground,
133
+ quiet: s.secondary,
134
+ quietForeground: s.secondaryForeground,
135
+ muted: s.muted,
136
+ mutedForeground: s.mutedForeground,
137
+ border: s.border,
138
+ ring: s.ring,
139
+ destructive: s.destructive,
140
+ destructiveForeground: s.destructiveForeground
141
+ };
142
+ if (s.input) roles.input = s.input;
143
+ const brand = asChannels(brandMark?.brand);
144
+ if (brand) roles.brand = brand;
145
+ const brandFg = asChannels(brandMark?.brandForeground);
146
+ if (brandFg) roles.brandForeground = brandFg;
147
+ if (s.success) roles.success = s.success;
148
+ if (s.successForeground) roles.successForeground = s.successForeground;
149
+ if (s.warning) roles.warning = s.warning;
150
+ if (s.warningForeground) roles.warningForeground = s.warningForeground;
151
+ if (s.info) roles.info = s.info;
152
+ if (s.infoForeground) roles.infoForeground = s.infoForeground;
153
+ return roles;
154
+ }
155
+ function semanticFromRoles(r) {
156
+ const accent = r.brand ?? r.quiet;
157
+ const accentFg = r.brandForeground ?? r.quietForeground;
158
+ const semantic = {
159
+ background: r.canvas,
160
+ foreground: r.canvasForeground,
161
+ card: r.surface,
162
+ cardForeground: r.surfaceForeground,
163
+ popover: r.surface,
164
+ popoverForeground: r.surfaceForeground,
165
+ primary: r.action,
166
+ primaryForeground: r.actionForeground,
167
+ secondary: r.quiet,
168
+ secondaryForeground: r.quietForeground,
169
+ muted: r.muted,
170
+ mutedForeground: r.mutedForeground,
171
+ accent,
172
+ accentForeground: accentFg,
173
+ destructive: r.destructive,
174
+ destructiveForeground: r.destructiveForeground,
175
+ border: r.border,
176
+ ring: r.ring
177
+ };
178
+ if (r.input) semantic.input = r.input;
179
+ if (r.success) semantic.success = r.success;
180
+ if (r.successForeground) semantic.successForeground = r.successForeground;
181
+ if (r.warning) semantic.warning = r.warning;
182
+ if (r.warningForeground) semantic.warningForeground = r.warningForeground;
183
+ if (r.info) semantic.info = r.info;
184
+ if (r.infoForeground) semantic.infoForeground = r.infoForeground;
185
+ return semantic;
186
+ }
187
+ function normalizeRadii(recipe) {
188
+ const button = recipe.radii?.button ?? recipe.buttonRadius ?? "0.375rem";
189
+ const card = recipe.radii?.card ?? recipe.cardRadius ?? "0.75rem";
190
+ const radii = {
191
+ button,
192
+ card,
193
+ badge: recipe.radii?.badge ?? recipe.badgeRadius ?? "9999px",
194
+ input: recipe.radii?.input ?? recipe.inputRadius ?? button,
195
+ pill: recipe.radii?.pill ?? "9999px"
196
+ };
197
+ return radii;
198
+ }
199
+ function normalizeElevation(recipe) {
200
+ if (recipe.elevationTokens?.card) {
201
+ const elev = { card: recipe.elevationTokens.card };
202
+ elev.control = recipe.elevationTokens.control ?? NONE;
203
+ elev.raised = recipe.elevationTokens.raised ?? recipe.elevationTokens.card;
204
+ return elev;
205
+ }
206
+ return elevationPresetToTokens(recipe.elevation, recipe.cardShadow);
207
+ }
208
+ function normalizeBadgeDefault(badge) {
209
+ if (!badge || badge === "match-primary") return "match-action";
210
+ return badge;
211
+ }
212
+ function categoryBrandMark(brand) {
213
+ return typeof brand.extensions?.categories?.brand === "string" ? brand.extensions.categories.brand : void 0;
214
+ }
215
+ function normalizeModePalette(palette, categoryBrand) {
216
+ let baseRoles;
217
+ if (palette.roles) {
218
+ baseRoles = { ...palette.roles };
219
+ } else if (palette.semantic) {
220
+ const mark = {
221
+ brandForeground: palette.semantic.primaryForeground
222
+ };
223
+ if (categoryBrand) mark.brand = categoryBrand;
224
+ baseRoles = rolesFromSemantic(palette.semantic, mark);
225
+ } else {
226
+ throw new Error("BrandModePalette requires roles or semantic");
227
+ }
228
+ const roles = {
229
+ canvas: baseRoles.canvas,
230
+ canvasForeground: baseRoles.canvasForeground,
231
+ surface: baseRoles.surface,
232
+ surfaceForeground: baseRoles.surfaceForeground,
233
+ action: baseRoles.action,
234
+ actionForeground: baseRoles.actionForeground,
235
+ quiet: baseRoles.quiet,
236
+ quietForeground: baseRoles.quietForeground,
237
+ muted: baseRoles.muted,
238
+ mutedForeground: baseRoles.mutedForeground,
239
+ border: baseRoles.border,
240
+ ring: baseRoles.ring,
241
+ destructive: baseRoles.destructive,
242
+ destructiveForeground: baseRoles.destructiveForeground
243
+ };
244
+ if (baseRoles.input) roles.input = baseRoles.input;
245
+ const brandCh = asChannels(baseRoles.brand);
246
+ if (brandCh) roles.brand = brandCh;
247
+ const brandFg = asChannels(baseRoles.brandForeground) ?? baseRoles.actionForeground;
248
+ if (brandCh) roles.brandForeground = brandFg;
249
+ if (baseRoles.success) roles.success = baseRoles.success;
250
+ if (baseRoles.successForeground) roles.successForeground = baseRoles.successForeground;
251
+ if (baseRoles.warning) roles.warning = baseRoles.warning;
252
+ if (baseRoles.warningForeground) roles.warningForeground = baseRoles.warningForeground;
253
+ if (baseRoles.info) roles.info = baseRoles.info;
254
+ if (baseRoles.infoForeground) roles.infoForeground = baseRoles.infoForeground;
255
+ return { roles, semantic: semanticFromRoles(roles) };
256
+ }
257
+ function normalizeModes(brand, categoryBrand) {
258
+ const raw = brand.modes;
259
+ if (!raw?.light && !raw?.dark) return void 0;
260
+ const modes = {};
261
+ if (raw.light) {
262
+ const n = normalizeModePalette(raw.light, categoryBrand);
263
+ modes.light = { roles: n.roles, semantic: n.semantic };
264
+ }
265
+ if (raw.dark) {
266
+ const n = normalizeModePalette(raw.dark, categoryBrand);
267
+ modes.dark = { roles: n.roles, semantic: n.semantic };
268
+ }
269
+ if (modes.light && modes.dark) return modes;
270
+ return modes;
271
+ }
272
+ function normalizeBrandPackage(brand) {
273
+ const categoryBrand = categoryBrandMark(brand);
274
+ const modes = normalizeModes(brand, categoryBrand);
275
+ const defaultModeKey = brand.darkDefault ? "dark" : "light";
276
+ const defaultMode = modes?.[defaultModeKey] ?? modes?.light ?? modes?.dark;
277
+ let primaryPalette;
278
+ if (defaultMode?.roles || defaultMode?.semantic) {
279
+ primaryPalette = {};
280
+ if (defaultMode.roles) primaryPalette.roles = defaultMode.roles;
281
+ if (defaultMode.semantic) primaryPalette.semantic = defaultMode.semantic;
282
+ } else {
283
+ primaryPalette = { semantic: brand.semantic };
284
+ if (brand.roles) primaryPalette.roles = brand.roles;
285
+ }
286
+ const { roles, semantic } = normalizeModePalette(primaryPalette, categoryBrand);
287
+ const looseRecipe = brand.recipe;
288
+ const radii = normalizeRadii(looseRecipe);
289
+ const elevationTokens = normalizeElevation(looseRecipe);
290
+ const recipe = {
291
+ buttonDefault: looseRecipe.buttonDefault,
292
+ density: looseRecipe.density ?? "comfortable",
293
+ badgeDefault: normalizeBadgeDefault(looseRecipe.badgeDefault),
294
+ radii,
295
+ elevationTokens
296
+ };
297
+ if (looseRecipe.primaryStrokeGradient) {
298
+ recipe.primaryStrokeGradient = looseRecipe.primaryStrokeGradient;
299
+ }
300
+ if (looseRecipe.outlineBorder) {
301
+ recipe.outlineBorder = looseRecipe.outlineBorder;
302
+ }
303
+ const out = {
304
+ ...brand,
305
+ roles,
306
+ semantic,
307
+ recipe
308
+ };
309
+ if (modes) out.modes = modes;
310
+ else delete out.modes;
311
+ return out;
312
+ }
313
+ function isDualModeBrand(brand) {
314
+ return Boolean(brand.modes?.light?.semantic && brand.modes?.dark?.semantic);
315
+ }
316
+
317
+ // src/brand-package/emit-css.ts
318
+ function cssFontStack(stack) {
319
+ return (withNearestRegistryFont(stack) ?? stack).replace(/'/g, '"');
320
+ }
321
+ var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
322
+ "ui-sans-serif",
323
+ "ui-serif",
324
+ "ui-monospace",
325
+ "system-ui",
326
+ "sans-serif",
327
+ "serif",
328
+ "monospace",
329
+ "-apple-system",
330
+ "BlinkMacSystemFont"
331
+ ]);
332
+ function withCjkTail(stack) {
333
+ const families = stack.split(",").map((f) => f.trim());
334
+ const cut = families.findIndex((f) => GENERIC_FAMILIES.has(f.replace(/"/g, "")));
335
+ if (cut === -1 || families.some((f) => f.includes("MiSans"))) return stack;
336
+ const cjk = ['var(--font-misans, "MiSans")', '"PingFang SC"'];
337
+ return [...families.slice(0, cut), ...cjk, ...families.slice(cut)].join(", ");
338
+ }
339
+ function motionVars(motion) {
340
+ if (!motion) return [];
341
+ const lines = [];
342
+ const curves = [
343
+ ["easeOut", "ease-out"],
344
+ ["easeInOut", "ease-in-out"],
345
+ ["easeSpring", "ease-spring"]
346
+ ];
347
+ for (const [key, name] of curves) {
348
+ const value = motion[key];
349
+ if (typeof value !== "string" || !value.trim()) continue;
350
+ lines.push(` --${name}: ${value};`);
351
+ lines.push(` --motion-${name}: ${value};`);
352
+ }
353
+ const durations = [
354
+ ["micro", "micro"],
355
+ ["flow", "flow"],
356
+ ["reveal", "reveal"],
357
+ ["cinematic", "cinematic"]
358
+ ];
359
+ for (const [key, name] of durations) {
360
+ const value = motion[key];
361
+ if (typeof value !== "number" || !Number.isFinite(value)) continue;
362
+ lines.push(` --duration-${name}: ${value}ms;`);
363
+ lines.push(` --motion-duration-${name}: ${value}ms;`);
364
+ }
365
+ return lines.length ? [``, ` /* Motion */`, ...lines] : [];
366
+ }
367
+ function spacingVars(spacing) {
368
+ if (!spacing) return [];
369
+ const lines = [];
370
+ const steps = [
371
+ ["xs", "xs"],
372
+ ["sm", "sm"],
373
+ ["md", "md"],
374
+ ["lg", "lg"],
375
+ ["xl", "xl"],
376
+ ["2xl", "2xl"]
377
+ ];
378
+ for (const [key, name] of steps) {
379
+ const value = spacing[key];
380
+ if (typeof value !== "string" || !value.trim()) continue;
381
+ lines.push(` --space-source-${name}: ${value};`);
382
+ }
383
+ return lines.length ? [``, ` /* Spacing */`, ...lines] : [];
384
+ }
385
+ function recipeVars(recipe) {
386
+ const radii = recipe.radii;
387
+ const elev = recipe.elevationTokens;
388
+ const lines = [
389
+ ` /* Shape slots */`,
390
+ ` --btn-default-radius: ${radii.button};`,
391
+ ` --radius-button: ${radii.button};`,
392
+ ` --radius-buttons: ${radii.button};`,
393
+ // A step on the size scale, not the button role.
394
+ //
395
+ // Aliasing it straight to `radii.button` is fine while a language's buttons
396
+ // are modestly rounded — five of the seven are 4–8px and never noticed. It
397
+ // breaks for the two whose buttons are pills: GSAP emitted
398
+ // `--radius-md: 100px` and Vanta 999px, and forty-two files read that step
399
+ // for panels and surfaces. The Combobox popover under GSAP came out as a
400
+ // lozenge, which is what a 100px radius does to a 260px-wide panel.
401
+ //
402
+ // `min()` keeps every language whose button is the tighter of the two
403
+ // exactly where it was, and stops a pill from escaping its role. A pill is
404
+ // a control decision; it has no business setting the radius of a surface.
405
+ ` --radius-md: min(${radii.button}, ${radii.card});`,
406
+ ` --radius-card: ${radii.card};`,
407
+ ` --radius-lg: ${radii.card};`,
408
+ ` --radius-badge: ${radii.badge ?? "9999px"};`,
409
+ ` --badge-default-radius: ${radii.badge ?? "9999px"};`,
410
+ ` --radius-inputs: ${radii.input ?? radii.button};`,
411
+ ` --input-radius: ${radii.input ?? radii.button};`,
412
+ ` --radius-pill: ${radii.pill ?? "9999px"};`,
413
+ ``,
414
+ ` /* Free elevation (carrier-provided CSS shadows) */`,
415
+ ` --elevation-card: ${elev.card};`,
416
+ ` --elevation-control: ${elev.control ?? "0 0 #0000"};`,
417
+ ` --elevation-raised: ${elev.raised ?? elev.card};`,
418
+ // --elevation-*, not --shadow-*. The theme block maps `--shadow-md` to
419
+ // `var(--elevation-md)` and inlines that into `.shadow-md`, so a skin that
420
+ // set --shadow-md was writing to the alias while the utility read the
421
+ // source. Every language's shadows were therefore byte-identical in the
422
+ // browser — measured across all seven with getComputedStyle — while the
423
+ // token files disagreed convincingly. Setting the source is what a brand
424
+ // switch needs; the alias takes care of itself.
425
+ ` --elevation-xs: ${elev.control ?? "0 0 #0000"};`,
426
+ ` --elevation-sm: ${elev.card};`,
427
+ ` --elevation-md: ${elev.raised ?? elev.card};`,
428
+ ` --elevation-lg: ${elev.raised ?? elev.card};`,
429
+ ` --btn-default-shadow: 0 0 #0000;`
430
+ ];
431
+ switch (recipe.buttonDefault) {
432
+ case "outline": {
433
+ const edge = recipe.outlineBorder ?? "hsl(var(--foreground))";
434
+ lines.push(" --btn-default-bg: transparent;");
435
+ lines.push(" --btn-default-fg: hsl(var(--foreground));");
436
+ lines.push(" --btn-default-border-width: 1px;");
437
+ lines.push(" --btn-default-border: transparent;");
438
+ lines.push(` --btn-default-stroke-gradient: linear-gradient(${edge}, ${edge});`);
439
+ lines.push(" --btn-default-hover-bg: hsl(var(--foreground) / 0.06);");
440
+ break;
441
+ }
442
+ case "gradient-stroke": {
443
+ const grad = recipe.primaryStrokeGradient ?? "linear-gradient(135deg, hsl(var(--primary)), color-mix(in srgb, hsl(var(--primary)) 55%, white))";
444
+ lines.push(" --btn-default-bg: transparent;");
445
+ lines.push(" --btn-default-fg: hsl(var(--foreground));");
446
+ lines.push(" --btn-default-border-width: 1.5px;");
447
+ lines.push(" --btn-default-border: transparent;");
448
+ lines.push(` --btn-default-stroke-gradient: ${grad};`);
449
+ lines.push(" --btn-default-hover-bg: hsl(var(--primary) / 0.08);");
450
+ break;
451
+ }
452
+ default:
453
+ lines.push(" --btn-default-bg: hsl(var(--primary));");
454
+ lines.push(" --btn-default-fg: hsl(var(--primary-foreground));");
455
+ lines.push(" --btn-default-border-width: 0px;");
456
+ lines.push(" --btn-default-border: transparent;");
457
+ lines.push(" --btn-default-stroke-gradient: linear-gradient(transparent, transparent);");
458
+ lines.push(
459
+ " --btn-default-hover-bg: color-mix(in srgb, hsl(var(--primary)) 90%, transparent);"
460
+ );
461
+ break;
462
+ }
463
+ const badgeMode = recipe.badgeDefault ?? "match-action";
464
+ if (badgeMode === "outline") {
465
+ const edge = recipe.outlineBorder ?? "hsl(var(--border))";
466
+ lines.push(" --badge-default-bg: transparent;");
467
+ lines.push(" --badge-default-fg: hsl(var(--foreground));");
468
+ lines.push(` --badge-default-border: ${edge};`);
469
+ lines.push(" --badge-default-hover-bg: hsl(var(--foreground) / 0.06);");
470
+ } else if (badgeMode === "muted") {
471
+ lines.push(" --badge-default-bg: hsl(var(--secondary));");
472
+ lines.push(" --badge-default-fg: hsl(var(--secondary-foreground));");
473
+ lines.push(" --badge-default-border: transparent;");
474
+ lines.push(" --badge-default-hover-bg: color-mix(in srgb, hsl(var(--secondary)) 90%, white);");
475
+ } else if (badgeMode === "brand") {
476
+ lines.push(" --badge-default-bg: hsl(var(--brand-mark, var(--accent)));");
477
+ lines.push(
478
+ " --badge-default-fg: hsl(var(--brand-mark-foreground, var(--accent-foreground)));"
479
+ );
480
+ lines.push(" --badge-default-border: transparent;");
481
+ lines.push(
482
+ " --badge-default-hover-bg: color-mix(in srgb, hsl(var(--brand-mark, var(--accent))) 85%, transparent);"
483
+ );
484
+ } else if (recipe.buttonDefault === "outline" || recipe.buttonDefault === "gradient-stroke") {
485
+ const edge = recipe.outlineBorder ?? "hsl(var(--foreground))";
486
+ lines.push(" --badge-default-bg: transparent;");
487
+ lines.push(" --badge-default-fg: hsl(var(--foreground));");
488
+ lines.push(` --badge-default-border: ${edge};`);
489
+ lines.push(" --badge-default-hover-bg: hsl(var(--foreground) / 0.06);");
490
+ } else {
491
+ lines.push(" --badge-default-bg: hsl(var(--primary));");
492
+ lines.push(" --badge-default-fg: hsl(var(--primary-foreground));");
493
+ lines.push(" --badge-default-border: transparent;");
494
+ lines.push(
495
+ " --badge-default-hover-bg: color-mix(in srgb, hsl(var(--primary)) 80%, transparent);"
496
+ );
497
+ }
498
+ if (recipe.density === "compact") {
499
+ lines.push(" --btn-default-padding-y: 0.5rem;");
500
+ lines.push(" --btn-default-padding-x: 0.875rem;");
501
+ lines.push(" --control-height-tiny: 1.25rem;");
502
+ lines.push(" --control-height-sm: 1.75rem;");
503
+ lines.push(" --control-height-md: 2rem;");
504
+ lines.push(" --control-height-lg: 2.5rem;");
505
+ lines.push(" --control-height-icon-sm: 1.5rem;");
506
+ lines.push(" --control-height-icon-md: 1.75rem;");
507
+ lines.push(" --control-height-icon-lg: 2rem;");
508
+ lines.push(" --control-font-size-md: 0.8125rem;");
509
+ } else if (recipe.density === "spacious") {
510
+ lines.push(" --btn-default-padding-y: 0.875rem;");
511
+ lines.push(" --btn-default-padding-x: 1.5rem;");
512
+ lines.push(" --control-height-tiny: 1.75rem;");
513
+ lines.push(" --control-height-sm: 2.25rem;");
514
+ lines.push(" --control-height-md: 2.75rem;");
515
+ lines.push(" --control-height-lg: 3.25rem;");
516
+ lines.push(" --control-height-icon-sm: 2rem;");
517
+ lines.push(" --control-height-icon-md: 2.25rem;");
518
+ lines.push(" --control-height-icon-lg: 2.5rem;");
519
+ }
520
+ return lines;
521
+ }
522
+ function isServableFaceUrl(url) {
523
+ return url.startsWith("/") && !url.startsWith("//");
524
+ }
525
+ function emitFontFaces(faces) {
526
+ const servable = faces?.filter((face) => face.src.every((s) => isServableFaceUrl(s.url)));
527
+ if (!servable?.length) return [];
528
+ const out = ["/* Brand font faces */"];
529
+ for (const face of servable) {
530
+ const src = face.src.map((s) => {
531
+ const fmt = s.format ? ` format("${s.format}")` : "";
532
+ return `url("${s.url}")${fmt}`;
533
+ }).join(", ");
534
+ out.push("@font-face {");
535
+ out.push(` font-family: "${face.family}";`);
536
+ out.push(` src: ${src};`);
537
+ if (face.weight != null) out.push(` font-weight: ${face.weight};`);
538
+ if (face.style) out.push(` font-style: ${face.style};`);
539
+ out.push(` font-display: ${face.display ?? "swap"};`);
540
+ if (face.unicodeRange) out.push(` unicode-range: ${face.unicodeRange};`);
541
+ out.push("}");
542
+ out.push("");
543
+ }
544
+ return out;
545
+ }
546
+ function emitGlobalSkinSelector(brandId, darkDefault, root = "html") {
547
+ if (darkDefault) {
548
+ return `:root,
549
+ .dark,
550
+ ${root}[data-brand="${brandId}"] {`;
551
+ }
552
+ return `:root,
553
+ ${root}[data-brand="${brandId}"] {`;
554
+ }
555
+ function emitLightModeSelector(brandId, mode, root = "html") {
556
+ if (mode === "scoped") return `${root}[data-brand="${brandId}"] {`;
557
+ return `:root,
558
+ ${root}[data-brand="${brandId}"] {`;
559
+ }
560
+ function emitDarkModeSelector(brandId, mode, root = "html") {
561
+ if (mode === "scoped") return `${root}.dark[data-brand="${brandId}"] {`;
562
+ return `.dark,
563
+ ${root}.dark[data-brand="${brandId}"] {`;
564
+ }
565
+ function neutralRamp(s, r) {
566
+ const hsl = (triple) => `hsl(${triple})`;
567
+ const anchors = [
568
+ [1, hsl(r?.canvas ?? s.background)],
569
+ [2, hsl(r?.surface ?? s.card)],
570
+ [7, hsl(r?.border ?? s.border)],
571
+ [11, hsl(r?.mutedForeground ?? s.mutedForeground)],
572
+ [12, hsl(r?.canvasForeground ?? s.foreground)]
573
+ ];
574
+ const lines = [``, ` /* \u2500\u2500 Neutral ramp, anchored on this language's roles \u2500\u2500 */`];
575
+ for (let step = 1; step <= 12; step++) {
576
+ const exact = anchors.find(([n]) => n === step);
577
+ if (exact) {
578
+ lines.push(` --neutral-${step}: ${exact[1]};`);
579
+ continue;
580
+ }
581
+ const lower = [...anchors].reverse().find(([n]) => n < step);
582
+ const upper = anchors.find(([n]) => n > step);
583
+ if (!lower || !upper) continue;
584
+ const t = Math.round((step - lower[0]) / (upper[0] - lower[0]) * 100);
585
+ lines.push(` --neutral-${step}: color-mix(in oklab, ${upper[1]} ${t}%, ${lower[1]});`);
586
+ }
587
+ return lines;
588
+ }
589
+ function emitColorVars(s, r) {
590
+ const roleLines = [
591
+ ` /* \u2500\u2500 Color roles (carrier) \u2500\u2500 */`,
592
+ ` --role-canvas: ${r?.canvas ?? s.background};`,
593
+ ` --role-canvas-fg: ${r?.canvasForeground ?? s.foreground};`,
594
+ ` --role-surface: ${r?.surface ?? s.card};`,
595
+ ` --role-surface-fg: ${r?.surfaceForeground ?? s.cardForeground};`,
596
+ ` --role-action: ${r?.action ?? s.primary};`,
597
+ ` --role-action-fg: ${r?.actionForeground ?? s.primaryForeground};`,
598
+ ` --role-quiet: ${r?.quiet ?? s.secondary};`,
599
+ ` --role-quiet-fg: ${r?.quietForeground ?? s.secondaryForeground};`,
600
+ ` --role-muted: ${r?.muted ?? s.muted};`,
601
+ ` --role-muted-fg: ${r?.mutedForeground ?? s.mutedForeground};`,
602
+ ` --role-border: ${r?.border ?? s.border};`,
603
+ ` --role-input: ${r?.input ?? s.input ?? s.border};`,
604
+ ` --role-ring: ${r?.ring ?? s.ring};`
605
+ ];
606
+ if (r?.brand) {
607
+ roleLines.push(` --role-brand: ${r.brand};`);
608
+ roleLines.push(
609
+ ` --role-brand-fg: ${r.brandForeground ?? r.actionForeground ?? s.primaryForeground};`
610
+ );
611
+ roleLines.push(` --brand-mark: ${r.brand};`);
612
+ roleLines.push(
613
+ ` --brand-mark-foreground: ${r.brandForeground ?? r.actionForeground ?? s.primaryForeground};`
614
+ );
615
+ }
616
+ const semantic = [
617
+ ``,
618
+ ` /* \u2500\u2500 shadcn bridge (primary = action CTA) \u2500\u2500 */`,
619
+ ` --background: ${s.background};`,
620
+ ` --foreground: ${s.foreground};`,
621
+ ` --card: ${s.card};`,
622
+ ` --card-foreground: ${s.cardForeground};`,
623
+ ` --popover: ${s.popover};`,
624
+ ` --popover-foreground: ${s.popoverForeground};`,
625
+ ` --primary: ${s.primary};`,
626
+ ` --primary-foreground: ${s.primaryForeground};`,
627
+ ` --secondary: ${s.secondary};`,
628
+ ` --secondary-foreground: ${s.secondaryForeground};`,
629
+ ` --muted: ${s.muted};`,
630
+ ` --muted-foreground: ${s.mutedForeground};`,
631
+ ` --accent: ${s.accent};`,
632
+ ` --accent-foreground: ${s.accentForeground};`,
633
+ ` --destructive: ${s.destructive};`,
634
+ ` --destructive-foreground: ${s.destructiveForeground};`,
635
+ ` --border: ${s.border};`,
636
+ // Field stroke, not field fill: --input reaches the DOM only through
637
+ // `border-input`. A language that omits it inherits the hairline colour,
638
+ // which is always a visible boundary — writing the surface colour here
639
+ // drew the outline in the same colour as what sits behind it.
640
+ ` --input: ${s.input ?? s.border};`,
641
+ ` --ring: ${s.ring};`
642
+ ];
643
+ if (s.success) semantic.push(` --success: ${s.success};`);
644
+ if (s.successForeground) semantic.push(` --success-foreground: ${s.successForeground};`);
645
+ if (s.warning) semantic.push(` --warning: ${s.warning};`);
646
+ if (s.warningForeground) semantic.push(` --warning-foreground: ${s.warningForeground};`);
647
+ if (s.info) semantic.push(` --info: ${s.info};`);
648
+ if (s.infoForeground) semantic.push(` --info-foreground: ${s.infoForeground};`);
649
+ semantic.push(
650
+ ` --sidebar: ${s.card};`,
651
+ ` --sidebar-foreground: ${s.foreground};`,
652
+ ` --sidebar-primary: ${s.primary};`,
653
+ ` --sidebar-primary-foreground: ${s.primaryForeground};`,
654
+ ` --sidebar-accent: ${s.accent};`,
655
+ ` --sidebar-accent-foreground: ${s.accentForeground};`,
656
+ ` --sidebar-border: ${s.border};`,
657
+ ` --sidebar-ring: ${s.ring};`,
658
+ // The identity aliases, taken over by the language.
659
+ //
660
+ // These were the one family a Brand Package did not reach: --brand-primary
661
+ // and --brand-accent are declared in styles.css as Nebutra's own #0033FE and
662
+ // #0BF1C3, and no skin overrode them. Switching to Linear moved three
663
+ // thousand semantic usages and left seventy-five sitting in Nebutra's cyan —
664
+ // a page that reads as half-switched rather than as another language.
665
+ //
666
+ // A language has exactly two vivid hues to offer, and they are `roles.brand`
667
+ // — the identity mark — and `roles.action`, the product fill. Notion is blue
668
+ // on black, Raycast coral behind near-white chrome, Stripe indigo on
669
+ // midnight. --brand-primary takes the mark, --brand-accent the action, and a
670
+ // language declaring no separate mark simply shows one hue in both, which is
671
+ // true of it rather than a gap to paper over.
672
+ //
673
+ // NOT `semantic.accent`: that is the shadcn hover-surface role, and mapping
674
+ // it here produced --brand-accent: 220 14% 96% for Linear and 70 5% 25% for
675
+ // GSAP. The glow elevations would have tinted themselves with a hover grey.
676
+ // Same slot-versus-role confusion as --radius-md and --input, one family out.
677
+ //
678
+ // --brand-tertiary falls back to the accent rather than inventing a third
679
+ // hue: two honest hues beat three where one is made up.
680
+ //
681
+ // Emitted as complete colours, not channel triples. --brand-accent is read
682
+ // inside `color-mix(in srgb, var(--brand-accent) 8%, transparent)` by the
683
+ // glow elevations, and a bare triple there voids the whole declaration —
684
+ // silently, which is the failure this codebase keeps relearning.
685
+ ` --brand-primary: hsl(${r?.brand ?? s.primary});`,
686
+ ` --brand-accent: hsl(${s.primary});`,
687
+ ` --brand-accent-foreground: hsl(${s.primaryForeground});`,
688
+ ` --brand-tertiary: hsl(${s.primary});`,
689
+ ` --brand-gradient: hsl(var(--primary));`,
690
+ ` --brand-gradient-reverse: hsl(var(--primary));`,
691
+ ` --brand-gradient-vertical: hsl(var(--primary));`,
692
+ ` --brand-gradient-radial: hsl(var(--primary));`
693
+ );
694
+ return [...roleLines, ...semantic, ...neutralRamp(s, r)];
695
+ }
696
+ function emitBrandCss(brand, options = {}) {
697
+ const mode = options.mode ?? "global";
698
+ const root = options.root ?? "html";
699
+ const b = normalizeBrandPackage(brand);
700
+ const t = b.typography;
701
+ const dual = isDualModeBrand(b);
702
+ const parts = [
703
+ `/**`,
704
+ ` * Brand carrier skin: ${b.name} (${b.id}) v${b.version}`,
705
+ ` * darkDefault=${b.darkDefault} dualMode=${dual} button=${b.recipe.buttonDefault}`,
706
+ ` * fonts=${t.faces?.length ?? 0} mode=${mode}`,
707
+ ` * Contract: roles.action \u2192 --primary; roles.brand \u2192 --brand-mark (never default CTA)`,
708
+ ` */`,
709
+ ``,
710
+ ...emitFontFaces(t.faces)
711
+ ];
712
+ const fontSans = withCjkTail(cssFontStack(t.fontSans));
713
+ const fontDisplay = withCjkTail(cssFontStack(t.fontDisplay ?? t.fontSans));
714
+ const typeLines = [
715
+ ` --font-sans: ${fontSans};`,
716
+ // The CJK-locale rule in base.css sets body's font-family from --font-cn,
717
+ // which sits above --font-sans in specificity. A skin that left it alone
718
+ // was silently overruled on every Chinese page: picking a design language
719
+ // changed nothing about the type. The tail is already in fontSans, so the
720
+ // two stacks are the same stack.
721
+ ` --font-cn: ${fontSans};`,
722
+ ` --font-heading: ${fontDisplay};`,
723
+ ` --font-display: ${fontDisplay};`
724
+ ];
725
+ if (t.fontMono) typeLines.push(` --font-mono: ${cssFontStack(t.fontMono)};`);
726
+ if (t.headingWeight != null) {
727
+ typeLines.push(` --font-weight-heading: ${t.headingWeight};`);
728
+ }
729
+ const cats = b.extensions?.categories;
730
+ const decorative = b.extensions?.decorative;
731
+ const extLines = [];
732
+ if (cats) {
733
+ for (const [key, value] of Object.entries(cats)) {
734
+ const safe = key.replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
735
+ extLines.push(` --brand-category-${safe}: ${value};`);
736
+ }
737
+ }
738
+ if (decorative) {
739
+ for (const [key, value] of Object.entries(decorative)) {
740
+ const safe = key.replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
741
+ extLines.push(` --brand-decorative-${safe}: ${value};`);
742
+ }
743
+ }
744
+ const sharedChrome = [
745
+ ``,
746
+ ` /* Recipe (action language + free elev/radii) */`,
747
+ ...recipeVars(b.recipe),
748
+ ``,
749
+ ` /* Typography */`,
750
+ ...typeLines,
751
+ ...motionVars(b.motion),
752
+ ...spacingVars(b.spacing),
753
+ ...extLines.length ? ["", " /* Taxonomy / decorative (not product CTA) */", ...extLines] : []
754
+ ];
755
+ if (dual && b.modes?.light?.semantic && b.modes?.dark?.semantic) {
756
+ parts.push(emitLightModeSelector(b.id, mode, root));
757
+ parts.push(
758
+ ...emitColorVars(b.modes.light.semantic, b.modes.light.roles),
759
+ ...sharedChrome,
760
+ `}`,
761
+ ``
762
+ );
763
+ parts.push(emitDarkModeSelector(b.id, mode, root));
764
+ parts.push(...emitColorVars(b.modes.dark.semantic, b.modes.dark.roles), `}`, ``);
765
+ } else {
766
+ const selector = mode === "scoped" ? `${root}[data-brand="${b.id}"] {` : emitGlobalSkinSelector(b.id, b.darkDefault, root);
767
+ parts.push(selector);
768
+ parts.push(...emitColorVars(b.semantic, b.roles), ...sharedChrome, `}`, ``);
769
+ }
770
+ return parts.join("\n");
771
+ }
772
+
773
+ // src/brand-package/apply-brand.ts
774
+ var BRAND_STYLE_ELEMENT_ID = "nebutra-brand-skin";
775
+ var BRAND_STORAGE_KEY = "nebutra-brand-package";
776
+ function targetDoc(doc) {
777
+ if (doc) return doc;
778
+ if (typeof document === "undefined") return null;
779
+ return document;
780
+ }
781
+ function applyBrandCss(css, brandId, options = {}) {
782
+ const d = targetDoc(options.doc);
783
+ if (!d) return;
784
+ let el = d.getElementById(BRAND_STYLE_ELEMENT_ID);
785
+ if (!el) {
786
+ el = d.createElement("style");
787
+ el.id = BRAND_STYLE_ELEMENT_ID;
788
+ el.setAttribute("data-nebutra-brand", brandId ?? "custom");
789
+ d.head.appendChild(el);
790
+ }
791
+ el.textContent = css;
792
+ if (brandId) {
793
+ d.documentElement.dataset.brand = brandId;
794
+ }
795
+ }
796
+ function applyBrandPackage(brand, options = {}) {
797
+ const css = emitBrandCss(brand);
798
+ applyBrandCss(css, brand.id, options);
799
+ if (options.persist && typeof localStorage !== "undefined") {
800
+ try {
801
+ localStorage.setItem(BRAND_STORAGE_KEY, JSON.stringify(brand));
802
+ } catch {
803
+ }
804
+ }
805
+ }
806
+ function clearBrand(options = {}) {
807
+ const d = targetDoc(options.doc);
808
+ if (!d) return;
809
+ d.getElementById(BRAND_STYLE_ELEMENT_ID)?.remove();
810
+ delete d.documentElement.dataset.brand;
811
+ if (options.persist && typeof localStorage !== "undefined") {
812
+ try {
813
+ localStorage.removeItem(BRAND_STORAGE_KEY);
814
+ } catch {
815
+ }
816
+ }
817
+ }
818
+ function restorePersistedBrand(options = {}) {
819
+ if (typeof localStorage === "undefined") return null;
820
+ try {
821
+ const raw = localStorage.getItem(BRAND_STORAGE_KEY);
822
+ if (!raw) return null;
823
+ const brand = JSON.parse(raw);
824
+ if (!brand?.id || !brand?.semantic || !brand?.recipe) return null;
825
+ applyBrandPackage(brand, { ...options, persist: false });
826
+ return brand;
827
+ } catch {
828
+ return null;
829
+ }
830
+ }
831
+ function getActiveBrandId(doc) {
832
+ const d = targetDoc(doc);
833
+ return d?.documentElement.dataset.brand ?? null;
834
+ }
835
+
836
+ // src/brand-package/use-brand.ts
837
+ function useBrand(options = {}) {
838
+ const { autoRestore = false, persist = true } = options;
839
+ const [brand, setBrand] = useState(null);
840
+ const [brandId, setBrandId] = useState(null);
841
+ useEffect(() => {
842
+ if (!autoRestore) return;
843
+ const restored = restorePersistedBrand({ persist: false });
844
+ if (restored) {
845
+ setBrand(restored);
846
+ setBrandId(restored.id);
847
+ } else {
848
+ setBrandId(getActiveBrandId());
849
+ }
850
+ }, [autoRestore]);
851
+ const apply = useCallback(
852
+ (next) => {
853
+ applyBrandPackage(next, { persist });
854
+ setBrand(next);
855
+ setBrandId(next.id);
856
+ },
857
+ [persist]
858
+ );
859
+ const clear = useCallback(() => {
860
+ clearBrand({ persist });
861
+ setBrand(null);
862
+ setBrandId(null);
863
+ }, [persist]);
864
+ const restore = useCallback(() => {
865
+ const restored = restorePersistedBrand({ persist: false });
866
+ if (restored) {
867
+ setBrand(restored);
868
+ setBrandId(restored.id);
869
+ }
870
+ return restored;
871
+ }, []);
872
+ return { brand, brandId, apply, clear, restore };
873
+ }
874
+ function ensureIframeDoc(iframe) {
875
+ if (!iframe) return null;
876
+ try {
877
+ return iframe.contentDocument;
878
+ } catch {
879
+ return null;
880
+ }
881
+ }
882
+ function injectBaseStyles(doc, hrefs) {
883
+ if (!hrefs?.length) return;
884
+ for (const href of hrefs) {
885
+ const id = `nebutra-base-${hashHref(href)}`;
886
+ if (doc.getElementById(id)) continue;
887
+ const link = doc.createElement("link");
888
+ link.id = id;
889
+ link.rel = "stylesheet";
890
+ link.href = href;
891
+ doc.head.appendChild(link);
892
+ }
893
+ }
894
+ function hashHref(href) {
895
+ let h = 0;
896
+ for (let i = 0; i < href.length; i++) h = h * 31 + href.charCodeAt(i) | 0;
897
+ return Math.abs(h).toString(36);
898
+ }
899
+ function useBrandIframePreview(options = {}) {
900
+ const iframeRef = useRef(null);
901
+ const [brand, setBrand] = useState(null);
902
+ const optsRef = useRef(options);
903
+ optsRef.current = options;
904
+ const apply = useCallback((next) => {
905
+ const doc = ensureIframeDoc(iframeRef.current);
906
+ if (!doc?.documentElement) {
907
+ setBrand(next);
908
+ return;
909
+ }
910
+ injectBaseStyles(doc, optsRef.current.baseStylesheetHrefs);
911
+ applyBrandPackage(next, { doc, persist: false });
912
+ setBrand(next);
913
+ optsRef.current.onApplied?.(next);
914
+ }, []);
915
+ const clear = useCallback(() => {
916
+ const doc = ensureIframeDoc(iframeRef.current);
917
+ if (doc) clearBrand({ doc, persist: false });
918
+ setBrand(null);
919
+ optsRef.current.onApplied?.(null);
920
+ }, []);
921
+ const writePreviewDocument = useCallback((next, bodyHtml = "") => {
922
+ const iframe = iframeRef.current;
923
+ if (!iframe) return;
924
+ const css = emitBrandCss(next);
925
+ const links = (optsRef.current.baseStylesheetHrefs ?? []).map((href) => `<link rel="stylesheet" href="${href}" />`).join("\n");
926
+ const html = `<!DOCTYPE html>
927
+ <html lang="en" data-brand="${next.id}" class="${next.darkDefault ? "dark" : ""}">
928
+ <head>
929
+ <meta charset="utf-8" />
930
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
931
+ ${links}
932
+ ${optsRef.current.headHtml ?? ""}
933
+ <style id="nebutra-brand-skin">${css}</style>
934
+ <style>
935
+ html, body { margin: 0; min-height: 100%; }
936
+ body {
937
+ font-family: var(--font-sans, system-ui, sans-serif);
938
+ background: hsl(var(--background));
939
+ color: hsl(var(--foreground));
940
+ }
941
+ </style>
942
+ </head>
943
+ <body class="${optsRef.current.bodyClassName ?? ""} bg-background text-foreground">
944
+ ${bodyHtml}
945
+ </body>
946
+ </html>`;
947
+ iframe.srcdoc = html;
948
+ setBrand(next);
949
+ optsRef.current.onApplied?.(next);
950
+ }, []);
951
+ return { iframeRef, brand, apply, clear, writePreviewDocument };
952
+ }
953
+ function applyBrandToIframe(iframe, brand, options = {}) {
954
+ const doc = iframe.contentDocument;
955
+ if (!doc) return;
956
+ injectBaseStyles(doc, options.baseStylesheetHrefs);
957
+ applyBrandPackage(brand, { ...options, doc, persist: false });
958
+ }
7
959
  export {
8
960
  applyBrandToIframe,
9
961
  useBrand,