@nebutra/design-tokens 0.1.2 → 2.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/README.md +1 -1
- package/build/css/dark.css +65 -49
- package/build/css/light.css +62 -46
- package/build/css/styles.generated.css +477 -115
- package/build/css/theme-inline.css +86 -15
- package/build/tailwind/dark.preset.cjs +64 -36
- package/build/tailwind/light.preset.cjs +62 -34
- package/build/ts/dark.d.ts +79 -9
- package/build/ts/dark.js +75 -49
- package/build/ts/light.d.ts +76 -12
- package/build/ts/light.js +72 -46
- package/package.json +18 -1
- package/scripts/css-var-name.d.mts +2 -0
- package/scripts/css-var-name.mjs +98 -0
- package/scripts/derive-border-tier.d.mts +36 -0
- package/scripts/derive-border-tier.mjs +346 -0
- package/style-dictionary.config.mjs +175 -89
- package/tokens/core.json +33 -18
- package/tokens/semantic.json +0 -12
- package/tokens/themes/dark.json +594 -109
- package/tokens/themes/light.json +535 -103
|
@@ -24,6 +24,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
24
24
|
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import StyleDictionary from "style-dictionary";
|
|
27
|
+
import { multiThemeName, pathToCssVarName } from "./scripts/css-var-name.mjs";
|
|
28
|
+
import { assertLadder, deriveBorderTier } from "./scripts/derive-border-tier.mjs";
|
|
27
29
|
|
|
28
30
|
// Multi-mood oklch catalog removed (2026.07). Only light/dark modes remain.
|
|
29
31
|
// Design-language swap lives in @nebutra/theme (Brand Packages / skins.css).
|
|
@@ -49,10 +51,13 @@ function formatCssWithBiome(content, stdinFilePath) {
|
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
// Mirror of FONT_REGISTRY in packages/design/fonts/src/index.ts (duplicated
|
|
55
|
+
// because this generator is plain ESM and cannot import the TS source).
|
|
52
56
|
const FONT_REGISTRY = {
|
|
53
57
|
geist: "--font-geist-sans",
|
|
54
58
|
"geist sans": "--font-geist-sans",
|
|
55
59
|
"geist mono": "--font-geist-mono",
|
|
60
|
+
"vivo sans sc": "--font-noto-sans-sc",
|
|
56
61
|
inter: "--font-inter",
|
|
57
62
|
"space grotesk": "--font-space-grotesk",
|
|
58
63
|
"playfair display": "--font-playfair-display",
|
|
@@ -85,85 +90,12 @@ function withRegistryFont(stack) {
|
|
|
85
90
|
return variable ? `var(${variable}), ${stack}` : stack;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
/**
|
|
89
|
-
* Custom CSS variable namer.
|
|
90
|
-
*
|
|
91
|
-
* Maps DTCG paths to the same names the legacy SSOT uses.
|
|
92
|
-
*/
|
|
93
|
-
function pathToCssVarName(token) {
|
|
94
|
-
const path = token.path;
|
|
95
|
-
const [head, ...rest] = path;
|
|
96
|
-
|
|
97
|
-
// Drop primitive `color.` prefix → keep child name.
|
|
98
|
-
if (head === "color") {
|
|
99
|
-
if (rest[0] === "white" || rest[0] === "black") return null;
|
|
100
|
-
if (rest[0] === "tertiary-purple") return "brand-tertiary";
|
|
101
|
-
return rest.join("-");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (head === "size") return rest.join("-");
|
|
105
|
-
if (head === "duration") return `duration-${rest.join("-")}`;
|
|
106
|
-
if (head === "easing") return `ease-${rest.join("-")}`;
|
|
107
|
-
if (head === "fontFamily") return `font-${rest.join("-")}`;
|
|
108
|
-
|
|
109
|
-
if (head === "brand") {
|
|
110
|
-
if (rest[0] === "gradient") {
|
|
111
|
-
const sub = rest.slice(1).join("-");
|
|
112
|
-
return sub === "primary" ? "brand-gradient" : `brand-gradient-${sub}`;
|
|
113
|
-
}
|
|
114
|
-
return `brand-${rest.join("-")}`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (head === "status") return `status-${rest.join("-")}`;
|
|
118
|
-
|
|
119
|
-
if (head === "container") return `container-${rest.join("-")}`;
|
|
120
|
-
if (head === "radius") {
|
|
121
|
-
return rest[0] === "default" ? "radius" : `radius-${rest.join("-")}`;
|
|
122
|
-
}
|
|
123
|
-
if (head === "transition") {
|
|
124
|
-
if (rest[0] === "shorthand") return "transition";
|
|
125
|
-
if (rest[0] === "default") return null; // composite — emitted via post-process
|
|
126
|
-
return `transition-${rest.join("-")}`;
|
|
127
|
-
}
|
|
128
|
-
if (head === "focusRing") {
|
|
129
|
-
if (rest[0] === "default") return "focus-ring";
|
|
130
|
-
return `focus-ring-${rest.join("-")}`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (head === "scale") return rest.join("-");
|
|
134
|
-
if (head === "shadcn") return rest.join("-");
|
|
135
|
-
if (head === "ds") return `ds-${rest.join("-")}`;
|
|
136
|
-
if (head === "elevation") return `elevation-${rest.join("-")}`;
|
|
137
|
-
|
|
138
|
-
if (head === "theme") return null;
|
|
139
|
-
|
|
140
|
-
if (head === "shadow") return `shadow-${rest.join("-")}`;
|
|
141
|
-
|
|
142
|
-
return path.join("-");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
93
|
StyleDictionary.registerTransform({
|
|
146
94
|
name: "name/nebutra/css",
|
|
147
95
|
type: "name",
|
|
148
96
|
transform: (token) => pathToCssVarName(token) ?? `__skip__${token.path.join("-")}`,
|
|
149
97
|
});
|
|
150
98
|
|
|
151
|
-
/**
|
|
152
|
-
* CSS variable namer for theme token trees (light/dark).
|
|
153
|
-
* Emits Tailwind v4 `@theme`-compatible names: color.primary → --color-primary.
|
|
154
|
-
*/
|
|
155
|
-
function multiThemeName(token) {
|
|
156
|
-
const path = token.path;
|
|
157
|
-
const [head, ...rest] = path;
|
|
158
|
-
if (head === "theme") return null;
|
|
159
|
-
if (head === "color") return `color-${rest.join("-")}`;
|
|
160
|
-
if (head === "radius") return `radius-${rest.join("-")}`;
|
|
161
|
-
if (head === "fontFamily") return `font-${rest.join("-")}`;
|
|
162
|
-
if (head === "shadow") return `shadow-${rest.join("-")}`;
|
|
163
|
-
if (head === "transition") return `transition-${rest.join("-")}`;
|
|
164
|
-
return path.join("-");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
99
|
StyleDictionary.registerTransform({
|
|
168
100
|
name: "name/nebutra/css/multi-theme",
|
|
169
101
|
type: "name",
|
|
@@ -292,6 +224,60 @@ function registerLayeredNameTransform({ name, sources, nameForPath }) {
|
|
|
292
224
|
return name;
|
|
293
225
|
}
|
|
294
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Border-tier derivation.
|
|
229
|
+
*
|
|
230
|
+
* core.json ships 11 primitive stops; the functional layer needs 12 steps. The
|
|
231
|
+
* gap used to be closed by aliasing 6/7/8 to the same primitives as 3/4/5,
|
|
232
|
+
* which made the border tier identical to the component-background tier. Steps
|
|
233
|
+
* 6/7/8 are now computed from the scale's own 5 and 9 (or 10, for bright
|
|
234
|
+
* scales) in OKLab. See scripts/derive-border-tier.mjs for the rule.
|
|
235
|
+
*
|
|
236
|
+
* This runs as a global preprocessor, so it sees the merged core + semantic +
|
|
237
|
+
* theme tree and can resolve the `{color.…}` references the anchors point at.
|
|
238
|
+
* Deriving here rather than authoring literals in core.json is what makes the
|
|
239
|
+
* fix survive `scripts/brand-apply.ts`, which rewrites the three brand palettes
|
|
240
|
+
* wholesale over a fixed 11-stop list on every rebrand.
|
|
241
|
+
*/
|
|
242
|
+
StyleDictionary.registerPreprocessor({
|
|
243
|
+
name: "nebutra/derive-border-tier",
|
|
244
|
+
preprocessor: (tokens) => {
|
|
245
|
+
deriveBorderTier(tokens);
|
|
246
|
+
return tokens;
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* A token description must not be able to break the build.
|
|
252
|
+
*
|
|
253
|
+
* $description is emitted verbatim into `/** … *\/` comments in the CSS and the
|
|
254
|
+
* TypeScript declarations, so a description containing the comment terminator
|
|
255
|
+
* closes it early and the generated .ts stops parsing. Writing an asterisk-slash inside a note
|
|
256
|
+
* about a colour ramp is enough to do it, and the failure surfaces as a
|
|
257
|
+
* sixty-line minified prettier stack trace pointing at a blank line in
|
|
258
|
+
* build/ts/estree.ts — nowhere near the token that caused it.
|
|
259
|
+
*
|
|
260
|
+
* Neutralised rather than rejected: the author's sentence still reads the same,
|
|
261
|
+
* and there is nothing for a contributor to learn or remember.
|
|
262
|
+
*/
|
|
263
|
+
StyleDictionary.registerPreprocessor({
|
|
264
|
+
name: "nebutra/safe-descriptions",
|
|
265
|
+
preprocessor: (tokens) => {
|
|
266
|
+
const walk = (node) => {
|
|
267
|
+
if (node === null || typeof node !== "object") return;
|
|
268
|
+
if (typeof node.$description === "string" && node.$description.includes("*/")) {
|
|
269
|
+
node.$description = node.$description.replaceAll("*/", "* /");
|
|
270
|
+
}
|
|
271
|
+
for (const [key, child] of Object.entries(node)) {
|
|
272
|
+
if (key === "$description") continue;
|
|
273
|
+
walk(child);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
walk(tokens);
|
|
277
|
+
return tokens;
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
295
281
|
StyleDictionary.registerTransform({
|
|
296
282
|
name: "color/nebutra/passthrough",
|
|
297
283
|
type: "value",
|
|
@@ -330,7 +316,7 @@ const buildMode = ({ mode, selector, sources, outputFile, nameTransform = "name/
|
|
|
330
316
|
|
|
331
317
|
return {
|
|
332
318
|
log: { verbosity: "default", warnings: "error" },
|
|
333
|
-
preprocessors: ["tokens-studio"],
|
|
319
|
+
preprocessors: ["tokens-studio", "nebutra/safe-descriptions", "nebutra/derive-border-tier"],
|
|
334
320
|
include: sources.slice(0, -1),
|
|
335
321
|
source: sources.slice(-1),
|
|
336
322
|
platforms: {
|
|
@@ -448,6 +434,35 @@ for (const cfg of configs) {
|
|
|
448
434
|
await sd.buildAllPlatforms();
|
|
449
435
|
}
|
|
450
436
|
|
|
437
|
+
// ─── Ladder invariant ────────────────────────────────────────────────────────
|
|
438
|
+
//
|
|
439
|
+
// Fail the build if the 12-step scales regress to the state this package was
|
|
440
|
+
// fixed out of: duplicated steps, or a ramp (1…8) that is not monotonic in
|
|
441
|
+
// OKLab lightness. Step 9 is exempt from monotonicity — on bright scales the
|
|
442
|
+
// solid accent legitimately sits off the ramp.
|
|
443
|
+
//
|
|
444
|
+
// This is the guard that makes the derivation load-bearing rather than
|
|
445
|
+
// decorative: if the preprocessor ever stops running, the placeholder `$value`
|
|
446
|
+
// aliases collapse 6/7/8 onto step 5 and this throws instead of shipping.
|
|
447
|
+
for (const mode of ["light", "dark"]) {
|
|
448
|
+
const css = await readFile(`build/css/${mode}.css`, "utf8");
|
|
449
|
+
const scales = new Map();
|
|
450
|
+
for (const [, scaleName, step, value] of css.matchAll(
|
|
451
|
+
/--(neutral|blue|cyan)-(\d{1,2}):\s*(#[0-9a-f]{6})\s*;/giu,
|
|
452
|
+
)) {
|
|
453
|
+
const key = scaleName.toLowerCase();
|
|
454
|
+
if (!scales.has(key)) scales.set(key, {});
|
|
455
|
+
scales.get(key)[step] = value;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const problems = [...scales].flatMap(([name, values]) => assertLadder(`${mode} ${name}`, values));
|
|
459
|
+
if (problems.length > 0) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
`[design-tokens] 12-step scale ladder invariant violated:\n ${problems.join("\n ")}`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
451
466
|
const tsIndex = `${tsExportModes
|
|
452
467
|
.map((mode) => `export * as ${toNamespaceIdentifier(mode)} from "./${mode}.js";`)
|
|
453
468
|
.join("\n")}
|
|
@@ -788,6 +803,22 @@ function buildTailwindThemeInline() {
|
|
|
788
803
|
--radius-3xl: 1.5rem;
|
|
789
804
|
--radius-full: 9999px;
|
|
790
805
|
|
|
806
|
+
/* Containers — the page-width contract from CLAUDE.md, as real utilities.
|
|
807
|
+
max-w-text (reading), max-w-content (pricing/blog), max-w-wide (feature bento, navbar).
|
|
808
|
+
|
|
809
|
+
These values also reach :root through the semantic layer, so var(--container-wide) keeps
|
|
810
|
+
working; what was missing was the Tailwind side. Without it there is no max-w-wide to reach
|
|
811
|
+
for, and 92 call sites across apps/** had typed max-w-wide instead — a decided number
|
|
812
|
+
living in a className, where changing the token cannot move it.
|
|
813
|
+
|
|
814
|
+
Literals rather than var(--container-wide): Tailwind emits an @theme entry into its own
|
|
815
|
+
layer, so a same-named reference becomes self-referential and survives only because the
|
|
816
|
+
semantic :root happens to come later in the cascade. Duplicating the number and pinning it
|
|
817
|
+
with a parity test is the honest trade — see design-tokens' container parity test. */
|
|
818
|
+
--container-text: 896px;
|
|
819
|
+
--container-content: 1152px;
|
|
820
|
+
--container-wide: 1400px;
|
|
821
|
+
|
|
791
822
|
/* Shadows */
|
|
792
823
|
--shadow-xs: var(--elevation-xs);
|
|
793
824
|
--shadow-sm: var(--elevation-sm);
|
|
@@ -798,23 +829,78 @@ function buildTailwindThemeInline() {
|
|
|
798
829
|
--shadow-brand: var(--elevation-brand);
|
|
799
830
|
--shadow-brand-lg: var(--elevation-brand-lg);
|
|
800
831
|
|
|
801
|
-
/*
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
--
|
|
805
|
-
--
|
|
832
|
+
/* shadow-ambient-md / shadow-glass-lg / shadow-sheen — so marketing surfaces
|
|
833
|
+
reach for a step on the ramp instead of spelling out a blur and a spread.
|
|
834
|
+
Aliased to var() rather than inlined so the dark theme's values apply. */
|
|
835
|
+
--shadow-ambient-sm: var(--elevation-ambient-sm);
|
|
836
|
+
--shadow-ambient-md: var(--elevation-ambient-md);
|
|
837
|
+
--shadow-ambient-lg: var(--elevation-ambient-lg);
|
|
838
|
+
--shadow-glass-sm: var(--elevation-glass-sm);
|
|
839
|
+
--shadow-glass-md: var(--elevation-glass-md);
|
|
840
|
+
--shadow-glass-lg: var(--elevation-glass-lg);
|
|
841
|
+
--shadow-sheen: var(--elevation-sheen);
|
|
842
|
+
|
|
843
|
+
/* Coloured and centred elevation. shadow-ambient-glow is the one
|
|
844
|
+
non-directional step; shadow-glow-accent|-lg|-sm carry a brand-accent halo
|
|
845
|
+
so a glass card never hand-types the accent as raw rgba; shadow-glow-primary
|
|
846
|
+
tints the xl geometry with the action fill. */
|
|
847
|
+
--shadow-ambient-glow: var(--elevation-ambient-glow);
|
|
848
|
+
--shadow-glow-accent: var(--elevation-glow-accent);
|
|
849
|
+
--shadow-glow-accent-lg: var(--elevation-glow-accent-lg);
|
|
850
|
+
--shadow-glow-accent-sm: var(--elevation-glow-accent-sm);
|
|
851
|
+
--shadow-glow-primary: var(--elevation-glow-primary);
|
|
852
|
+
|
|
853
|
+
/* Motion — Easing curves.
|
|
854
|
+
Indirect, like the shadows above: an @theme entry holding a literal is
|
|
855
|
+
inlined into the utility, so the ease-out class compiled to a fixed
|
|
856
|
+
cubic-bezier and no Brand Package could move it. Pointing at the runtime
|
|
857
|
+
rail in static/base.css makes it follow the language the page is under.
|
|
858
|
+
(No backticks in this block — it is inside a JS template literal.) */
|
|
859
|
+
--ease-in: var(--motion-ease-in);
|
|
860
|
+
--ease-out: var(--motion-ease-out);
|
|
861
|
+
--ease-brand: var(--motion-ease-brand);
|
|
862
|
+
--ease-in-out: var(--motion-ease-in-out);
|
|
863
|
+
--ease-spring: var(--motion-ease-spring);
|
|
806
864
|
|
|
807
865
|
/* Motion — Durations (four-rail; see core.json:duration). Names denote intent, not relative speed. */
|
|
808
|
-
--duration-micro:
|
|
809
|
-
--duration-flow:
|
|
810
|
-
--duration-reveal:
|
|
811
|
-
--duration-cinematic:
|
|
812
|
-
|
|
813
|
-
/*
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
866
|
+
--duration-micro: var(--motion-duration-micro);
|
|
867
|
+
--duration-flow: var(--motion-duration-flow);
|
|
868
|
+
--duration-reveal: var(--motion-duration-reveal);
|
|
869
|
+
--duration-cinematic: var(--motion-duration-cinematic);
|
|
870
|
+
|
|
871
|
+
/* The same four rails on the namespace Tailwind reads for duration-*.
|
|
872
|
+
--duration-* is the variable other CSS consumes; the utility class is built
|
|
873
|
+
from --transition-duration-*, and only that one. Without these,
|
|
874
|
+
duration-micro and duration-reveal are class names that generate nothing:
|
|
875
|
+
they were in use in the component library and on the marketing site, and
|
|
876
|
+
every one of them silently fell back to the 150ms default. */
|
|
877
|
+
--transition-duration-micro: var(--motion-duration-micro);
|
|
878
|
+
--transition-duration-flow: var(--motion-duration-flow);
|
|
879
|
+
--transition-duration-reveal: var(--motion-duration-reveal);
|
|
880
|
+
--transition-duration-cinematic: var(--motion-duration-cinematic);
|
|
881
|
+
|
|
882
|
+
/* Likewise for ease-*: the utility namespace is --ease-* already, so the four
|
|
883
|
+
above are reachable as ease-brand / ease-spring without further mapping. */
|
|
884
|
+
|
|
885
|
+
/* Spacing — DO NOT register --spacing-sm|md|lg|xl|2xl here.
|
|
886
|
+
Tailwind v4 puts every --spacing-{key} on the shared size rail, so
|
|
887
|
+
max-w-sm / max-w-2xl (and width/height/gap utilities with the same keys)
|
|
888
|
+
resolve to the breathing-room lengths (0.75rem to 3rem) instead of the
|
|
889
|
+
container scale (24rem to 42rem). That shipped as one-word-per-line prose
|
|
890
|
+
and a 32px install box on nebutra.com (measured 2026-08-03).
|
|
891
|
+
Brand Packages still override --space-source-*; consumers must read
|
|
892
|
+
those vars directly (var(--space-source-md, ...)), never via a @theme
|
|
893
|
+
--spacing-{step} alias. --spacing itself (0.25rem arithmetic base) stays
|
|
894
|
+
untouched. See static/base.css. */
|
|
895
|
+
|
|
896
|
+
/* Font family — Geist keeps Latin + numerals, self-hosted Noto Sans SC takes
|
|
897
|
+
CJK, system CJK faces behind it. Order is the design decision; see the
|
|
898
|
+
fontFamily $description in tokens/core.json. Mirror of those tokens — keep
|
|
899
|
+
the two in sync (verify:parity compares them). */
|
|
900
|
+
--font-sans: var(--font-geist-sans), "Geist", var(--font-noto-sans-sc, "Noto Sans SC"), "PingFang SC", "Microsoft YaHei", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
|
901
|
+
--font-cn: var(--font-geist-sans), "Geist", var(--font-noto-sans-sc, "Noto Sans SC"), "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
902
|
+
--font-display: var(--font-geist-sans), "Geist", var(--font-noto-sans-sc, "Noto Sans SC"), sans-serif;
|
|
903
|
+
--font-heading: var(--font-geist-sans), "Geist", var(--font-noto-sans-sc, "Noto Sans SC"), sans-serif;
|
|
818
904
|
--font-mono: var(--font-geist-mono), "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
|
819
905
|
}
|
|
820
906
|
`;
|
package/tokens/core.json
CHANGED
|
@@ -267,52 +267,66 @@
|
|
|
267
267
|
}
|
|
268
268
|
},
|
|
269
269
|
"radius": {
|
|
270
|
+
"$description": "Radius ladder. Every step names a job; nothing on the ladder exists only because a t-shirt scale has that size. Steps below 1rem climb by 2px so a nested corner can sit one step inside its parent without looking loose; above 1rem the ladder jumps, because a large panel does not read a 2px difference. The three intent aliases (button, card, panel) are the names product code should reach for — they resolve onto lg/xl/2xl today and exist so a future re-shape moves one value instead of a thousand call sites. Nothing above 3xl: a corner larger than 24px is a composition, not a surface, and belongs in the component that draws it.",
|
|
270
271
|
"none": {
|
|
271
272
|
"$value": "0",
|
|
272
|
-
"$type": "dimension"
|
|
273
|
+
"$type": "dimension",
|
|
274
|
+
"$description": "Job: flush. Edge-to-edge surfaces — a full-bleed table row, a segment welded to its neighbour, an inset that must not round twice."
|
|
275
|
+
},
|
|
276
|
+
"xs": {
|
|
277
|
+
"$value": "0.125rem",
|
|
278
|
+
"$type": "dimension",
|
|
279
|
+
"$description": "Job: swatch. The smallest corner that still reads as intentional at 8-12px: chart legend keys, mark highlights, progress and sparkline bar caps. Added because 18 call sites had hand-written 1px, 2px and 3px corners with no step to reach for. Matches the bottom of the Tailwind scale, so rounded-xs and var(--radius-xs) agree."
|
|
273
280
|
},
|
|
274
281
|
"sm": {
|
|
275
282
|
"$value": "0.25rem",
|
|
276
|
-
"$type": "dimension"
|
|
283
|
+
"$type": "dimension",
|
|
284
|
+
"$description": "Job: inner. The corner of something nested inside a control — a thumbnail inside a chip, a swatch inside a tile, a code span inside a paragraph. One step inside md."
|
|
277
285
|
},
|
|
278
286
|
"md": {
|
|
279
287
|
"$value": "0.375rem",
|
|
280
|
-
"$type": "dimension"
|
|
288
|
+
"$type": "dimension",
|
|
289
|
+
"$description": "Job: control. The default for anything you can click or type into at text height: input, select trigger, small button, menu item, badge with square corners. The most-used step on the ladder."
|
|
281
290
|
},
|
|
282
291
|
"lg": {
|
|
283
292
|
"$value": "0.5rem",
|
|
284
|
-
"$type": "dimension"
|
|
293
|
+
"$type": "dimension",
|
|
294
|
+
"$description": "Job: container. Things that hold controls rather than being one: popover, dropdown surface, tooltip, toolbar, inline code block. Aliased as radius-button."
|
|
285
295
|
},
|
|
286
296
|
"xl": {
|
|
287
297
|
"$value": "0.75rem",
|
|
288
|
-
"$type": "dimension"
|
|
298
|
+
"$type": "dimension",
|
|
299
|
+
"$description": "Job: card. A discrete surface in a list or grid that carries its own padding and elevation. Aliased as radius-card."
|
|
289
300
|
},
|
|
290
301
|
"2xl": {
|
|
291
302
|
"$value": "1rem",
|
|
292
|
-
"$type": "dimension"
|
|
303
|
+
"$type": "dimension",
|
|
304
|
+
"$description": "Job: panel. A region that holds cards — bento cell, dialog, sheet, dashboard section. Aliased as radius-panel."
|
|
293
305
|
},
|
|
294
306
|
"3xl": {
|
|
295
307
|
"$value": "1.5rem",
|
|
296
|
-
"$type": "dimension"
|
|
308
|
+
"$type": "dimension",
|
|
309
|
+
"$description": "Job: frame. The outermost shell of a marketing or docs composition — mockup window, hero frame, screenshot chrome. The top of the ladder."
|
|
297
310
|
},
|
|
298
311
|
"full": {
|
|
299
312
|
"$value": "9999px",
|
|
300
|
-
"$type": "dimension"
|
|
313
|
+
"$type": "dimension",
|
|
314
|
+
"$description": "Job: pill. Fully round by intent, not by size: avatar, dot, toggle track, tag. Kept as one step, not a pair, so there is exactly one way to say round."
|
|
301
315
|
},
|
|
302
316
|
"button": {
|
|
303
317
|
"$value": "0.5rem",
|
|
304
318
|
"$type": "dimension",
|
|
305
|
-
"$description": "
|
|
319
|
+
"$description": "Intent alias for the container step. Reach for this in product code instead of lg so the shape can be re-tuned in one place."
|
|
306
320
|
},
|
|
307
321
|
"card": {
|
|
308
322
|
"$value": "0.75rem",
|
|
309
323
|
"$type": "dimension",
|
|
310
|
-
"$description": "
|
|
324
|
+
"$description": "Intent alias for the card step. Reach for this in product code instead of xl."
|
|
311
325
|
},
|
|
312
326
|
"panel": {
|
|
313
327
|
"$value": "1rem",
|
|
314
328
|
"$type": "dimension",
|
|
315
|
-
"$description": "
|
|
329
|
+
"$description": "Intent alias for the panel step. Reach for this in product code instead of 2xl."
|
|
316
330
|
}
|
|
317
331
|
}
|
|
318
332
|
},
|
|
@@ -345,7 +359,7 @@
|
|
|
345
359
|
"$type": "cubicBezier"
|
|
346
360
|
},
|
|
347
361
|
"out": {
|
|
348
|
-
"$value": "cubic-bezier(0,
|
|
362
|
+
"$value": "cubic-bezier(0.16, 1, 0.3, 1)",
|
|
349
363
|
"$type": "cubicBezier"
|
|
350
364
|
},
|
|
351
365
|
"in-out": {
|
|
@@ -403,21 +417,22 @@
|
|
|
403
417
|
}
|
|
404
418
|
},
|
|
405
419
|
"fontFamily": {
|
|
406
|
-
"$description": "Geist + Geist Mono
|
|
420
|
+
"$description": "Geist + Geist Mono for Latin, self-hosted Noto Sans SC (SIL OFL) for CJK, system CJK faces (PingFang SC / Microsoft YaHei) as the fallback behind it. ORDER IS THE DESIGN DECISION: Geist comes first so it keeps Latin and the numerals (tabular figures, tighter x-height — what dense dashboard tables need, and the locked UI face); Noto Sans SC follows so only CJK falls through to it. Reversed, Noto would take the Latin too and its Latin is not as good as Geist's for UI. Each stack prefixes the next/font variable (--font-geist-sans / --font-geist-mono / --font-noto-sans-sc) so the actual loaded face wins over the literal family name, which the browser cannot resolve directly once next/font assigns a generated unique font-family identifier. The Noto entry carries a literal fallback inside var() so a surface that never applies cjkFontClassName degrades to the family name instead of making the whole declaration invalid at computed-value time. `cn` is kept rather than deleted because it still has a job: the `font-cn` utility and the `html:lang(zh|ja|ko) body` auto-activation rule in static/base.css both set font-family to that token ALONE (they exist for the CJK line-height / word-break metrics), so it governs Chinese pages entirely — including their Latin.",
|
|
407
421
|
"sans": {
|
|
408
|
-
"$value": "var(--font-geist-sans), \"Geist\", \"Noto Sans SC\", \"PingFang SC\", \"Microsoft YaHei\", -apple-system, BlinkMacSystemFont, system-ui, sans-serif",
|
|
422
|
+
"$value": "var(--font-geist-sans), \"Geist\", var(--font-noto-sans-sc, \"Noto Sans SC\"), \"PingFang SC\", \"Microsoft YaHei\", -apple-system, BlinkMacSystemFont, system-ui, sans-serif",
|
|
409
423
|
"$type": "fontFamily"
|
|
410
424
|
},
|
|
411
425
|
"cn": {
|
|
412
|
-
"$value": "\"Noto Sans SC\", \"PingFang SC\", \"Microsoft YaHei\",
|
|
413
|
-
"$type": "fontFamily"
|
|
426
|
+
"$value": "var(--font-geist-sans), \"Geist\", var(--font-noto-sans-sc, \"Noto Sans SC\"), \"PingFang SC\", \"Microsoft YaHei\", sans-serif",
|
|
427
|
+
"$type": "fontFamily",
|
|
428
|
+
"$description": "CJK-locale stack — same order as sans, minus the Latin-only system faces"
|
|
414
429
|
},
|
|
415
430
|
"display": {
|
|
416
|
-
"$value": "var(--font-geist-sans), \"Geist\", \"Noto Sans SC\", sans-serif",
|
|
431
|
+
"$value": "var(--font-geist-sans), \"Geist\", var(--font-noto-sans-sc, \"Noto Sans SC\"), sans-serif",
|
|
417
432
|
"$type": "fontFamily"
|
|
418
433
|
},
|
|
419
434
|
"heading": {
|
|
420
|
-
"$value": "var(--font-geist-sans), \"Geist\", \"Noto Sans SC\", sans-serif",
|
|
435
|
+
"$value": "var(--font-geist-sans), \"Geist\", var(--font-noto-sans-sc, \"Noto Sans SC\"), sans-serif",
|
|
421
436
|
"$type": "fontFamily"
|
|
422
437
|
},
|
|
423
438
|
"mono": {
|
package/tokens/semantic.json
CHANGED
|
@@ -87,18 +87,6 @@
|
|
|
87
87
|
"cinematic": { "$value": "{duration.cinematic}", "$type": "duration" }
|
|
88
88
|
}
|
|
89
89
|
},
|
|
90
|
-
"transition": {
|
|
91
|
-
"duration": {
|
|
92
|
-
"$value": "{duration.flow}",
|
|
93
|
-
"$type": "duration",
|
|
94
|
-
"$description": "Default transition duration — 200ms flow rail."
|
|
95
|
-
},
|
|
96
|
-
"easing": {
|
|
97
|
-
"$value": "ease-out",
|
|
98
|
-
"$type": "string",
|
|
99
|
-
"$description": "ease-out keyword form (CSS-friendly). Equivalent to cubic-bezier(0,0,0.2,1)."
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
90
|
"focusRing": {
|
|
103
91
|
"default": {
|
|
104
92
|
"$type": "string",
|