@nebutra/tokens 0.1.2 → 0.1.3
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/brands/gsap/brand.json +17 -64
- package/brands/linear/brand.json +18 -57
- package/brands/notion/brand.json +18 -62
- package/brands/raycast/brand.json +17 -64
- package/brands/stripe/brand.json +16 -72
- package/brands/vanta/brand.json +16 -63
- package/brands/vercel/brand.json +16 -50
- package/dist/brand-package/index.d.ts +123 -0
- package/dist/brand-package/index.js +61 -0
- package/dist/brand-package/index.js.map +1 -0
- package/dist/brand-package/use-brand.d.ts +2 -0
- package/dist/brand-package/use-brand.js +12 -0
- package/dist/brand-package/use-brand.js.map +1 -0
- package/dist/chunk-ALJTP5HL.js +881 -0
- package/dist/chunk-ALJTP5HL.js.map +1 -0
- package/dist/chunk-RGUJQOLH.js +1582 -0
- package/dist/chunk-RGUJQOLH.js.map +1 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +187 -0
- package/dist/index.js.map +1 -0
- package/dist/use-brand-C8d4DPqE.d.ts +371 -0
- package/package.json +34 -9
- package/recipe.css +322 -1
- package/skins/README.md +2 -4
- package/skins/gsap.css +31 -119
- package/skins/linear.css +33 -117
- package/skins/notion.css +27 -117
- package/skins/raycast.css +32 -119
- package/skins/stripe.css +27 -129
- package/skins/vanta.css +27 -122
- package/skins/vercel.css +30 -111
- package/styles.css +388 -113
- package/.turbo/turbo-build.log +0 -8
- package/.turbo/turbo-typecheck.log +0 -4
- package/AGENTS.md +0 -45
- package/DESIGN.md +0 -357
- package/scripts/compile-brand-run.ts +0 -79
- package/scripts/compile-brand.mjs +0 -25
- package/scripts/emit-skins-run.ts +0 -62
- package/scripts/emit-skins.mjs +0 -31
- package/scripts/sync-styles.mjs +0 -29
- package/src/brand-package/__tests__/compile-refero.test.ts +0 -427
- package/src/brand-package/__tests__/emit-css.test.ts +0 -130
- package/src/brand-package/apply-brand.ts +0 -92
- package/src/brand-package/compile-helpers.ts +0 -170
- package/src/brand-package/compile-refero.ts +0 -69
- package/src/brand-package/emit-css.ts +0 -468
- package/src/brand-package/hex-to-hsl.ts +0 -100
- package/src/brand-package/index.ts +0 -71
- package/src/brand-package/infer-recipe.ts +0 -198
- package/src/brand-package/normalize.ts +0 -295
- package/src/brand-package/presets/context.ts +0 -17
- package/src/brand-package/presets/generic.ts +0 -214
- package/src/brand-package/presets/gsap.ts +0 -146
- package/src/brand-package/presets/index.ts +0 -31
- package/src/brand-package/presets/linear.ts +0 -183
- package/src/brand-package/presets/notion.ts +0 -208
- package/src/brand-package/presets/raycast.ts +0 -138
- package/src/brand-package/presets/recipe.ts +0 -61
- package/src/brand-package/presets/stripe.ts +0 -207
- package/src/brand-package/presets/vanta.ts +0 -220
- package/src/brand-package/presets/vercel.ts +0 -187
- package/src/brand-package/types.ts +0 -285
- package/src/brand-package/use-brand.ts +0 -207
- package/src/brand-package/validate.ts +0 -108
- package/src/index.ts +0 -67
- package/src/theme-provider.tsx +0 -246
- package/tsconfig.json +0 -11
- package/turbo.json +0 -15
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { compileReferoTokens } from "../compile-refero";
|
|
4
|
-
import { hexToHslChannels } from "../hex-to-hsl";
|
|
5
|
-
|
|
6
|
-
describe("hexToHslChannels", () => {
|
|
7
|
-
it("converts green", () => {
|
|
8
|
-
assert.match(hexToHslChannels("#0ae448"), /^\d+ \d+% \d+%$/);
|
|
9
|
-
});
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
describe("compileReferoTokens — GSAP", () => {
|
|
13
|
-
it("produces gradient-stroke recipe and category extensions", () => {
|
|
14
|
-
const result = compileReferoTokens({
|
|
15
|
-
id: "gsap",
|
|
16
|
-
designMd: "gradient-stroked CTA pill outlined-only",
|
|
17
|
-
tokens: {
|
|
18
|
-
color: {
|
|
19
|
-
"just-black": { $value: "#0e100f", $type: "color" },
|
|
20
|
-
"surface-cream": { $value: "#fffce1", $type: "color" },
|
|
21
|
-
"surface-50": { $value: "#7c7c6f", $type: "color" },
|
|
22
|
-
"surface-25": { $value: "#42433d", $type: "color" },
|
|
23
|
-
"off-black": { $value: "#191919", $type: "color" },
|
|
24
|
-
"shockingly-green": { $value: "#0ae448", $type: "color" },
|
|
25
|
-
pink: { $value: "#fec5fb", $type: "color" },
|
|
26
|
-
orangey: { $value: "#ff8709", $type: "color" },
|
|
27
|
-
lilac: { $value: "#9d95ff", $type: "color" },
|
|
28
|
-
blue: { $value: "#00bae2", $type: "color" },
|
|
29
|
-
},
|
|
30
|
-
radius: {
|
|
31
|
-
full: { $value: "100px", $type: "dimension" },
|
|
32
|
-
lg: { $value: "8px", $type: "dimension" },
|
|
33
|
-
},
|
|
34
|
-
$extensions: {
|
|
35
|
-
"com.refero.extraction": { siteName: "Gsap", url: "https://gsap.com" },
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
assert.equal(result.brand.recipe.buttonDefault, "gradient-stroke");
|
|
40
|
-
assert.equal(result.brand.recipe.radii.button, "100px");
|
|
41
|
-
assert.match(result.brand.recipe.elevationTokens.card, /0 0 #0000|0 0 #0000/);
|
|
42
|
-
assert.ok(result.brand.extensions?.categories?.scroll);
|
|
43
|
-
assert.match(result.css, /--btn-default-bg: transparent/);
|
|
44
|
-
assert.match(result.css, /--btn-default-stroke-gradient:/);
|
|
45
|
-
assert.match(result.css, /--btn-default-border-width: 1\.5px/);
|
|
46
|
-
assert.match(result.css, /@font-face/);
|
|
47
|
-
assert.match(result.css, /zone-marketing/);
|
|
48
|
-
assert.equal(result.brand.zones?.marketing?.display?.fontSize, "224px");
|
|
49
|
-
assert.ok((result.brand.typography.faces?.length ?? 0) > 0);
|
|
50
|
-
assert.ok(result.warnings.some((w) => w.includes("shockingly-green")));
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe("compileReferoTokens — Linear", () => {
|
|
55
|
-
it("produces solid CTA recipe", () => {
|
|
56
|
-
const result = compileReferoTokens({
|
|
57
|
-
id: "linear",
|
|
58
|
-
tokens: {
|
|
59
|
-
color: {
|
|
60
|
-
void: { $value: "#08090a", $type: "color" },
|
|
61
|
-
carbon: { $value: "#0f1011", $type: "color" },
|
|
62
|
-
graphite: { $value: "#23252a", $type: "color" },
|
|
63
|
-
paper: { $value: "#ffffff", $type: "color" },
|
|
64
|
-
"acid-lime": { $value: "#e4f222", $type: "color" },
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
assert.equal(result.brand.recipe.buttonDefault, "solid");
|
|
69
|
-
assert.match(result.css, /--btn-default-bg: hsl\(var\(--primary\)\)/);
|
|
70
|
-
assert.match(result.css, /--btn-default-border-width: 0px/);
|
|
71
|
-
// Canonical free radii/elev
|
|
72
|
-
assert.equal(result.brand.recipe.radii?.button, "6px");
|
|
73
|
-
assert.ok(result.brand.recipe.elevationTokens?.card);
|
|
74
|
-
// Dual-mode dark default + light paper
|
|
75
|
-
assert.equal(result.brand.darkDefault, true);
|
|
76
|
-
assert.ok(result.brand.modes?.dark?.semantic);
|
|
77
|
-
assert.ok(result.brand.modes?.light?.semantic);
|
|
78
|
-
assert.match(result.brand.modes?.light?.semantic?.background ?? "", /^0 0% 100%$/);
|
|
79
|
-
assert.match(result.css, /dualMode=true/);
|
|
80
|
-
assert.match(result.css, /\.dark,\nhtml\.dark\[data-brand="linear"\]/);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("validateBrandPackage", () => {
|
|
85
|
-
it("accepts compiled packages", async () => {
|
|
86
|
-
const { validateBrandPackage } = await import("../validate");
|
|
87
|
-
const result = compileReferoTokens({
|
|
88
|
-
id: "linear",
|
|
89
|
-
tokens: {
|
|
90
|
-
color: {
|
|
91
|
-
void: { $value: "#08090a", $type: "color" },
|
|
92
|
-
paper: { $value: "#ffffff", $type: "color" },
|
|
93
|
-
"acid-lime": { $value: "#e4f222", $type: "color" },
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
const v = validateBrandPackage(result.brand);
|
|
98
|
-
assert.equal(v.ok, true);
|
|
99
|
-
assert.ok(result.brand.roles?.action);
|
|
100
|
-
assert.equal(result.brand.semantic.primary, result.brand.roles?.action);
|
|
101
|
-
assert.ok(result.brand.recipe.elevationTokens?.card);
|
|
102
|
-
assert.ok(result.brand.recipe.radii?.button);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe("carrier contract — action ≠ brand", () => {
|
|
107
|
-
it("keeps Raycast brand mark off primary", () => {
|
|
108
|
-
const result = compileReferoTokens({
|
|
109
|
-
id: "raycast",
|
|
110
|
-
tokens: {
|
|
111
|
-
color: {
|
|
112
|
-
"void-black": { $value: "#040506", $type: "color" },
|
|
113
|
-
mist: { $value: "#e6e6e6", $type: "color" },
|
|
114
|
-
iron: { $value: "#454647", $type: "color" },
|
|
115
|
-
"coral-pulse": { $value: "#ff6363", $type: "color" },
|
|
116
|
-
"pure-white": { $value: "#ffffff", $type: "color" },
|
|
117
|
-
ink: { $value: "#07080a", $type: "color" },
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
assert.ok(result.brand.roles?.brand);
|
|
122
|
-
assert.notEqual(result.brand.roles?.action, result.brand.roles?.brand);
|
|
123
|
-
assert.equal(result.brand.semantic.primary, result.brand.roles?.action);
|
|
124
|
-
assert.match(result.css, /--brand-mark:/);
|
|
125
|
-
assert.match(result.css, /--role-action:/);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe("compileReferoTokens — Vercel", () => {
|
|
130
|
-
it("uses light monochrome Obsidian CTA and hairline elevation", () => {
|
|
131
|
-
const result = compileReferoTokens({
|
|
132
|
-
id: "vercel",
|
|
133
|
-
designMd:
|
|
134
|
-
"Typeset terminal on white paper. Filled Black Button #171717. Build depth with hairline borders via stacked box-shadows — never with drop-shadow. Never introduce chromatic color outside Terminal Green.",
|
|
135
|
-
tokens: {
|
|
136
|
-
color: {
|
|
137
|
-
"paper-white": { $value: "#fafafa", $type: "color" },
|
|
138
|
-
"pure-white": { $value: "#ffffff", $type: "color" },
|
|
139
|
-
hairline: { $value: "#ebebeb", $type: "color" },
|
|
140
|
-
obsidian: { $value: "#171717", $type: "color" },
|
|
141
|
-
charcoal: { $value: "#4d4d4d", $type: "color" },
|
|
142
|
-
"terminal-green": { $value: "#297a3a", $type: "color" },
|
|
143
|
-
},
|
|
144
|
-
surface: {
|
|
145
|
-
"page-canvas": { $value: "#fafafa", $type: "color" },
|
|
146
|
-
"card-surface": { $value: "#ffffff", $type: "color" },
|
|
147
|
-
},
|
|
148
|
-
$extensions: {
|
|
149
|
-
"com.refero.extraction": { siteName: "Vercel", url: "https://vercel.com" },
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
});
|
|
153
|
-
assert.equal(result.brand.id, "vercel");
|
|
154
|
-
assert.equal(result.brand.darkDefault, false);
|
|
155
|
-
assert.equal(result.brand.recipe.buttonDefault, "solid");
|
|
156
|
-
assert.equal(result.brand.recipe.radii.button, "6px");
|
|
157
|
-
assert.match(result.brand.recipe.elevationTokens.card, /1px|inset/);
|
|
158
|
-
// Light paper background (~98%)
|
|
159
|
-
assert.match(result.brand.semantic.background, /9[0-9]%/);
|
|
160
|
-
// Obsidian primary (~9%)
|
|
161
|
-
assert.match(result.brand.semantic.primary, /^0 0% 9%|0 0% 8%|0 0% 10%/);
|
|
162
|
-
assert.match(result.css, /0px 0px 0px 1px/);
|
|
163
|
-
assert.ok(!result.css.includes("rgba(255, 255, 255, 0.05) 0px 1px 0px 0px inset"));
|
|
164
|
-
// Dual-mode: light + dark palettes, separate CSS blocks
|
|
165
|
-
assert.ok(result.brand.modes?.light?.semantic);
|
|
166
|
-
assert.ok(result.brand.modes?.dark?.semantic);
|
|
167
|
-
assert.match(result.brand.modes!.dark!.semantic!.background, /^0 0% 0%$/);
|
|
168
|
-
assert.match(result.brand.modes!.dark!.semantic!.primary, /^0 0% 100%$/);
|
|
169
|
-
assert.match(result.css, /dualMode=true/);
|
|
170
|
-
assert.match(result.css, /\.dark,\nhtml\.dark\[data-brand="vercel"\]/);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
describe("compileReferoTokens — Notion", () => {
|
|
175
|
-
it("uses paper canvas, notion-blue action, ink brand-mark, 8/12 radii, elev none", () => {
|
|
176
|
-
const result = compileReferoTokens({
|
|
177
|
-
id: "notion",
|
|
178
|
-
designMd: `
|
|
179
|
-
# Notion — Style Reference
|
|
180
|
-
**Theme:** light
|
|
181
|
-
**Density:** comfortable
|
|
182
|
-
Page canvas #f6f5f4, cards #ffffff.
|
|
183
|
-
Do not add shadows to content cards — hairline borders only.
|
|
184
|
-
| cards | 12px |
|
|
185
|
-
| pills | 9999px |
|
|
186
|
-
| small | 4px |
|
|
187
|
-
| buttons | 8px |
|
|
188
|
-
Primary CTA Button Background #0075de. Notion Blue only filled button.
|
|
189
|
-
Outlined Text Button border-radius 4px (tertiary only).
|
|
190
|
-
`,
|
|
191
|
-
tokens: {
|
|
192
|
-
color: {
|
|
193
|
-
"notion-blue": { $value: "#0075de", $type: "color" },
|
|
194
|
-
"paper-warmth": { $value: "#f6f5f4", $type: "color" },
|
|
195
|
-
"pure-white": { $value: "#ffffff", $type: "color" },
|
|
196
|
-
"ink-black": { $value: "#000000", $type: "color" },
|
|
197
|
-
stone: { $value: "#757575", $type: "color" },
|
|
198
|
-
"sky-tint": { $value: "#e6f3fe", $type: "color" },
|
|
199
|
-
marigold: { $value: "#ffb110", $type: "color" },
|
|
200
|
-
coral: { $value: "#f64932", $type: "color" },
|
|
201
|
-
"midnight-ink": { $value: "#02093a", $type: "color" },
|
|
202
|
-
},
|
|
203
|
-
font: {
|
|
204
|
-
notioninter: { $value: "NotionInter", $type: "fontFamily" },
|
|
205
|
-
"lyon-text": { $value: "Lyon Text", $type: "fontFamily" },
|
|
206
|
-
},
|
|
207
|
-
radius: {
|
|
208
|
-
md: { $value: "4px", $type: "dimension" },
|
|
209
|
-
lg: { $value: "8px", $type: "dimension" },
|
|
210
|
-
xl: { $value: "12px", $type: "dimension" },
|
|
211
|
-
full: { $value: "9999px", $type: "dimension" },
|
|
212
|
-
},
|
|
213
|
-
$extensions: {
|
|
214
|
-
"com.refero.extraction": { siteName: "Notion", url: "https://www.notion.com" },
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
assert.equal(result.brand.id, "notion");
|
|
220
|
-
assert.equal(result.brand.darkDefault, false);
|
|
221
|
-
assert.equal(result.brand.recipe.buttonDefault, "solid");
|
|
222
|
-
assert.match(result.brand.recipe.elevationTokens.card, /0 0 #0000|0 0 #0000/);
|
|
223
|
-
assert.equal(result.brand.recipe.radii.button, "8px");
|
|
224
|
-
assert.equal(result.brand.recipe.radii.card, "12px");
|
|
225
|
-
// Paper warmth canvas (~96% L), not pure white as page if distinct
|
|
226
|
-
assert.match(result.brand.semantic.background, /9[0-9]%/);
|
|
227
|
-
assert.notEqual(result.brand.semantic.background, result.brand.semantic.card);
|
|
228
|
-
// Notion blue action
|
|
229
|
-
assert.match(result.brand.semantic.primary, /4[0-5]%|43%|44%/);
|
|
230
|
-
assert.ok(result.brand.roles?.brand);
|
|
231
|
-
assert.notEqual(result.brand.roles?.action, result.brand.roles?.brand);
|
|
232
|
-
assert.match(result.css, /--elevation-card: 0 0 #0000/);
|
|
233
|
-
assert.match(result.css, /--radius-button: 8px/);
|
|
234
|
-
assert.match(result.css, /--radius-card: 12px/);
|
|
235
|
-
assert.match(result.css, /--brand-mark:/);
|
|
236
|
-
assert.ok(result.warnings.some((w) => /notion-blue|decorative/i.test(w)));
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
describe("compileReferoTokens — shadcn-named import mapping", () => {
|
|
241
|
-
it("maps HSL channel triples under shadcn keys without falling back to defaults", () => {
|
|
242
|
-
const result = compileReferoTokens({
|
|
243
|
-
id: "imported-shadcn",
|
|
244
|
-
name: "Imported",
|
|
245
|
-
tokens: {
|
|
246
|
-
color: {
|
|
247
|
-
background: { $value: "0 0% 98%", $type: "color" },
|
|
248
|
-
foreground: { $value: "0 0% 9%", $type: "color" },
|
|
249
|
-
primary: { $value: "228 85% 56%", $type: "color" },
|
|
250
|
-
"primary-foreground": { $value: "0 0% 100%", $type: "color" },
|
|
251
|
-
card: { $value: "0 0% 100%", $type: "color" },
|
|
252
|
-
border: { $value: "0 0% 90%", $type: "color" },
|
|
253
|
-
muted: { $value: "0 0% 94%", $type: "color" },
|
|
254
|
-
"muted-foreground": { $value: "0 0% 45%", $type: "color" },
|
|
255
|
-
},
|
|
256
|
-
},
|
|
257
|
-
});
|
|
258
|
-
assert.equal(result.brand.darkDefault, false);
|
|
259
|
-
assert.equal(result.brand.semantic.background, "0 0% 98%");
|
|
260
|
-
assert.equal(result.brand.semantic.primary, "228 85% 56%");
|
|
261
|
-
assert.equal(result.brand.semantic.foreground, "0 0% 9%");
|
|
262
|
-
assert.equal(result.brand.semantic.card, "0 0% 100%");
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
describe("compileReferoTokens — Stripe", () => {
|
|
267
|
-
it("uses indigo action, midnight brand-mark, 4px radius, elevation none", () => {
|
|
268
|
-
const result = compileReferoTokens({
|
|
269
|
-
id: "stripe",
|
|
270
|
-
designMd: `
|
|
271
|
-
# Stripe — Style Reference
|
|
272
|
-
**Theme:** light
|
|
273
|
-
**Density:** comfortable
|
|
274
|
-
Stripe avoids shadows entirely. Depth comes from background tint.
|
|
275
|
-
Do not use shadows, blurs, or any form of CSS elevation.
|
|
276
|
-
Use 4px border-radius on all buttons, inputs, and cards — never pill.
|
|
277
|
-
Indigo Ink #533afd for filled buttons. Midnight Ink #061b31 for wordmark.
|
|
278
|
-
| buttons | 4px |
|
|
279
|
-
| cards | 4px |
|
|
280
|
-
Primary Filled Button Background #533afd.
|
|
281
|
-
`,
|
|
282
|
-
tokens: {
|
|
283
|
-
color: {
|
|
284
|
-
"indigo-ink": { $value: "#533afd", $type: "color" },
|
|
285
|
-
"indigo-hover": { $value: "#7389ff", $type: "color" },
|
|
286
|
-
"midnight-ink": { $value: "#061b31", $type: "color" },
|
|
287
|
-
slate: { $value: "#64748d", $type: "color" },
|
|
288
|
-
"pure-white": { $value: "#ffffff", $type: "color" },
|
|
289
|
-
mist: { $value: "#f8fafd", $type: "color" },
|
|
290
|
-
frost: { $value: "#e5edf5", $type: "color" },
|
|
291
|
-
"lavender-border": { $value: "#b9b9f9", $type: "color" },
|
|
292
|
-
"periwinkle-wash": { $value: "#e8e9ff", $type: "color" },
|
|
293
|
-
},
|
|
294
|
-
font: {
|
|
295
|
-
"sohne-var": { $value: "sohne-var", $type: "fontFamily" },
|
|
296
|
-
},
|
|
297
|
-
$extensions: {
|
|
298
|
-
"com.refero.extraction": { siteName: "Stripe", url: "https://stripe.com" },
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
assert.equal(result.brand.id, "stripe");
|
|
304
|
-
assert.equal(result.brand.darkDefault, false);
|
|
305
|
-
assert.equal(result.brand.recipe.buttonDefault, "solid");
|
|
306
|
-
assert.match(result.brand.recipe.elevationTokens.card, /0 0 #0000|0 0 #0000/);
|
|
307
|
-
assert.equal(result.brand.recipe.radii.button, "4px");
|
|
308
|
-
assert.equal(result.brand.recipe.radii.card, "4px");
|
|
309
|
-
assert.equal(result.brand.typography.headingWeight, 300);
|
|
310
|
-
// Indigo action (~61% L), not mist/white
|
|
311
|
-
assert.match(result.brand.semantic.primary, /6[0-5]%|5[5-9]%/);
|
|
312
|
-
assert.ok(result.brand.roles?.brand);
|
|
313
|
-
assert.notEqual(result.brand.roles?.action, result.brand.roles?.brand);
|
|
314
|
-
assert.match(result.brand.semantic.background, /100%|99%/);
|
|
315
|
-
assert.match(result.css, /--elevation-card: 0 0 #0000/);
|
|
316
|
-
assert.match(result.css, /--radius-button: 4px/);
|
|
317
|
-
assert.match(result.css, /--brand-mark:/);
|
|
318
|
-
assert.ok(result.warnings.some((w) => /indigo-ink|midnight/i.test(w)));
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
describe("compileReferoTokens — Vanta", () => {
|
|
323
|
-
it("separates vivid-violet action from indigo-ink brand-mark; pill + no shadow", () => {
|
|
324
|
-
const result = compileReferoTokens({
|
|
325
|
-
id: "vanta",
|
|
326
|
-
designMd: `
|
|
327
|
-
# Vanta — Style Reference
|
|
328
|
-
Editorial violet ledger on parchment
|
|
329
|
-
**Density:** comfortable
|
|
330
|
-
Don't drop shadows on cards — the flat 1px-border treatment is the system.
|
|
331
|
-
Don't use #5e05c4 as decorative icon fill — it's action-only.
|
|
332
|
-
Set every button, input, and badge to 999px radius — the pill is the signature.
|
|
333
|
-
Vanta rejects drop shadows entirely. Elevation is 1px hairline borders.
|
|
334
|
-
Primary Filled Button fill #5e05c4. Logo in Reckless #260048.
|
|
335
|
-
`,
|
|
336
|
-
tokens: {
|
|
337
|
-
color: {
|
|
338
|
-
"indigo-ink": { $value: "#260048", $type: "color" },
|
|
339
|
-
"vivid-violet": { $value: "#5e05c4", $type: "color" },
|
|
340
|
-
"mid-violet": { $value: "#8f47d5", $type: "color" },
|
|
341
|
-
"lavender-wash": { $value: "#ddd6ff", $type: "color" },
|
|
342
|
-
carbon: { $value: "#181822", $type: "color" },
|
|
343
|
-
slate: { $value: "#484960", $type: "color" },
|
|
344
|
-
parchment: { $value: "#f7f8fa", $type: "color" },
|
|
345
|
-
paper: { $value: "#ffffff", $type: "color" },
|
|
346
|
-
"amber-signal": { $value: "#ffbe0f", $type: "color" },
|
|
347
|
-
},
|
|
348
|
-
font: {
|
|
349
|
-
reckless: { $value: "Reckless", $type: "fontFamily" },
|
|
350
|
-
"inter-variable": { $value: "Inter Variable", $type: "fontFamily" },
|
|
351
|
-
},
|
|
352
|
-
radius: {
|
|
353
|
-
buttons: { $value: "999px", $type: "dimension" },
|
|
354
|
-
cards: { $value: "16px", $type: "dimension" },
|
|
355
|
-
badges: { $value: "999px", $type: "dimension" },
|
|
356
|
-
inputs: { $value: "999px", $type: "dimension" },
|
|
357
|
-
},
|
|
358
|
-
$extensions: {
|
|
359
|
-
"com.refero.extraction": { siteName: "Vanta", url: "https://www.vanta.com" },
|
|
360
|
-
},
|
|
361
|
-
},
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
assert.equal(result.brand.id, "vanta");
|
|
365
|
-
assert.equal(result.brand.darkDefault, false);
|
|
366
|
-
assert.equal(result.brand.recipe.buttonDefault, "solid");
|
|
367
|
-
assert.match(result.brand.recipe.elevationTokens.card, /0 0 #0000|0 0 #0000/);
|
|
368
|
-
assert.equal(result.brand.recipe.density, "comfortable");
|
|
369
|
-
assert.match(result.brand.recipe.radii.button, /999/);
|
|
370
|
-
assert.equal(result.brand.recipe.radii.card, "16px");
|
|
371
|
-
assert.equal(result.brand.recipe.badgeDefault, "muted");
|
|
372
|
-
|
|
373
|
-
// action = vivid violet (~39% L), not indigo (~14% L)
|
|
374
|
-
assert.match(result.brand.semantic.primary, /3[6-9]%|40%/);
|
|
375
|
-
assert.ok(result.brand.roles?.brand);
|
|
376
|
-
assert.notEqual(result.brand.roles?.action, result.brand.roles?.brand);
|
|
377
|
-
assert.equal(result.brand.semantic.primary, result.brand.roles?.action);
|
|
378
|
-
|
|
379
|
-
// Light parchment canvas + dark carbon text (not white-on-white)
|
|
380
|
-
assert.match(result.brand.semantic.background, /9[0-9]%/);
|
|
381
|
-
assert.match(result.brand.semantic.foreground, /^24[0-9] |^240 |^0 0% 1|^273 /);
|
|
382
|
-
|
|
383
|
-
assert.match(result.css, /--brand-mark:/);
|
|
384
|
-
assert.match(result.css, /--elevation-card: 0 0 #0000/);
|
|
385
|
-
assert.match(result.css, /--radius-button: 999/);
|
|
386
|
-
assert.match(result.brand.typography.fontSans, /Inter/i);
|
|
387
|
-
assert.match(result.brand.typography.fontDisplay ?? "", /Reckless/i);
|
|
388
|
-
assert.equal(result.brand.zones?.marketing?.display?.fontSize, "90px");
|
|
389
|
-
assert.ok(result.warnings.some((w) => /indigo-ink|brand-mark/i.test(w)));
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
describe("compileReferoTokens — Raycast", () => {
|
|
394
|
-
it("uses neutral Mist CTA and key elevation, not coral fill", () => {
|
|
395
|
-
const result = compileReferoTokens({
|
|
396
|
-
id: "raycast",
|
|
397
|
-
designMd:
|
|
398
|
-
"Neutral Filled Button Mist fill. Don't use chromatic action buttons. keyboard key shadow stack. Hero gradient artwork only.",
|
|
399
|
-
tokens: {
|
|
400
|
-
color: {
|
|
401
|
-
"void-black": { $value: "#040506", $type: "color" },
|
|
402
|
-
ink: { $value: "#07080a", $type: "color" },
|
|
403
|
-
mist: { $value: "#e6e6e6", $type: "color" },
|
|
404
|
-
iron: { $value: "#454647", $type: "color" },
|
|
405
|
-
"coral-pulse": { $value: "#ff6363", $type: "color" },
|
|
406
|
-
"pure-white": { $value: "#ffffff", $type: "color" },
|
|
407
|
-
graphite: { $value: "#1b1c1e", $type: "color" },
|
|
408
|
-
slate: { $value: "#2f3031", $type: "color" },
|
|
409
|
-
},
|
|
410
|
-
$extensions: {
|
|
411
|
-
"com.refero.extraction": { siteName: "Raycast", url: "https://raycast.com" },
|
|
412
|
-
},
|
|
413
|
-
},
|
|
414
|
-
});
|
|
415
|
-
assert.equal(result.brand.id, "raycast");
|
|
416
|
-
assert.equal(result.brand.recipe.buttonDefault, "solid");
|
|
417
|
-
assert.equal(result.brand.recipe.radii.button, "8px");
|
|
418
|
-
assert.equal(result.brand.recipe.radii.card, "16px");
|
|
419
|
-
assert.match(result.brand.recipe.elevationTokens.card, /inset|1px/);
|
|
420
|
-
assert.equal(result.brand.recipe.badgeDefault, "muted");
|
|
421
|
-
// Mist primary (light), not void-black / coral
|
|
422
|
-
assert.match(result.brand.semantic.primary, /90%|89%|91%/);
|
|
423
|
-
assert.match(result.css, /--elevation-card: rgba\(255/);
|
|
424
|
-
assert.match(result.css, /--badge-default-bg: hsl\(var\(--secondary\)\)/);
|
|
425
|
-
assert.ok(result.warnings.some((w) => w.includes("coral")));
|
|
426
|
-
});
|
|
427
|
-
});
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { emitBrandCss, emitGlobalSkinSelector } from "../emit-css";
|
|
4
|
-
import type { BrandPackage } from "../types";
|
|
5
|
-
|
|
6
|
-
function minimalBrand(
|
|
7
|
-
overrides: Partial<BrandPackage> & Pick<BrandPackage, "id" | "darkDefault">,
|
|
8
|
-
): BrandPackage {
|
|
9
|
-
return {
|
|
10
|
-
name: overrides.name ?? overrides.id,
|
|
11
|
-
version: "1.0.0",
|
|
12
|
-
semantic: {
|
|
13
|
-
background: "0 0% 100%",
|
|
14
|
-
foreground: "0 0% 10%",
|
|
15
|
-
card: "0 0% 100%",
|
|
16
|
-
cardForeground: "0 0% 10%",
|
|
17
|
-
popover: "0 0% 100%",
|
|
18
|
-
popoverForeground: "0 0% 10%",
|
|
19
|
-
primary: "220 90% 50%",
|
|
20
|
-
primaryForeground: "0 0% 100%",
|
|
21
|
-
secondary: "0 0% 96%",
|
|
22
|
-
secondaryForeground: "0 0% 10%",
|
|
23
|
-
muted: "0 0% 96%",
|
|
24
|
-
mutedForeground: "0 0% 40%",
|
|
25
|
-
accent: "0 0% 96%",
|
|
26
|
-
accentForeground: "0 0% 10%",
|
|
27
|
-
destructive: "0 72% 51%",
|
|
28
|
-
destructiveForeground: "0 0% 100%",
|
|
29
|
-
border: "0 0% 90%",
|
|
30
|
-
input: "0 0% 100%",
|
|
31
|
-
ring: "220 90% 50%",
|
|
32
|
-
},
|
|
33
|
-
recipe: {
|
|
34
|
-
buttonDefault: "solid",
|
|
35
|
-
density: "comfortable",
|
|
36
|
-
radii: {
|
|
37
|
-
button: "0.375rem",
|
|
38
|
-
card: "0.75rem",
|
|
39
|
-
badge: "9999px",
|
|
40
|
-
input: "0.375rem",
|
|
41
|
-
pill: "9999px",
|
|
42
|
-
},
|
|
43
|
-
elevationTokens: {
|
|
44
|
-
card: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
|
45
|
-
control: "0 0 #0000",
|
|
46
|
-
raised: "0 4px 6px -1px rgb(0 0 0 / 0.1)",
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
typography: { fontSans: "Inter, sans-serif" },
|
|
50
|
-
...overrides,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe("emitBrandCss darkDefault binding", () => {
|
|
55
|
-
it("includes .dark only for darkDefault packs in global mode", () => {
|
|
56
|
-
const dark = emitBrandCss(minimalBrand({ id: "linear-like", darkDefault: true }));
|
|
57
|
-
assert.match(dark, /:root,\n\.dark,\nhtml\[data-brand="linear-like"\] \{/);
|
|
58
|
-
|
|
59
|
-
const light = emitBrandCss(minimalBrand({ id: "stripe-like", darkDefault: false }));
|
|
60
|
-
assert.match(light, /:root,\nhtml\[data-brand="stripe-like"\] \{/);
|
|
61
|
-
assert.doesNotMatch(light, /\.dark/);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("scoped mode never binds :root or .dark", () => {
|
|
65
|
-
const css = emitBrandCss(minimalBrand({ id: "vanta", darkDefault: false }), { mode: "scoped" });
|
|
66
|
-
assert.match(css, /html\[data-brand="vanta"\] \{/);
|
|
67
|
-
assert.doesNotMatch(css, /:root/);
|
|
68
|
-
assert.doesNotMatch(css, /\.dark/);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("emitGlobalSkinSelector matches darkDefault contract", () => {
|
|
72
|
-
assert.equal(emitGlobalSkinSelector("x", true), `:root,\n.dark,\nhtml[data-brand="x"] {`);
|
|
73
|
-
assert.equal(emitGlobalSkinSelector("y", false), `:root,\nhtml[data-brand="y"] {`);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("dual-mode packs emit separate light and dark color blocks", () => {
|
|
77
|
-
const lightSemantic = minimalBrand({ id: "dual", darkDefault: false }).semantic;
|
|
78
|
-
const darkSemantic = {
|
|
79
|
-
...lightSemantic,
|
|
80
|
-
background: "0 0% 6%",
|
|
81
|
-
foreground: "0 0% 98%",
|
|
82
|
-
card: "0 0% 10%",
|
|
83
|
-
primary: "220 90% 60%",
|
|
84
|
-
};
|
|
85
|
-
const css = emitBrandCss({
|
|
86
|
-
...minimalBrand({ id: "dual", darkDefault: false }),
|
|
87
|
-
modes: {
|
|
88
|
-
light: { semantic: lightSemantic },
|
|
89
|
-
dark: { semantic: darkSemantic },
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
assert.match(css, /dualMode=true/);
|
|
93
|
-
assert.match(css, /:root,\nhtml\[data-brand="dual"\] \{/);
|
|
94
|
-
assert.match(css, /\.dark,\nhtml\.dark\[data-brand="dual"\] \{/);
|
|
95
|
-
assert.match(css, /--background: 0 0% 100%/); // light
|
|
96
|
-
assert.match(css, /--background: 0 0% 6%/); // dark
|
|
97
|
-
// light block must not list bare .dark in the same selector
|
|
98
|
-
assert.doesNotMatch(css, /:root,\n\.dark,\nhtml\[data-brand="dual"\]/);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("product zone emits --text-display once with heading fallback", () => {
|
|
102
|
-
const css = emitBrandCss(
|
|
103
|
-
minimalBrand({
|
|
104
|
-
id: "zoney",
|
|
105
|
-
darkDefault: false,
|
|
106
|
-
zones: {
|
|
107
|
-
product: {
|
|
108
|
-
body: { fontSize: "16px", lineHeight: 1.5 },
|
|
109
|
-
heading: { fontSize: "24px" },
|
|
110
|
-
display: { fontSize: "32px" },
|
|
111
|
-
},
|
|
112
|
-
marketing: {
|
|
113
|
-
body: { fontSize: "18px", lineHeight: 1.5 },
|
|
114
|
-
display: { fontSize: "48px" },
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
}),
|
|
118
|
-
);
|
|
119
|
-
const productBlock = css.match(
|
|
120
|
-
/\[data-zone="product"\][\s\S]*?(?=\[data-zone="marketing"\]|$)/,
|
|
121
|
-
)?.[0];
|
|
122
|
-
assert.ok(productBlock, "expected product zone block");
|
|
123
|
-
const displayDecls = productBlock.match(/--text-display:/g) ?? [];
|
|
124
|
-
assert.equal(displayDecls.length, 1, "no duplicate --text-display in product zone");
|
|
125
|
-
assert.match(
|
|
126
|
-
productBlock,
|
|
127
|
-
/--text-display:\s*var\(--zone-product-display-size,\s*var\(--zone-product-heading-size,\s*inherit\)\)/,
|
|
128
|
-
);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { emitBrandCss } from "./emit-css";
|
|
2
|
-
import type { BrandPackage } from "./types";
|
|
3
|
-
|
|
4
|
-
export const BRAND_STYLE_ELEMENT_ID = "nebutra-brand-skin";
|
|
5
|
-
export const BRAND_STORAGE_KEY = "nebutra-brand-package";
|
|
6
|
-
|
|
7
|
-
export interface ApplyBrandOptions {
|
|
8
|
-
/** Persist package JSON to localStorage for Create Center preview reload */
|
|
9
|
-
persist?: boolean;
|
|
10
|
-
/** Target document (iframe preview support) */
|
|
11
|
-
doc?: Document;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function targetDoc(doc?: Document): Document | null {
|
|
15
|
-
if (doc) return doc;
|
|
16
|
-
if (typeof document === "undefined") return null;
|
|
17
|
-
return document;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Inject brand skin CSS at runtime (Create Center preview / tenant switch).
|
|
22
|
-
* Does not require rebuilding app CSS — overrides semantic + recipe vars.
|
|
23
|
-
*/
|
|
24
|
-
export function applyBrandCss(
|
|
25
|
-
css: string,
|
|
26
|
-
brandId?: string,
|
|
27
|
-
options: ApplyBrandOptions = {},
|
|
28
|
-
): void {
|
|
29
|
-
const d = targetDoc(options.doc);
|
|
30
|
-
if (!d) return;
|
|
31
|
-
|
|
32
|
-
let el = d.getElementById(BRAND_STYLE_ELEMENT_ID) as HTMLStyleElement | null;
|
|
33
|
-
if (!el) {
|
|
34
|
-
el = d.createElement("style");
|
|
35
|
-
el.id = BRAND_STYLE_ELEMENT_ID;
|
|
36
|
-
el.setAttribute("data-nebutra-brand", brandId ?? "custom");
|
|
37
|
-
d.head.appendChild(el);
|
|
38
|
-
}
|
|
39
|
-
el.textContent = css;
|
|
40
|
-
if (brandId) {
|
|
41
|
-
d.documentElement.dataset.brand = brandId;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Apply a full Brand Package (emit CSS + optional persist). */
|
|
46
|
-
export function applyBrandPackage(brand: BrandPackage, options: ApplyBrandOptions = {}): void {
|
|
47
|
-
const css = emitBrandCss(brand);
|
|
48
|
-
applyBrandCss(css, brand.id, options);
|
|
49
|
-
|
|
50
|
-
if (options.persist && typeof localStorage !== "undefined") {
|
|
51
|
-
try {
|
|
52
|
-
localStorage.setItem(BRAND_STORAGE_KEY, JSON.stringify(brand));
|
|
53
|
-
} catch {
|
|
54
|
-
// quota / private mode — ignore
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Remove runtime brand skin and restore default Nebutra tokens. */
|
|
60
|
-
export function clearBrand(options: ApplyBrandOptions = {}): void {
|
|
61
|
-
const d = targetDoc(options.doc);
|
|
62
|
-
if (!d) return;
|
|
63
|
-
d.getElementById(BRAND_STYLE_ELEMENT_ID)?.remove();
|
|
64
|
-
delete d.documentElement.dataset.brand;
|
|
65
|
-
if (options.persist && typeof localStorage !== "undefined") {
|
|
66
|
-
try {
|
|
67
|
-
localStorage.removeItem(BRAND_STORAGE_KEY);
|
|
68
|
-
} catch {
|
|
69
|
-
// ignore
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Restore brand package previously persisted by applyBrandPackage({ persist: true }). */
|
|
75
|
-
export function restorePersistedBrand(options: ApplyBrandOptions = {}): BrandPackage | null {
|
|
76
|
-
if (typeof localStorage === "undefined") return null;
|
|
77
|
-
try {
|
|
78
|
-
const raw = localStorage.getItem(BRAND_STORAGE_KEY);
|
|
79
|
-
if (!raw) return null;
|
|
80
|
-
const brand = JSON.parse(raw) as BrandPackage;
|
|
81
|
-
if (!brand?.id || !brand?.semantic || !brand?.recipe) return null;
|
|
82
|
-
applyBrandPackage(brand, { ...options, persist: false });
|
|
83
|
-
return brand;
|
|
84
|
-
} catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function getActiveBrandId(doc?: Document): string | null {
|
|
90
|
-
const d = targetDoc(doc);
|
|
91
|
-
return d?.documentElement.dataset.brand ?? null;
|
|
92
|
-
}
|