@nebutra/tokens 0.1.1 → 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/CHANGELOG.md +23 -0
- package/LICENSE +21 -676
- package/README.md +41 -8
- package/brands/gsap/brand.json +133 -0
- package/brands/linear/brand.json +163 -0
- package/brands/notion/brand.json +145 -0
- package/brands/raycast/brand.json +120 -0
- package/brands/stripe/brand.json +127 -0
- package/brands/vanta/brand.json +151 -0
- package/brands/vercel/brand.json +173 -0
- 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 +46 -12
- package/recipe.css +435 -0
- package/skins/README.md +149 -0
- package/skins/gsap.css +159 -0
- package/skins/linear.css +198 -0
- package/skins/notion.css +158 -0
- package/skins/raycast.css +185 -0
- package/skins/stripe.css +146 -0
- package/skins/vanta.css +160 -0
- package/skins/vercel.css +202 -0
- package/styles.css +528 -169
- package/.turbo/turbo-build.log +0 -5
- package/.turbo/turbo-typecheck.log +0 -4
- package/AGENTS.md +0 -42
- package/DESIGN.md +0 -335
- package/src/index.ts +0 -37
- package/src/theme-provider.tsx +0 -244
- package/tsconfig.json +0 -11
package/recipe.css
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product chrome recipe surface — carrier defaults for Nebutra factory brand.
|
|
3
|
+
*
|
|
4
|
+
* Contract:
|
|
5
|
+
* --primary = roles.action (CTA only)
|
|
6
|
+
* --brand-mark = roles.brand (logo / AI badge; never default CTA)
|
|
7
|
+
* --elevation-card|… = free CSS box-shadow from brand package
|
|
8
|
+
* --radius-button|card|… = shape slots
|
|
9
|
+
*
|
|
10
|
+
* VI logo lock (--brand-primary hex) remains separate from product action.
|
|
11
|
+
* Tailwind utilities: bg-brand-mark / text-brand-mark (see @theme below).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
:root {
|
|
15
|
+
/* ── Color roles (overridden by brand skins) ──────────────────── */
|
|
16
|
+
--role-action: var(--primary);
|
|
17
|
+
--role-action-fg: var(--primary-foreground);
|
|
18
|
+
--brand-mark: var(--primary);
|
|
19
|
+
--brand-mark-foreground: var(--primary-foreground);
|
|
20
|
+
|
|
21
|
+
/* ── Button default (solid | outline | gradient-stroke) ───────── */
|
|
22
|
+
--btn-default-bg: hsl(var(--primary));
|
|
23
|
+
--btn-default-fg: hsl(var(--primary-foreground));
|
|
24
|
+
--btn-default-border: transparent;
|
|
25
|
+
--btn-default-border-width: 0px;
|
|
26
|
+
/* An image, never a colour — see the @property registration below. */
|
|
27
|
+
--btn-default-stroke-gradient: linear-gradient(transparent, transparent);
|
|
28
|
+
--btn-default-hover-bg: color-mix(in srgb, hsl(var(--primary)) 90%, transparent);
|
|
29
|
+
--btn-default-radius: var(--radius-button, var(--radius-md, 0.375rem));
|
|
30
|
+
--btn-default-shadow: 0 0 #0000;
|
|
31
|
+
--btn-default-padding-y: 0.5rem;
|
|
32
|
+
--btn-default-padding-x: 1rem;
|
|
33
|
+
|
|
34
|
+
/* ── Shape slots ──────────────────────────────────────────────── */
|
|
35
|
+
--radius-button: var(--radius-md, 0.375rem);
|
|
36
|
+
--radius-card: var(--radius-lg, 0.75rem);
|
|
37
|
+
--radius-badge: 9999px;
|
|
38
|
+
--radius-inputs: var(--radius-md, 0.375rem);
|
|
39
|
+
--radius-pill: 9999px;
|
|
40
|
+
|
|
41
|
+
/* ── Control density (factory = Geist ladder; brands may override) ─ */
|
|
42
|
+
--control-height-tiny: 1.5rem; /* 24 */
|
|
43
|
+
--control-height-sm: 2rem; /* 32 */
|
|
44
|
+
--control-height-md: 2.5rem; /* 40 */
|
|
45
|
+
--control-height-lg: 3rem; /* 48 */
|
|
46
|
+
/* Icon-only triggers — orthogonal to the text-button ladder above.
|
|
47
|
+
apps use 28/32/36 rounded-md squares; circle uses the same boxes. */
|
|
48
|
+
--control-height-icon-sm: 1.75rem; /* 28 */
|
|
49
|
+
--control-height-icon-md: 2rem; /* 32 */
|
|
50
|
+
--control-height-icon-lg: 2.25rem; /* 36 */
|
|
51
|
+
--control-font-size-tiny: 0.6875rem; /* 11 */
|
|
52
|
+
--control-font-size-sm: 0.75rem; /* 12 */
|
|
53
|
+
--control-font-size-md: 0.875rem; /* 14 */
|
|
54
|
+
--control-font-size-lg: 1rem; /* 16 */
|
|
55
|
+
--font-weight-medium: 500;
|
|
56
|
+
--font-weight-heading: 600;
|
|
57
|
+
|
|
58
|
+
/* ── Elevation (free CSS; brand package replaces these) ───────── */
|
|
59
|
+
--elevation-card: var(--shadow-sm, 0 1px 2px 0 rgb(0 0 0 / 0.05));
|
|
60
|
+
--elevation-control: var(--shadow-xs, 0 1px 2px 0 rgb(0 0 0 / 0.04));
|
|
61
|
+
--elevation-raised: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1));
|
|
62
|
+
|
|
63
|
+
/* ── Badge default = brand mark (AI / identity chips; not CTA) ─── */
|
|
64
|
+
--badge-default-bg: hsl(var(--brand-mark));
|
|
65
|
+
--badge-default-fg: hsl(var(--brand-mark-foreground));
|
|
66
|
+
--badge-default-border: transparent;
|
|
67
|
+
--badge-default-radius: var(--radius-badge, 9999px);
|
|
68
|
+
--badge-default-hover-bg: color-mix(in srgb, hsl(var(--brand-mark)) 80%, transparent);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The stroke slot is consumed as a background-image layer, so its value has to
|
|
73
|
+
* be an <image>. A colour there is not merely ignored: background-image becomes
|
|
74
|
+
* invalid as a whole and the browser drops every layer, including the solid
|
|
75
|
+
* brand fill in front of it. The button then renders with no background at all,
|
|
76
|
+
* while `color` — a separate declaration — keeps painting white text. That is
|
|
77
|
+
* what shipped: every default Button outside the gsap skin was white-on-white,
|
|
78
|
+
* because the slot held `transparent`, which is a colour.
|
|
79
|
+
*
|
|
80
|
+
* Registering the property makes that unrepresentable rather than merely
|
|
81
|
+
* discouraged. A colour written here now fails to parse against `<image>`, so
|
|
82
|
+
* the declaration is rejected and the slot keeps this initial value — the fill
|
|
83
|
+
* survives. Skins are generated, and a generator is exactly the place where one
|
|
84
|
+
* wrong branch reaches every brand at once.
|
|
85
|
+
*
|
|
86
|
+
* The var() fallbacks at the use sites stay as the unregistered path, so the
|
|
87
|
+
* recipe is still correct where @property is unsupported.
|
|
88
|
+
*/
|
|
89
|
+
@property --btn-default-stroke-gradient {
|
|
90
|
+
syntax: "<image>";
|
|
91
|
+
inherits: true;
|
|
92
|
+
initial-value: linear-gradient(transparent, transparent);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* ── Type contracts for every other recipe slot ───────────────────────────────
|
|
97
|
+
*
|
|
98
|
+
* Same reasoning, generalised. Each slot below is read from a typed CSS position
|
|
99
|
+
* (a colour, a length, a font weight), so a wrong-typed value written by a skin
|
|
100
|
+
* does not fail locally — it makes the whole consuming declaration invalid at
|
|
101
|
+
* computed-value time and the browser silently drops it. Registering the slot
|
|
102
|
+
* moves the failure to the slot itself: the bad value is rejected, the property
|
|
103
|
+
* keeps its initial-value, and the declaration that reads it still renders.
|
|
104
|
+
*
|
|
105
|
+
* Rules observed here:
|
|
106
|
+
* • `inherits: true` on every slot. Custom properties inherit by default, and
|
|
107
|
+
* every skin sets these on `:root` / `[data-theme]` for descendants to read.
|
|
108
|
+
* `inherits: false` would hand every non-root element the initial-value.
|
|
109
|
+
* • `initial-value` is computationally independent: absolute colours and `px`
|
|
110
|
+
* only. No `var()`, no `em`/`rem`, no `currentcolor`. The declared values in
|
|
111
|
+
* `:root` above are free to keep using `var()` and `rem` — the restriction
|
|
112
|
+
* applies to the initial-value alone. The px initials are the rem defaults
|
|
113
|
+
* resolved at a 16px root (0.375rem → 6px) so the fallback matches the
|
|
114
|
+
* factory ladder rather than inventing a new one.
|
|
115
|
+
* • Registering a `<length>` slot absolutises `rem` at computed-value time.
|
|
116
|
+
* Every skin writes `rem` or `px`, so this is a no-op today. A skin that
|
|
117
|
+
* wants element-relative sizing must not use `em` in these slots: it would
|
|
118
|
+
* resolve against the element's own font-size before the slot is read.
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
/* ── Colour slots ─────────────────────────────────────────────────────────────
|
|
122
|
+
* Read inside `linear-gradient()` (button) and `background-color` (badge).
|
|
123
|
+
* Initial-values are the light-theme action blue (222.8 85% 55.7%) and its
|
|
124
|
+
* white foreground, i.e. the factory default rendered without a skin.
|
|
125
|
+
*/
|
|
126
|
+
@property --btn-default-bg {
|
|
127
|
+
syntax: "<color>";
|
|
128
|
+
inherits: true;
|
|
129
|
+
initial-value: hsl(222.8 85% 55.7%);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@property --btn-default-fg {
|
|
133
|
+
syntax: "<color>";
|
|
134
|
+
inherits: true;
|
|
135
|
+
initial-value: #ffffff;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@property --btn-default-border {
|
|
139
|
+
syntax: "<color>";
|
|
140
|
+
inherits: true;
|
|
141
|
+
initial-value: transparent;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@property --btn-default-hover-bg {
|
|
145
|
+
syntax: "<color>";
|
|
146
|
+
inherits: true;
|
|
147
|
+
initial-value: hsl(222.8 85% 55.7%);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@property --badge-default-bg {
|
|
151
|
+
syntax: "<color>";
|
|
152
|
+
inherits: true;
|
|
153
|
+
initial-value: hsl(222.8 85% 55.7%);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@property --badge-default-fg {
|
|
157
|
+
syntax: "<color>";
|
|
158
|
+
inherits: true;
|
|
159
|
+
initial-value: #ffffff;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@property --badge-default-border {
|
|
163
|
+
syntax: "<color>";
|
|
164
|
+
inherits: true;
|
|
165
|
+
initial-value: transparent;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@property --badge-default-hover-bg {
|
|
169
|
+
syntax: "<color>";
|
|
170
|
+
inherits: true;
|
|
171
|
+
initial-value: hsl(222.8 85% 55.7%);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* ── Length slots ─────────────────────────────────────────────────────────────
|
|
175
|
+
* `border-width` and the control ladder are plain lengths. Radii and padding go
|
|
176
|
+
* into `border-radius` / `padding`, which both accept percentages, so those get
|
|
177
|
+
* `<length-percentage>` — a skin expressing a pill as `50%` stays legal.
|
|
178
|
+
*/
|
|
179
|
+
@property --btn-default-border-width {
|
|
180
|
+
syntax: "<length>";
|
|
181
|
+
inherits: true;
|
|
182
|
+
initial-value: 0px;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@property --btn-default-radius {
|
|
186
|
+
syntax: "<length-percentage>";
|
|
187
|
+
inherits: true;
|
|
188
|
+
initial-value: 6px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@property --btn-default-padding-y {
|
|
192
|
+
syntax: "<length-percentage>";
|
|
193
|
+
inherits: true;
|
|
194
|
+
initial-value: 8px;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@property --btn-default-padding-x {
|
|
198
|
+
syntax: "<length-percentage>";
|
|
199
|
+
inherits: true;
|
|
200
|
+
initial-value: 16px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@property --badge-default-radius {
|
|
204
|
+
syntax: "<length-percentage>";
|
|
205
|
+
inherits: true;
|
|
206
|
+
initial-value: 9999px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@property --radius-button {
|
|
210
|
+
syntax: "<length-percentage>";
|
|
211
|
+
inherits: true;
|
|
212
|
+
initial-value: 6px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@property --radius-card {
|
|
216
|
+
syntax: "<length-percentage>";
|
|
217
|
+
inherits: true;
|
|
218
|
+
initial-value: 12px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@property --radius-badge {
|
|
222
|
+
syntax: "<length-percentage>";
|
|
223
|
+
inherits: true;
|
|
224
|
+
initial-value: 9999px;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@property --radius-inputs {
|
|
228
|
+
syntax: "<length-percentage>";
|
|
229
|
+
inherits: true;
|
|
230
|
+
initial-value: 6px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@property --radius-pill {
|
|
234
|
+
syntax: "<length-percentage>";
|
|
235
|
+
inherits: true;
|
|
236
|
+
initial-value: 9999px;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@property --control-height-tiny {
|
|
240
|
+
syntax: "<length>";
|
|
241
|
+
inherits: true;
|
|
242
|
+
initial-value: 24px;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@property --control-height-sm {
|
|
246
|
+
syntax: "<length>";
|
|
247
|
+
inherits: true;
|
|
248
|
+
initial-value: 32px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@property --control-height-md {
|
|
252
|
+
syntax: "<length>";
|
|
253
|
+
inherits: true;
|
|
254
|
+
initial-value: 40px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@property --control-height-lg {
|
|
258
|
+
syntax: "<length>";
|
|
259
|
+
inherits: true;
|
|
260
|
+
initial-value: 48px;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@property --control-height-icon-sm {
|
|
264
|
+
syntax: "<length>";
|
|
265
|
+
inherits: true;
|
|
266
|
+
initial-value: 28px;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@property --control-height-icon-md {
|
|
270
|
+
syntax: "<length>";
|
|
271
|
+
inherits: true;
|
|
272
|
+
initial-value: 32px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@property --control-height-icon-lg {
|
|
276
|
+
syntax: "<length>";
|
|
277
|
+
inherits: true;
|
|
278
|
+
initial-value: 36px;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@property --control-font-size-tiny {
|
|
282
|
+
syntax: "<length>";
|
|
283
|
+
inherits: true;
|
|
284
|
+
initial-value: 11px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
@property --control-font-size-sm {
|
|
288
|
+
syntax: "<length>";
|
|
289
|
+
inherits: true;
|
|
290
|
+
initial-value: 12px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
@property --control-font-size-md {
|
|
294
|
+
syntax: "<length>";
|
|
295
|
+
inherits: true;
|
|
296
|
+
initial-value: 14px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@property --control-font-size-lg {
|
|
300
|
+
syntax: "<length>";
|
|
301
|
+
inherits: true;
|
|
302
|
+
initial-value: 16px;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* ── Numeric slots ────────────────────────────────────────────────────────────
|
|
306
|
+
* `font-weight` also accepts `normal` / `bold` in CSS, so those two idents are
|
|
307
|
+
* spelled into the syntax rather than excluded. `bolder` / `lighter` are left
|
|
308
|
+
* out on purpose: they are parent-relative and cannot survive as a slot value.
|
|
309
|
+
* Every skin currently writes a number (300–700).
|
|
310
|
+
*/
|
|
311
|
+
@property --font-weight-medium {
|
|
312
|
+
syntax: "<number> | normal | bold";
|
|
313
|
+
inherits: true;
|
|
314
|
+
initial-value: 500;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@property --font-weight-heading {
|
|
318
|
+
syntax: "<number> | normal | bold";
|
|
319
|
+
inherits: true;
|
|
320
|
+
initial-value: 600;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* ── Genuinely untypeable: box-shadow lists ───────────────────────────────────
|
|
324
|
+
* These four are consumed as `box-shadow`, and the `@property` syntax grammar
|
|
325
|
+
* has no `<shadow>` component — its component names are limited to <length>,
|
|
326
|
+
* <number>, <percentage>, <length-percentage>, <color>, <image>, <url>,
|
|
327
|
+
* <integer>, <angle>, <time>, <resolution>, <transform-function>,
|
|
328
|
+
* <transform-list>, <custom-ident> and <string>, with only `+` / `#`
|
|
329
|
+
* multipliers. A shadow list ("0 1px 2px 0 rgb(…)", possibly comma-separated)
|
|
330
|
+
* cannot be spelled in that grammar, so `*` is the honest answer, not a
|
|
331
|
+
* shortcut. These slots therefore remain unguarded and a colour written into
|
|
332
|
+
* one still voids the `box-shadow` declaration that reads it. The audit script
|
|
333
|
+
* is the backstop for them.
|
|
334
|
+
*/
|
|
335
|
+
@property --btn-default-shadow {
|
|
336
|
+
syntax: "*";
|
|
337
|
+
inherits: true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
@property --elevation-card {
|
|
341
|
+
syntax: "*";
|
|
342
|
+
inherits: true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
@property --elevation-control {
|
|
346
|
+
syntax: "*";
|
|
347
|
+
inherits: true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@property --elevation-raised {
|
|
351
|
+
syntax: "*";
|
|
352
|
+
inherits: true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* ── Genuinely untypeable: bare HSL channel triplets ──────────────────────────
|
|
356
|
+
* These carry channels ("222 47% 11%"), not colours — they exist to be wrapped
|
|
357
|
+
* as `hsl(var(--brand-mark))` / `hsl(var(--brand-mark) / 0.08)`, which is what
|
|
358
|
+
* makes the alpha-suffix idiom possible. `<color>` would reject every one of
|
|
359
|
+
* them. There is also no typed spelling for the triplet: `@property` syntax has
|
|
360
|
+
* no space-separated component composition and no <number-percentage>, so
|
|
361
|
+
* "<number> <percentage> <percentage>" is not expressible. `*` is correct here.
|
|
362
|
+
* The type error these slots invite is the inverse one — writing a full colour
|
|
363
|
+
* (`#0033FE`) where channels are expected, which the audit script catches at
|
|
364
|
+
* the `hsl()` call site.
|
|
365
|
+
*/
|
|
366
|
+
@property --role-action {
|
|
367
|
+
syntax: "*";
|
|
368
|
+
inherits: true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@property --role-action-fg {
|
|
372
|
+
syntax: "*";
|
|
373
|
+
inherits: true;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
@property --brand-mark {
|
|
377
|
+
syntax: "*";
|
|
378
|
+
inherits: true;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
@property --brand-mark-foreground {
|
|
382
|
+
syntax: "*";
|
|
383
|
+
inherits: true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* Tailwind v4: brand-mark as first-class color (Logo / BrandMark / badge) */
|
|
387
|
+
@theme inline {
|
|
388
|
+
--color-brand-mark: hsl(var(--brand-mark));
|
|
389
|
+
--color-brand-mark-foreground: hsl(var(--brand-mark-foreground));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* ── Button default ─────────────────────────────────────────────── */
|
|
393
|
+
.btn-brand-default {
|
|
394
|
+
border-width: var(--btn-default-border-width, 0px);
|
|
395
|
+
border-style: solid;
|
|
396
|
+
border-color: transparent;
|
|
397
|
+
border-radius: var(--btn-default-radius, var(--radius-md, 0.375rem));
|
|
398
|
+
background-color: transparent;
|
|
399
|
+
background-image:
|
|
400
|
+
linear-gradient(
|
|
401
|
+
var(--btn-default-bg, hsl(var(--primary))),
|
|
402
|
+
var(--btn-default-bg, hsl(var(--primary)))
|
|
403
|
+
),
|
|
404
|
+
var(--btn-default-stroke-gradient, linear-gradient(transparent, transparent));
|
|
405
|
+
background-origin: border-box;
|
|
406
|
+
background-clip: padding-box, border-box;
|
|
407
|
+
color: var(--btn-default-fg, hsl(var(--primary-foreground)));
|
|
408
|
+
box-shadow: var(--btn-default-shadow, 0 0 #0000);
|
|
409
|
+
font-weight: var(--font-weight-medium, 500);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.btn-brand-default:hover:not(:disabled):not([aria-disabled="true"]) {
|
|
413
|
+
background-image:
|
|
414
|
+
linear-gradient(
|
|
415
|
+
var(--btn-default-hover-bg, color-mix(in srgb, hsl(var(--primary)) 90%, transparent)),
|
|
416
|
+
var(--btn-default-hover-bg, color-mix(in srgb, hsl(var(--primary)) 90%, transparent))
|
|
417
|
+
),
|
|
418
|
+
var(--btn-default-stroke-gradient, linear-gradient(transparent, transparent));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* ── Badge default (brand-mark; skins may set match-action → primary) ─ */
|
|
422
|
+
.badge-brand-default {
|
|
423
|
+
border-color: var(--badge-default-border, transparent);
|
|
424
|
+
border-radius: var(--badge-default-radius, 9999px);
|
|
425
|
+
background-color: var(--badge-default-bg, hsl(var(--brand-mark)));
|
|
426
|
+
color: var(--badge-default-fg, hsl(var(--brand-mark-foreground)));
|
|
427
|
+
font-weight: var(--font-weight-medium, 500);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.badge-brand-default:hover {
|
|
431
|
+
background-color: var(
|
|
432
|
+
--badge-default-hover-bg,
|
|
433
|
+
color-mix(in srgb, hsl(var(--brand-mark)) 80%, transparent)
|
|
434
|
+
);
|
|
435
|
+
}
|
package/skins/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Design skins & Brand Packages
|
|
2
|
+
|
|
3
|
+
**Goal:** Create Center users swap a **design language** by applying one **Brand Package**.
|
|
4
|
+
This package is a *carrier* (roles + free elev/radii), not a list of brand hacks.
|
|
5
|
+
|
|
6
|
+
## Two layers
|
|
7
|
+
|
|
8
|
+
| Layer | What | Path |
|
|
9
|
+
|-------|------|------|
|
|
10
|
+
| **Recipe defaults** | Button default reads `--btn-default-*` | `@nebutra/tokens/recipe.css` (auto via `preset.css`) |
|
|
11
|
+
| **Brand JSON (SSOT)** | Machine contract for Create Center | `brands/<id>/brand.json` |
|
|
12
|
+
| **Brand skin (generated)** | Semantic colors + recipe overrides | `@nebutra/tokens/skins/<id>.css` only |
|
|
13
|
+
|
|
14
|
+
## Product chrome contract
|
|
15
|
+
|
|
16
|
+
| Semantic | Tailwind / CSS | Role |
|
|
17
|
+
|----------|----------------|------|
|
|
18
|
+
| `--primary` + `--primary-foreground` | `bg-primary` | **Atomic pair** |
|
|
19
|
+
| `--background` / `--foreground` | canvas | App shell |
|
|
20
|
+
| `--card` / `--muted` / `--border` / `--input` / `--ring` | surfaces | Controls |
|
|
21
|
+
| `--btn-default-bg/fg/border/radius` | `Button` default | **Recipe** (solid vs outline) |
|
|
22
|
+
|
|
23
|
+
**Do not** paint product chrome with raw hex / `bg-blue-9` / VI `--brand-primary`.
|
|
24
|
+
|
|
25
|
+
## Apply a brand (opt-in)
|
|
26
|
+
|
|
27
|
+
```css
|
|
28
|
+
@import "@nebutra/ui/styles/preset.css";
|
|
29
|
+
/* or: tokens + recipe + sources */
|
|
30
|
+
|
|
31
|
+
@import "@nebutra/tokens/skins/linear.css"; /* solid acid-lime CTA */
|
|
32
|
+
/* @import "@nebutra/tokens/skins/gsap.css"; outline-first / gradient-stroke */
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Optional: `html data-brand="gsap"` for runtime targeting (skins also bind `:root`).
|
|
36
|
+
|
|
37
|
+
Default Nebutra shipping brand = **no skin import**.
|
|
38
|
+
|
|
39
|
+
## Compile from Refero export (Create Center pipeline MVP)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Folder with tokens.json + DESIGN.md (Desktop/GSAP style)
|
|
43
|
+
pnpm --filter @nebutra/tokens exec node scripts/compile-brand.mjs ~/Desktop/GSAP --id gsap
|
|
44
|
+
|
|
45
|
+
# Writes brand.json (SSOT) + skins/<id>.css
|
|
46
|
+
# After hand-editing brand.json:
|
|
47
|
+
pnpm --filter @nebutra/tokens emit-skins
|
|
48
|
+
# Then refresh multi-language catalog:
|
|
49
|
+
pnpm --filter @nebutra/theme sync:skins
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Programmatic:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { compileReferoTokens } from "@nebutra/tokens/brand-package";
|
|
56
|
+
|
|
57
|
+
const { brand, css, warnings } = compileReferoTokens({
|
|
58
|
+
tokens: referoJson,
|
|
59
|
+
designMd,
|
|
60
|
+
id: "gsap",
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Fixtures
|
|
65
|
+
|
|
66
|
+
Cataloged as **design languages** on `@nebutra/theme` (`LANGUAGE_REGISTRY` / `skins.css`).
|
|
67
|
+
|
|
68
|
+
| Brand | Recipe | Notes |
|
|
69
|
+
|-------|--------|-------|
|
|
70
|
+
| **linear** | `solid`, 6px radius | Acid-lime filled CTA |
|
|
71
|
+
| **gsap** | `gradient-stroke` / outline, 100px pill | Green is **accent**, not solid fill |
|
|
72
|
+
| **raycast** | `solid` Mist/Iron, 8px, elevation=`key` | Coral is brand-only; CTA is neutral gray |
|
|
73
|
+
| **vercel** | `solid` Obsidian on paper, 6px, elevation=`hairline` | Light monochrome; no chromatic CTA |
|
|
74
|
+
| **vanta** | `solid` Vivid Violet, 999px pill, elevation=`none` | Light parchment; **indigo-ink** brand-mark ≠ CTA; carbon 1px borders |
|
|
75
|
+
| **stripe** | `solid` Indigo Ink, 4px, elevation=`none` | Light frost ledger; **midnight** brand-mark ≠ indigo CTA; tint-ladder depth |
|
|
76
|
+
| **notion** | `solid` Notion Blue, 8px/12px, elevation=`none` | Warm paper canvas ≠ white cards; ink brand-mark; accent hues decorative only |
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { applyLanguage, LANGUAGE_REGISTRY } from "@nebutra/theme";
|
|
80
|
+
applyLanguage("vanta"); // built-in package resolved automatically
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Multi-language catalog (scoped `html[data-brand]` only):
|
|
84
|
+
|
|
85
|
+
```css
|
|
86
|
+
@import "@nebutra/theme/skins.css";
|
|
87
|
+
/* then: document.documentElement.dataset.brand = "raycast" */
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Why recipe exists
|
|
91
|
+
|
|
92
|
+
Linear and GSAP both dark — but **button language differs**. Color-only skins fail Create Center.
|
|
93
|
+
`Button` / `Badge` defaults + Card/Input elevation/heights consume recipe vars so packages flip solid ↔ outline ↔ no-shadow without call-site edits.
|
|
94
|
+
|
|
95
|
+
Governed surface: `--btn-default-*`, `--badge-default-*`, `--control-height-*`, `--elevation-card|control|raised`, `--font-weight-medium`.
|
|
96
|
+
**Not governed (allowed):** VI logo hex, OAuth marks, decorative demos, trial/turbo special fills.
|
|
97
|
+
|
|
98
|
+
## Runtime apply (Create Center preview)
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import {
|
|
102
|
+
compileReferoTokens,
|
|
103
|
+
applyBrandPackage,
|
|
104
|
+
clearBrand,
|
|
105
|
+
restorePersistedBrand,
|
|
106
|
+
validateBrandPackage,
|
|
107
|
+
useBrand,
|
|
108
|
+
useBrandIframePreview,
|
|
109
|
+
} from "@nebutra/tokens";
|
|
110
|
+
|
|
111
|
+
const { brand, warnings } = compileReferoTokens({ tokens, designMd, id: "gsap" });
|
|
112
|
+
const v = validateBrandPackage(brand);
|
|
113
|
+
if (!v.ok) throw new Error(v.errors.join(", "));
|
|
114
|
+
|
|
115
|
+
applyBrandPackage(brand, { persist: true });
|
|
116
|
+
clearBrand({ persist: true });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### React hooks
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
// Host app
|
|
123
|
+
const { brand, apply, clear } = useBrand({ autoRestore: true });
|
|
124
|
+
|
|
125
|
+
// Multi-tenant iframe preview
|
|
126
|
+
const { iframeRef, apply: preview, writePreviewDocument } = useBrandIframePreview({
|
|
127
|
+
baseStylesheetHrefs: ["/tokens-preset.css"],
|
|
128
|
+
});
|
|
129
|
+
// <iframe ref={iframeRef} />
|
|
130
|
+
// preview(brand) or writePreviewDocument(brand, "<button class='btn-brand-default'>CTA</button>")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Fonts
|
|
134
|
+
|
|
135
|
+
- `typography.faces[]` → emitted as `@font-face` (swap URL for Create Center CDN/upload).
|
|
136
|
+
|
|
137
|
+
### design-sync
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { getDesignSync, compileBrandFromTokenSets } from "@nebutra/design-sync";
|
|
141
|
+
|
|
142
|
+
const sync = await getDesignSync();
|
|
143
|
+
const { sets } = await sync.pull();
|
|
144
|
+
const { brand, css, warnings } = compileBrandFromTokenSets(sets, { id: "tenant-a" });
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
design-sync brand --json --id tenant-a
|
|
149
|
+
```
|