@stridge/noctis-design-tokens 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +226 -0
  2. package/dist/apply-scopes.d.ts +12 -0
  3. package/dist/apply-scopes.js +30 -0
  4. package/dist/css.d.ts +10 -0
  5. package/dist/css.js +36 -0
  6. package/dist/graph/components.d.ts +6 -0
  7. package/dist/graph/components.js +4488 -0
  8. package/dist/graph/index.d.ts +11 -0
  9. package/dist/graph/index.js +26 -0
  10. package/dist/graph/inputs.d.ts +1 -0
  11. package/dist/graph/inputs.js +23 -0
  12. package/dist/graph/model.d.ts +205 -0
  13. package/dist/graph/model.js +282 -0
  14. package/dist/graph/registry.d.ts +67 -0
  15. package/dist/graph/registry.js +118 -0
  16. package/dist/graph/roles.d.ts +5 -0
  17. package/dist/graph/roles.js +151 -0
  18. package/dist/graph/scales.d.ts +1 -0
  19. package/dist/graph/scales.js +1296 -0
  20. package/dist/graph/serialize.d.ts +16 -0
  21. package/dist/graph/serialize.js +25 -0
  22. package/dist/graph/statics.d.ts +1 -0
  23. package/dist/graph/statics.js +419 -0
  24. package/dist/index.d.ts +26 -0
  25. package/dist/index.js +25 -0
  26. package/dist/palettes.d.ts +70 -0
  27. package/dist/palettes.js +114 -0
  28. package/dist/react/provider.d.ts +23 -0
  29. package/dist/react/provider.js +28 -0
  30. package/dist/react.d.ts +3 -0
  31. package/dist/react.js +3 -0
  32. package/dist/scales.d.ts +186 -0
  33. package/dist/scales.js +186 -0
  34. package/dist/semantic.d.ts +36 -0
  35. package/dist/semantic.js +49 -0
  36. package/dist/swatches.d.ts +24 -0
  37. package/dist/swatches.js +57 -0
  38. package/dist/tokens.css +2607 -0
  39. package/dist/tokens.dtcg.json +3475 -0
  40. package/dist/tokens.json +14658 -0
  41. package/dist/tokens.tailwind.css +479 -0
  42. package/package.json +67 -0
