@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.
- package/CHANGELOG.md +16 -0
- package/brands/README.md +66 -0
- package/brands/cosmos/DESIGN.md +356 -0
- package/brands/cosmos/brand.json +209 -0
- package/brands/cosmos/refero-tokens.json +312 -0
- package/brands/gsap/brand.json +1 -27
- package/brands/linear/DESIGN.md +478 -0
- package/brands/linear/brand.json +2 -19
- package/brands/notion/brand.json +2 -28
- package/brands/raycast/brand.json +2 -17
- package/brands/stripe/brand.json +1 -16
- package/brands/vanta/brand.json +1 -38
- package/brands/vercel/brand.json +1 -18
- package/dist/brand-package/index.d.ts +13 -7
- package/dist/brand-package/index.js +2290 -34
- package/dist/brand-package/index.js.map +1 -1
- package/dist/brand-package/use-brand.d.ts +1 -1
- package/dist/brand-package/use-brand.js +957 -5
- package/dist/brand-package/use-brand.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2417 -22
- package/dist/index.js.map +1 -1
- package/dist/{use-brand-C8d4DPqE.d.ts → use-brand-ClmvK_lC.d.ts} +19 -3
- package/dist/values.d.ts +1192 -0
- package/dist/values.js +518 -0
- package/dist/values.js.map +1 -0
- package/package.json +16 -4
- package/recipe.css +36 -2
- package/skins/cosmos.css +239 -0
- package/skins/gsap.css +32 -20
- package/skins/linear.css +54 -17
- package/skins/notion.css +36 -25
- package/skins/raycast.css +37 -19
- package/skins/stripe.css +34 -15
- package/skins/vanta.css +34 -30
- package/skins/vercel.css +55 -19
- package/styles.css +492 -149
- package/dist/chunk-ALJTP5HL.js +0 -881
- package/dist/chunk-ALJTP5HL.js.map +0 -1
- package/dist/chunk-RGUJQOLH.js +0 -1582
- package/dist/chunk-RGUJQOLH.js.map +0 -1
|
@@ -1,9 +1,961 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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,
|