@@ -0,0 +1,11 @@
1
+ import { ROLE_VAR_PREFIX } from "./registry.js";
2
+ import { DesignToken } from "./model.js";
3
+ import { SEMANTIC_IDS } from "./roles.js";
4
+ import { COMPONENT_DECLARATIONS } from "./components.js";
5
+ //#region src/graph/index.d.ts
6
+ /** Every authored token — semantics, statics, foundations, and seeds — in tier order. */
7
+ declare const DESIGN_TOKENS: readonly DesignToken[];
8
+ /** A semantic-role id from the engine-backed semantic set. */
9
+ type SemanticRoleId = (typeof SEMANTIC_IDS)[number];
10
+ //#endregion
11
+ export { DESIGN_TOKENS, SemanticRoleId };
@@ -0,0 +1,26 @@
1
+ import { COMPONENT_DECLARATIONS } from "./components.js";
2
+ import { SEED_TOKENS } from "./inputs.js";
3
+ import "./registry.js";
4
+ import { validateGraph } from "./model.js";
5
+ import { SEMANTIC_TOKENS } from "./roles.js";
6
+ import { FOUNDATION_TOKENS } from "./scales.js";
7
+ import { STATIC_SEMANTIC_TOKENS } from "./statics.js";
8
+ //#region src/graph/index.ts
9
+ /**
10
+ * The assembled token graph: the single source the views, generators, and exports derive from.
11
+ *
12
+ * Concatenates the authored semantic, static-semantic, foundation, and seed tokens, holds the
13
+ * component tier, and runs {@link validateGraph} at module load — a malformed graph is a
14
+ * load-time failure, which is the point. {@link ROLE_VAR_PREFIX} is the namespace every
15
+ * canonical token serializes under; the slot-internal `--_` namespace is fixed.
16
+ */
17
+ /** Every authored token — semantics, statics, foundations, and seeds — in tier order. */
18
+ const DESIGN_TOKENS = [
19
+ ...SEMANTIC_TOKENS,
20
+ ...STATIC_SEMANTIC_TOKENS,
21
+ ...FOUNDATION_TOKENS,
22
+ ...SEED_TOKENS
23
+ ];
24
+ validateGraph(DESIGN_TOKENS, COMPONENT_DECLARATIONS);
25
+ //#endregion
26
+ export { DESIGN_TOKENS };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,23 @@
1
+ //#region src/graph/inputs.ts
2
+ function seed(uuid, id, raw, description, usage) {
3
+ return {
4
+ uuid,
5
+ tier: "seed",
6
+ name: {
7
+ kind: "flat",
8
+ id
9
+ },
10
+ value: { raw },
11
+ description,
12
+ usage,
13
+ introduced: "0.0.0"
14
+ };
15
+ }
16
+ /** Seed input knobs — the foundations derive from these. */
17
+ const SEED_TOKENS = [
18
+ seed("d8dec6e4-07ff-403d-ad16-b072a39d36f7", "font-scale", "1", "Type-scale multiplier applied to every named text size.", "The --noctis-seed-font-scale knob the System Controls type-size slider drives."),
19
+ seed("1cb82eee-e029-4538-9817-b0e533570797", "radius", "9999px", "Corner-radius knob the whole rounded scale derives from.", "The --noctis-seed-radius knob; 9999px is the pill Noctis default — controls go fully round while surfaces stay capped."),
20
+ seed("bcb4c342-6046-417d-953c-54aaf3fdf50e", "density", "1", "Density multiplier applied to every spacing step, control height, and region padding.", "The --noctis-seed-density knob the System Controls density picker drives; 1 is the default scale.")
21
+ ];
22
+ //#endregion
23
+ export { SEED_TOKENS };
@@ -0,0 +1,205 @@
1
+ import { ComponentCategory, STATE_REGISTRY, TOKEN_TIERS, TokenCategory } from "./registry.js";
2
+ import { ThemeVar } from "@stridge/noctis-theme-engine";
3
+ import { z } from "zod";
4
+
5
+ //#region src/graph/model.d.ts
6
+ /**
7
+ * How a token reaches Tailwind's `@theme` layer (no `bridge` = canonical-only, reached via
8
+ * `calc()`/`var()` or a component mint):
9
+ * - `color` — `--color-{id}: var(canonical)` in `@theme inline` (the color-utility bridge);
10
+ * - `utility` — reached through a dedicated `@utility` rule the generator authors;
11
+ * - `theme` — a bare Tailwind name in plain `@theme`, carrying the **literal** value
12
+ * duplicated from the canonical (breakpoints — `var()` is rejected in query preludes);
13
+ * - `theme-inline` — a bare Tailwind name in `@theme inline` as `var(canonical)`.
14
+ */
15
+ type TokenBridge = {
16
+ readonly kind: "color";
17
+ } | {
18
+ readonly kind: "utility";
19
+ } | {
20
+ readonly kind: "theme";
21
+ readonly name: string;
22
+ } | {
23
+ readonly kind: "theme-inline";
24
+ readonly name: string;
25
+ };
26
+ /** How a token is grouped in the reference view (display metadata, not the structural category). */
27
+ type TokenGroup = "surface" | "text" | "border" | "accent" | "control" | "status" | "utility" | "chart" | "avatar";
28
+ /**
29
+ * A structured token name. The flat form carries an id verbatim (seed, foundation, and semantic
30
+ * tokens — the category segment comes from `tier`/`category` at serialization, never the id);
31
+ * the component form is `component[-anatomy]-property[-state]`.
32
+ */
33
+ type TokenName = {
34
+ readonly kind: "flat";
35
+ readonly id: string;
36
+ } | {
37
+ readonly kind: "component";
38
+ readonly component: string;
39
+ readonly anatomy?: string;
40
+ readonly property: string;
41
+ readonly state?: (typeof STATE_REGISTRY)[number];
42
+ };
43
+ /**
44
+ * A token's value in the alias graph: a sibling role id, another token's uuid, an engine
45
+ * primitive, or a raw literal. A raw value may carry a light-mode twin (`rawLight`) for the
46
+ * accent-independent palettes that switch on the resolved mode.
47
+ */
48
+ type TokenValue = {
49
+ readonly role: string;
50
+ } | {
51
+ readonly token: string;
52
+ } | {
53
+ readonly primitive: ThemeVar;
54
+ } | {
55
+ readonly raw: string;
56
+ readonly rawLight?: string;
57
+ };
58
+ /** A token's deprecation record — kept so a retired token stays in the graph as a redirect. */
59
+ interface TokenDeprecation {
60
+ readonly version: string;
61
+ readonly comment: string;
62
+ readonly replacedBy: readonly string[];
63
+ }
64
+ /**
65
+ * A design token: stable identity, a structured name, an alias-graph value, and the view
66
+ * metadata the reference surfaces render. Semantic/foundation tokens carry `label`/`group`/
67
+ * `category`/`utility`/`bridge`; seed and component tokens leave them unset (the validator
68
+ * enforces the `category` rule).
69
+ */
70
+ interface DesignToken {
71
+ readonly uuid: string;
72
+ readonly tier: (typeof TOKEN_TIERS)[number];
73
+ readonly name: TokenName;
74
+ readonly value: TokenValue;
75
+ readonly description: string;
76
+ readonly usage: string;
77
+ readonly introduced: string;
78
+ readonly deprecated?: TokenDeprecation;
79
+ readonly label?: string;
80
+ /** Reference-view grouping (display metadata) — formerly `category`; renamed to free the structural sense. */
81
+ readonly group?: TokenGroup;
82
+ /**
83
+ * The structural category — the `{category}` segment of the canonical
84
+ * `--noctis-{category}-{name}`. Required for `foundation` and `semantic` tokens, forbidden
85
+ * for `seed` and `component` tokens; semantic tokens are always `"color"` (all enforced by
86
+ * {@link validateGraph}).
87
+ */
88
+ readonly category?: TokenCategory;
89
+ /**
90
+ * The Tailwind utility (or utility prefix) that paints this token — **display metadata**
91
+ * for the docs/reference surfaces only. Emission is controlled exclusively by `bridge`.
92
+ */
93
+ readonly utility?: string;
94
+ readonly bridge?: TokenBridge;
95
+ /**
96
+ * Per-variant fallback re-points for a component mint: option axis → option value → the fallback
97
+ * the internal var takes under `[data-{axis}={value}]`. The public var stays at the chain head,
98
+ * so one consumer override still wins across every variant.
99
+ */
100
+ readonly variants?: Readonly<Record<string, Readonly<Record<string, TokenValue>>>>;
101
+ }
102
+ /** A primitive's token contract: its anatomy, option axes, consumed bridge utilities, and minted tokens. */
103
+ interface ComponentDeclaration {
104
+ readonly component: string;
105
+ /**
106
+ * The showcase group this component belongs to, from {@link CATEGORY_REGISTRY}. Catalog
107
+ * components carry one; mechanisms (the polymorphic base, radius scope, visually-hidden) leave it
108
+ * unset and never appear in the gallery.
109
+ */
110
+ readonly category?: ComponentCategory;
111
+ readonly anatomy: readonly string[];
112
+ readonly options: Readonly<Record<string, readonly string[]>>;
113
+ readonly states: readonly string[];
114
+ /**
115
+ * The exact bridge utilities the recipes name directly (`"bg-surface"`, `"border-border"`,
116
+ * `"shadow-modal"`, …) — `minted-identity`'s per-owner allow-list. Identity values that flow
117
+ * through a minted internal var are NOT listed here; a role aliased by a mint is the mint's value,
118
+ * not a consumed utility. Validated against {@link consumableUtilities}; pinned to the recipes by
119
+ * the parity test.
120
+ */
121
+ readonly consumes: readonly string[];
122
+ readonly mints: readonly DesignToken[];
123
+ /**
124
+ * Maps a `TokenName.anatomy` key to the `data-slot`s its `--_` internals attach to, for the case
125
+ * where one anatomy's tokens are shared across several slots (Menu's `item` tokens span four item
126
+ * slots). When unset for an anatomy, its internals attach to the single `{component}-{anatomy}`
127
+ * slot (or the root `{component}` slot when the anatomy is omitted).
128
+ */
129
+ readonly slotGroups?: Readonly<Record<string, readonly string[]>>;
130
+ /**
131
+ * Maps an option axis to the `data-slot` that stamps its `data-{axis}` attribute, so a variant
132
+ * re-point keys off the right selector (Menu's `variant` stamps on `menu-item`).
133
+ */
134
+ readonly optionSlots?: Readonly<Record<string, string>>;
135
+ /**
136
+ * Set when the component ships a recipe that is applied as a bare class list — exported as a
137
+ * `*Variants` helper or composed into another element through Base UI's `render`, where the host's
138
+ * own `data-slot` replaces this component's. The root-slot internals are then *also* declared on a
139
+ * `.noctis-{component}` class the recipe carries, so the chain resolves on the recipe element even
140
+ * when its `data-slot` is not `{component}` (Button composed into a `Menu.Trigger`/`Sheet.Close`).
141
+ */
142
+ readonly recipePortable?: boolean;
143
+ /**
144
+ * Optional per-component prefix prepended to every emitted `data-slot` value (NOT the anatomy
145
+ * names, NOT the public `--noctis-*` var names). Set to `"noctis"` on `tabs` so its generated
146
+ * `@layer noctis.components` block targets `[data-slot="noctis-tabs*"]` to match the prefixed
147
+ * slots the migrated `tabs.tsx` stamps. Pre-1.0 alpha: no dual back-compat arm.
148
+ *
149
+ * Doubles as the **migration signal** (D4 final): a declaration carrying a `slotPrefix` has moved
150
+ * to the single-`data-slot` anchor model and drops the `.noctis-{component}` class arm entirely
151
+ * (the generator suppresses the portable hook for it), since D12's `.props()` spreads `data-slot`
152
+ * onto a foreign element so the escape hatch works through the attribute. Un-migrated components
153
+ * keep the dual `:where([data-slot="x"], .noctis-x)` arm unchanged.
154
+ */
155
+ readonly slotPrefix?: string;
156
+ /**
157
+ * A stable marker attribute the component's **root-slot** internals (and the per-`[data-{axis}]`
158
+ * re-points keyed off the root) are declared on instead of `[data-slot="{prefix}-{component}"]`.
159
+ * Set to `"button"` on `button` so its `--_button-*` chain is emitted as `[data-button]{…}` and
160
+ * `[data-button][data-size="sm"]{…}` — the generalization of surface's hand-authored `data-surface`.
161
+ *
162
+ * Button is a class-escape-hatch primitive: `Button.props()` styles a *foreign* element (the rail's
163
+ * `Collapsible.Trigger`, a docs `<a>`) and must NOT clobber that element's own `data-slot`. Keying
164
+ * the internals off a dedicated marker lets the escape hatch carry `data-button` (plus the variant/
165
+ * size axes) without overwriting the host's slot, while the component itself still stamps the catalog
166
+ * `data-slot="{prefix}-{component}"` for SLOTS.md / testing. Only the root-slot internals move to the
167
+ * marker; descendant-anatomy internals (and re-points whose token lives on a descendant) stay on
168
+ * their `[data-slot]`. Requires `slotPrefix` (the marker model is part of the migrated single-anchor
169
+ * world).
170
+ */
171
+ readonly marker?: string;
172
+ }
173
+ /** Structural schema for a single token (graph-level invariants are checked by {@link validateGraph}). */
174
+ declare const designTokenSchema: z.ZodType<DesignToken>;
175
+ /** Structural schema for a component declaration. */
176
+ declare const componentDeclarationSchema: z.ZodType<ComponentDeclaration>;
177
+ /**
178
+ * The canonical CSS custom-property name for a token. Component tokens keep the ungoverned
179
+ * component grammar (`--noctis-{body}`); seeds serialize as `{prefix}-seed-{id}`; foundation
180
+ * and semantic tokens carry their structural category (`{prefix}-{category}-{id}`). The prefix
181
+ * is a parameter so the same graph can serialize under an alternate namespace; the default is
182
+ * {@link ROLE_VAR_PREFIX}.
183
+ */
184
+ declare function cssVarName(token: DesignToken, rolePrefix: string): string;
185
+ /**
186
+ * The bare Tailwind `@theme` key a token's bridge declares, or `undefined` when the token has
187
+ * no `@theme` key (no bridge, or a `utility`-kind bridge reached via a dedicated `@utility`
188
+ * rule). `color`-kind bridges derive `--color-{id}`; `theme`/`theme-inline` bridges carry an
189
+ * explicit name. The single source of truth for bridge naming — generators must not re-derive it.
190
+ */
191
+ declare function bridgeVarName(token: DesignToken): string | undefined;
192
+ /** A graph validation failure, with the offending token/declaration named in the message. */
193
+ declare class GraphValidationError extends Error {
194
+ constructor(message: string);
195
+ }
196
+ /**
197
+ * Enforce the cross-token invariants the serializers and generators depend on: structural schema
198
+ * conformance, the tier↔category contract, unique identity (uuid + serialized var name), the
199
+ * reserved engine namespace, bridge-name uniqueness, resolvable acyclic `{ token }`/`{ role }`
200
+ * aliases, existing `replacedBy` redirects, grammar-clean component property names, and
201
+ * well-formed component declarations. Throws {@link GraphValidationError} on the first violation.
202
+ */
203
+ declare function validateGraph(tokens: readonly DesignToken[], components: readonly ComponentDeclaration[]): void;
204
+ //#endregion
205
+ export { ComponentDeclaration, DesignToken, GraphValidationError, TokenBridge, TokenDeprecation, TokenGroup, TokenName, TokenValue, bridgeVarName, componentDeclarationSchema, cssVarName, designTokenSchema, validateGraph };
@@ -0,0 +1,282 @@
1
+ import { ABBREVIATED_PROPERTIES, CATEGORY_REGISTRY, COMPOSITE_PROPERTIES, ROLE_VAR_PREFIX, STATE_REGISTRY, TOKEN_CATEGORIES, TOKEN_TIERS } from "./registry.js";
2
+ import { serializeTokenName } from "./serialize.js";
3
+ import { THEME_VARS } from "@stridge/noctis-theme-engine";
4
+ import { z } from "zod";
5
+ //#region src/graph/model.ts
6
+ /**
7
+ * The token graph data model: zod-validated TypeScript as the single source of truth.
8
+ *
9
+ * A {@link DesignToken} carries stable UUID identity, a structured {@link TokenName}, and an
10
+ * alias-graph {@link TokenValue} — an engine primitive, another token, a sibling role, or a raw
11
+ * literal. {@link ComponentDeclaration} records a primitive's anatomy, option axes, consumed
12
+ * roles, and the component tokens it mints. {@link validateGraph} enforces the cross-token
13
+ * invariants the serializers and generators rely on (unique identity, resolvable acyclic
14
+ * aliases, grammar-clean property names). JSON and DTCG are generated *from* this graph; nothing
15
+ * is authored in them.
16
+ */
17
+ const KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
18
+ /**
19
+ * Flat semantic/foundation ids — leading digits (`2xl`), the Tailwind `--modifier` double dash,
20
+ * and a fractional segment (`1.5`, the spacing-ramp half steps) permitted. The id carries the
21
+ * bare `.`; CSS emission backslash-escapes it (`--noctis-space-1\.5`) at the generator.
22
+ */
23
+ const FLAT_ID = /^[a-z0-9]+(?:\.[0-9]+)?(?:-{1,2}[a-z0-9]+(?:\.[0-9]+)?)*$/;
24
+ /**
25
+ * A bridge name: a bare Tailwind `@theme` key, double-dash modifier suffix
26
+ * (`--text-regular--line-height`) and fractional segment (`--spacing-1.5`) permitted — the
27
+ * fractional dot is stored bare and escaped at CSS emission, like {@link FLAT_ID}.
28
+ */
29
+ const BRIDGE_NAME = /^--[a-z][a-z0-9]*(?:-{1,2}[a-z0-9]+(?:\.[0-9]+)?)*$/;
30
+ const tokenNameSchema = z.discriminatedUnion("kind", [z.object({
31
+ kind: z.literal("flat"),
32
+ id: z.string().regex(FLAT_ID)
33
+ }), z.object({
34
+ kind: z.literal("component"),
35
+ component: z.string().regex(/^[a-z][a-z0-9-]*$/),
36
+ anatomy: z.string().regex(KEBAB).optional(),
37
+ property: z.string().min(1),
38
+ state: z.enum(STATE_REGISTRY).optional()
39
+ })]);
40
+ const tokenValueSchema = z.union([
41
+ z.object({ role: z.string() }).strict(),
42
+ z.object({ token: z.string() }).strict(),
43
+ z.object({ primitive: z.enum(THEME_VARS) }).strict(),
44
+ z.object({
45
+ raw: z.string(),
46
+ rawLight: z.string().optional()
47
+ }).strict()
48
+ ]);
49
+ const groupSchema = z.enum([
50
+ "surface",
51
+ "text",
52
+ "border",
53
+ "accent",
54
+ "control",
55
+ "status",
56
+ "utility",
57
+ "chart",
58
+ "avatar"
59
+ ]);
60
+ const tokenBridgeSchema = z.discriminatedUnion("kind", [
61
+ z.object({ kind: z.literal("color") }).strict(),
62
+ z.object({ kind: z.literal("utility") }).strict(),
63
+ z.object({
64
+ kind: z.literal("theme"),
65
+ name: z.string().regex(BRIDGE_NAME)
66
+ }).strict(),
67
+ z.object({
68
+ kind: z.literal("theme-inline"),
69
+ name: z.string().regex(BRIDGE_NAME)
70
+ }).strict()
71
+ ]);
72
+ /** Structural schema for a single token (graph-level invariants are checked by {@link validateGraph}). */
73
+ const designTokenSchema = z.object({
74
+ uuid: z.uuid(),
75
+ tier: z.enum(TOKEN_TIERS),
76
+ name: tokenNameSchema,
77
+ value: tokenValueSchema,
78
+ description: z.string(),
79
+ usage: z.string(),
80
+ introduced: z.string(),
81
+ deprecated: z.object({
82
+ version: z.string(),
83
+ comment: z.string(),
84
+ replacedBy: z.array(z.string())
85
+ }).optional(),
86
+ label: z.string().optional(),
87
+ group: groupSchema.optional(),
88
+ category: z.enum(TOKEN_CATEGORIES).optional(),
89
+ utility: z.string().optional(),
90
+ bridge: tokenBridgeSchema.optional(),
91
+ variants: z.record(z.string(), z.record(z.string(), tokenValueSchema)).optional()
92
+ });
93
+ /** Structural schema for a component declaration. */
94
+ const componentDeclarationSchema = z.object({
95
+ component: z.string().regex(/^[a-z][a-z0-9-]*$/),
96
+ category: z.enum(CATEGORY_REGISTRY).optional(),
97
+ anatomy: z.array(z.string().regex(KEBAB)),
98
+ options: z.record(z.string(), z.array(z.string())),
99
+ states: z.array(z.enum(STATE_REGISTRY)),
100
+ consumes: z.array(z.string()),
101
+ mints: z.array(designTokenSchema),
102
+ slotGroups: z.record(z.string(), z.array(z.string())).optional(),
103
+ optionSlots: z.record(z.string(), z.string()).optional(),
104
+ recipePortable: z.boolean().optional(),
105
+ slotPrefix: z.string().regex(/^[a-z][a-z0-9-]*$/).optional(),
106
+ marker: z.string().regex(/^[a-z][a-z0-9-]*$/).optional()
107
+ });
108
+ /**
109
+ * The canonical CSS custom-property name for a token. Component tokens keep the ungoverned
110
+ * component grammar (`--noctis-{body}`); seeds serialize as `{prefix}-seed-{id}`; foundation
111
+ * and semantic tokens carry their structural category (`{prefix}-{category}-{id}`). The prefix
112
+ * is a parameter so the same graph can serialize under an alternate namespace; the default is
113
+ * {@link ROLE_VAR_PREFIX}.
114
+ */
115
+ function cssVarName(token, rolePrefix) {
116
+ const body = serializeTokenName(token.name);
117
+ if (token.tier === "component") return `--noctis-${body}`;
118
+ if (token.tier === "seed") return `${rolePrefix}-seed-${body}`;
119
+ if (!token.category) throw new Error(`${token.tier} token "${body}" has no structural category — cannot derive its canonical name`);
120
+ return `${rolePrefix}-${token.category}-${body}`;
121
+ }
122
+ /**
123
+ * The bare Tailwind `@theme` key a token's bridge declares, or `undefined` when the token has
124
+ * no `@theme` key (no bridge, or a `utility`-kind bridge reached via a dedicated `@utility`
125
+ * rule). `color`-kind bridges derive `--color-{id}`; `theme`/`theme-inline` bridges carry an
126
+ * explicit name. The single source of truth for bridge naming — generators must not re-derive it.
127
+ */
128
+ function bridgeVarName(token) {
129
+ if (!token.bridge) return void 0;
130
+ switch (token.bridge.kind) {
131
+ case "color": return `--color-${serializeTokenName(token.name)}`;
132
+ case "utility": return;
133
+ case "theme":
134
+ case "theme-inline": return token.bridge.name;
135
+ }
136
+ }
137
+ /**
138
+ * The gated color-utility prefixes Tailwind generates for every `--color-{id}` (the family-table
139
+ * paint families a recipe may consume). `outline-*` is deliberately absent — it is a *free* family
140
+ * (the focus-ring composite's domain), so an `outline-{role}` is never a declared `consumes` entry.
141
+ */
142
+ const COLOR_UTILITY_PREFIXES = [
143
+ "bg",
144
+ "from",
145
+ "via",
146
+ "to",
147
+ "text",
148
+ "placeholder",
149
+ "caret",
150
+ "accent",
151
+ "decoration",
152
+ "fill",
153
+ "stroke",
154
+ "border",
155
+ "divide",
156
+ "ring",
157
+ "ring-offset",
158
+ "inset-ring"
159
+ ];
160
+ /**
161
+ * Every bridge utility a component recipe may name in its `consumes` declaration: a color role's
162
+ * full prefix expansion (`bg-`/`text-`/`border-`/… of each `color`-kind semantic token), the
163
+ * single named utility of a `utility`-kind semantic token, and the named semantic shadows. Scale
164
+ * steps (`rounded-*`, `text-{size}`, spacing) are excluded — identity values flow through mints,
165
+ * not `consumes`. The single source of truth shared by {@link validateGraph} and the parity test.
166
+ */
167
+ function consumableUtilities(tokens) {
168
+ const utilities = /* @__PURE__ */ new Set();
169
+ for (const token of tokens) if (token.tier === "semantic") {
170
+ if (token.bridge?.kind === "color") {
171
+ const id = serializeTokenName(token.name);
172
+ for (const prefix of COLOR_UTILITY_PREFIXES) utilities.add(`${prefix}-${id}`);
173
+ } else if (token.utility) utilities.add(token.utility);
174
+ } else if (token.tier === "foundation" && token.bridge?.kind === "theme-inline" && token.utility?.startsWith("shadow-")) utilities.add(token.utility);
175
+ return utilities;
176
+ }
177
+ function isCompositeProperty(property) {
178
+ return COMPOSITE_PROPERTIES.includes(property);
179
+ }
180
+ /** A graph validation failure, with the offending token/declaration named in the message. */
181
+ var GraphValidationError = class extends Error {
182
+ constructor(message) {
183
+ super(message);
184
+ this.name = "GraphValidationError";
185
+ }
186
+ };
187
+ /**
188
+ * Enforce the cross-token invariants the serializers and generators depend on: structural schema
189
+ * conformance, the tier↔category contract, unique identity (uuid + serialized var name), the
190
+ * reserved engine namespace, bridge-name uniqueness, resolvable acyclic `{ token }`/`{ role }`
191
+ * aliases, existing `replacedBy` redirects, grammar-clean component property names, and
192
+ * well-formed component declarations. Throws {@link GraphValidationError} on the first violation.
193
+ */
194
+ function validateGraph(tokens, components) {
195
+ for (const token of tokens) {
196
+ const parsed = designTokenSchema.safeParse(token);
197
+ if (!parsed.success) throw new GraphValidationError(`invalid token ${describe(token)}: ${parsed.error.issues[0]?.message}`);
198
+ }
199
+ for (const decl of components) {
200
+ const parsed = componentDeclarationSchema.safeParse(decl);
201
+ if (!parsed.success) throw new GraphValidationError(`invalid declaration "${decl.component}": ${parsed.error.issues[0]?.message}`);
202
+ }
203
+ const allTokens = [...tokens, ...components.flatMap((c) => c.mints)];
204
+ const byUuid = /* @__PURE__ */ new Map();
205
+ for (const token of allTokens) {
206
+ if (byUuid.has(token.uuid)) throw new GraphValidationError(`duplicate uuid ${token.uuid} (${describe(token)})`);
207
+ byUuid.set(token.uuid, token);
208
+ }
209
+ for (const token of allTokens) {
210
+ const categorized = token.tier === "foundation" || token.tier === "semantic";
211
+ if (categorized && !token.category) throw new GraphValidationError(`${token.tier} token ${describe(token)} is missing a structural category`);
212
+ if (!categorized && token.category) throw new GraphValidationError(`${token.tier} token ${describe(token)} must not carry a structural category`);
213
+ if (token.tier === "semantic" && token.category !== "color") throw new GraphValidationError(`semantic token ${describe(token)} must have category "color" (got "${token.category}")`);
214
+ }
215
+ const seenVars = /* @__PURE__ */ new Set();
216
+ for (const token of allTokens) {
217
+ const name = cssVarName(token, ROLE_VAR_PREFIX);
218
+ if (seenVars.has(name)) throw new GraphValidationError(`duplicate css variable ${name}`);
219
+ if (name.startsWith(`--noctis-engine-`)) throw new GraphValidationError(`token ${describe(token)} serializes into the reserved engine namespace (${name})`);
220
+ seenVars.add(name);
221
+ }
222
+ const seenBridges = /* @__PURE__ */ new Set();
223
+ for (const token of allTokens) {
224
+ const bridge = bridgeVarName(token);
225
+ if (!bridge) continue;
226
+ if (seenBridges.has(bridge)) throw new GraphValidationError(`duplicate bridge name ${bridge} (${describe(token)})`);
227
+ if (seenVars.has(bridge)) throw new GraphValidationError(`bridge name ${bridge} (${describe(token)}) collides with a canonical css variable`);
228
+ seenBridges.add(bridge);
229
+ }
230
+ const roleById = new Map(allTokens.filter((t) => t.tier === "semantic").map((t) => [serializeTokenName(t.name), t]));
231
+ for (const token of allTokens) {
232
+ if ("token" in token.value && !byUuid.has(token.value.token)) throw new GraphValidationError(`token ${describe(token)} aliases a missing uuid ${token.value.token}`);
233
+ if ("role" in token.value && !roleById.has(token.value.role)) throw new GraphValidationError(`token ${describe(token)} aliases a missing role "${token.value.role}"`);
234
+ if (token.deprecated) {
235
+ for (const uuid of token.deprecated.replacedBy) if (!byUuid.has(uuid)) throw new GraphValidationError(`token ${describe(token)} is replaced by a missing uuid ${uuid}`);
236
+ }
237
+ if (token.name.kind === "component") {
238
+ const { property } = token.name;
239
+ if (!isCompositeProperty(property) && !KEBAB.test(property)) throw new GraphValidationError(`token ${describe(token)} property "${property}" is not kebab-case`);
240
+ if (ABBREVIATED_PROPERTIES.includes(property)) throw new GraphValidationError(`token ${describe(token)} property "${property}" is an abbreviation — spell it out`);
241
+ }
242
+ }
243
+ const consumable = consumableUtilities(tokens);
244
+ for (const decl of components) {
245
+ for (const mint of decl.mints) {
246
+ if (mint.tier !== "component") throw new GraphValidationError(`"${decl.component}" mints a non-component token ${describe(mint)}`);
247
+ if (mint.name.kind !== "component" || mint.name.component !== decl.component) throw new GraphValidationError(`"${decl.component}" mints ${describe(mint)} under a foreign component`);
248
+ }
249
+ for (const utility of decl.consumes) if (!consumable.has(utility)) throw new GraphValidationError(`"${decl.component}" consumes an unknown bridge utility "${utility}"`);
250
+ const anatomy = new Set(decl.anatomy);
251
+ for (const [key, slots] of Object.entries(decl.slotGroups ?? {})) for (const slot of slots) if (!anatomy.has(slot)) throw new GraphValidationError(`"${decl.component}" slotGroup "${key}" references a missing slot "${slot}"`);
252
+ for (const [axis, slot] of Object.entries(decl.optionSlots ?? {})) {
253
+ if (!(axis in decl.options)) throw new GraphValidationError(`"${decl.component}" optionSlots axis "${axis}" is not an option`);
254
+ if (!anatomy.has(slot)) throw new GraphValidationError(`"${decl.component}" optionSlots axis "${axis}" stamps a missing slot "${slot}"`);
255
+ }
256
+ if (decl.marker !== void 0 && decl.slotPrefix === void 0) throw new GraphValidationError(`"${decl.component}" sets a marker "${decl.marker}" without a slotPrefix — the marker model is part of the migrated single-anchor world`);
257
+ for (const mint of decl.mints) for (const [axis, byValue] of Object.entries(mint.variants ?? {})) {
258
+ const values = decl.options[axis];
259
+ if (!values) throw new GraphValidationError(`${describe(mint)} re-points unknown axis "${axis}"`);
260
+ for (const value of Object.keys(byValue)) if (!values.includes(value)) throw new GraphValidationError(`${describe(mint)} re-points unknown "${axis}" value "${value}"`);
261
+ }
262
+ }
263
+ detectAliasCycles(allTokens, byUuid, roleById);
264
+ }
265
+ function detectAliasCycles(tokens, byUuid, roleById) {
266
+ const state = /* @__PURE__ */ new Map();
267
+ const visit = (token) => {
268
+ const mark = state.get(token.uuid);
269
+ if (mark === "done") return;
270
+ if (mark === "visiting") throw new GraphValidationError(`alias cycle through ${describe(token)}`);
271
+ state.set(token.uuid, "visiting");
272
+ const next = "token" in token.value ? byUuid.get(token.value.token) : "role" in token.value ? roleById.get(token.value.role) : void 0;
273
+ if (next) visit(next);
274
+ state.set(token.uuid, "done");
275
+ };
276
+ for (const token of tokens) visit(token);
277
+ }
278
+ function describe(token) {
279
+ return `"${serializeTokenName(token.name)}"`;
280
+ }
281
+ //#endregion
282
+ export { GraphValidationError, bridgeVarName, componentDeclarationSchema, cssVarName, designTokenSchema, validateGraph };
@@ -0,0 +1,67 @@
1
+ //#region src/graph/registry.d.ts
2
+ /**
3
+ * Closed vocabularies the token grammar is built from.
4
+ *
5
+ * These are the controlled sets a structured token name draws on: the interaction states a
6
+ * token may key off, the composite pseudo-properties that stand in where no single real CSS
7
+ * property names an anatomy-level seam, and the abbreviation denylist that keeps property
8
+ * names spelled out. Extending any of them is a deliberate schema change here — never an
9
+ * ad-hoc string at a call site.
10
+ */
11
+ /**
12
+ * Interaction states a component token may carry, as the final name segment. The default
13
+ * (rest) state is the absence of a segment, so there is no `default` member. Order is the
14
+ * serialization tie-break only; names never combine two states.
15
+ */
16
+ declare const STATE_REGISTRY: readonly ["hover", "active", "focus", "disabled", "selected", "highlighted", "open", "checked"];
17
+ /** A sanctioned interaction state. */
18
+ type TokenState = (typeof STATE_REGISTRY)[number];
19
+ /**
20
+ * Composite pseudo-properties: names admitted as a token `property` where no single real CSS
21
+ * property names the seam a consumer would override. Each expands to real CSS properties at the
22
+ * recipe — `size` is width + height (Tailwind's `size-*`). The seam vocabulary grows only by
23
+ * adding a member here.
24
+ */
25
+ declare const COMPOSITE_PROPERTIES: readonly ["size"];
26
+ /** A sanctioned composite pseudo-property. */
27
+ type CompositeProperty = (typeof COMPOSITE_PROPERTIES)[number];
28
+ /**
29
+ * The token tiers. `seed` is the knob stratum (the System Controls inputs the foundations
30
+ * derive from); `foundation` is the authored non-color scale stratum (type, radius, motion,
31
+ * z, …); `semantic` is the intent-named color stratum over the engine; `component` is the
32
+ * minted per-component seam stratum. The engine stratum is not a graph tier — it is the
33
+ * engine's own `--noctis-engine-*` namespace.
34
+ */
35
+ declare const TOKEN_TIERS: readonly ["seed", "foundation", "semantic", "component"];
36
+ /** A token tier. */
37
+ type TokenTier = (typeof TOKEN_TIERS)[number];
38
+ /**
39
+ * The closed structural-category vocabulary — the `{category}` segment of a canonical
40
+ * `--noctis-{category}-{name}` CSS variable. Required on `foundation` and
41
+ * `semantic` tokens, forbidden on `seed` and `component` tokens (the validator enforces
42
+ * both). Distinct from {@link TokenGroup} (the reference-view grouping) and from
43
+ * {@link ComponentCategory} (the showcase groups) — do not conflate the three. Growing the
44
+ * vocabulary is a deliberate schema change here, never an ad-hoc string in a data file.
45
+ */
46
+ declare const TOKEN_CATEGORIES: readonly ["color", "space", "size", "text", "leading", "radius", "shadow", "duration", "ease", "z", "font", "tracking", "breakpoint", "container", "border", "opacity", "blur", "animate"];
47
+ /** A sanctioned structural category — the category segment of a canonical CSS variable name. */
48
+ type TokenCategory = (typeof TOKEN_CATEGORIES)[number];
49
+ /**
50
+ * The namespace every canonical token serializes under. A parameter on the serializers so the
51
+ * same graph can emit under an alternate namespace; this constant is the single source of truth
52
+ * for the default. `--noctis-engine-*` is the engine's reserved private namespace — the
53
+ * validator rejects any graph token that serializes into it.
54
+ */
55
+ declare const ROLE_VAR_PREFIX: "--noctis";
56
+ /**
57
+ * The closed component-category vocabulary the showcase groups primitives by. A
58
+ * {@link ComponentDeclaration} draws its optional `category` from this set, and the gallery renders
59
+ * one localized group per member in this order. Mechanisms (the polymorphic base, the radius scope,
60
+ * the visually-hidden helper) carry no category — they are never showcased. Growing the vocabulary
61
+ * is a deliberate schema change here, never an ad-hoc string in the docs app.
62
+ */
63
+ declare const CATEGORY_REGISTRY: readonly ["actions", "fields", "navigation", "overlays", "surfaces", "data", "typography", "system"];
64
+ /** A sanctioned component category. */
65
+ type ComponentCategory = (typeof CATEGORY_REGISTRY)[number];
66
+ //#endregion
67
+ export { CATEGORY_REGISTRY, COMPOSITE_PROPERTIES, ComponentCategory, CompositeProperty, ROLE_VAR_PREFIX, STATE_REGISTRY, TOKEN_CATEGORIES, TOKEN_TIERS, TokenCategory, TokenState, TokenTier };