@apollion-dsi/tokens 4.0.0 → 4.2.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.
@@ -31,7 +31,14 @@ import { mountGreyscaleColors, mountNeutralColors, mountSingleColor } from './bu
31
31
  import { createSpacing } from './builders/spacing';
32
32
  import type { Variant } from './config-schema';
33
33
 
34
- type ColorsThemeInterface = ReturnType<typeof composeColors>;
34
+ export type ColorsThemeInterface = ReturnType<typeof composeColors>;
35
+
36
+ /** A variant's resolved themes: the consumer Foundation layer + the structural
37
+ * palette (colour primitives / SSOT). */
38
+ export interface VariantThemes {
39
+ foundation: FoundationLayer;
40
+ colors: ColorsThemeInterface;
41
+ }
35
42
 
36
43
  function composeColors(input: Variant['colors']) {
37
44
  return {
@@ -56,25 +63,33 @@ function composeColors(input: Variant['colors']) {
56
63
  }
57
64
 
58
65
  /**
59
- * Build the Foundation layer for one variant. Pure function, deterministic.
66
+ * Build a variant's resolved themes — the Foundation layer plus the structural
67
+ * palette (colour SSOT consumed by the IR for reference emission). Pure +
68
+ * deterministic.
60
69
  */
61
- export function buildFoundationForVariant(
62
- variant: Variant,
63
- spacingInput: SpacingInput = defaultInputSpacing,
64
- ): FoundationLayer {
70
+ export function buildVariantThemes(variant: Variant, spacingInput: SpacingInput = defaultInputSpacing): VariantThemes {
65
71
  const themeColors = composeColors(variant.colors) as unknown as ColorsThemeInterface;
66
72
  const themeSpacing = createSpacing(spacingInput, variant.dimension);
67
73
  const semantic = createSemantic({ colors: themeColors as never, spacing: themeSpacing as never });
68
74
 
69
- const baseFoundation = createFoundation(semantic);
75
+ let foundation = createFoundation(semantic);
70
76
 
71
77
  if (variant.surface === 'negative') {
72
78
  // invertForSurface acts on the full Theme — assemble a minimal shim.
73
79
  // S6 will refactor to accept (semantic, surface) directly.
74
80
  const fullTheme = { semantic, colors: themeColors as never } as never;
75
- const inverted = invertForSurface(fullTheme, 'negative');
76
- return createFoundation(inverted.semantic);
81
+ foundation = createFoundation(invertForSurface(fullTheme, 'negative').semantic);
77
82
  }
78
83
 
79
- return baseFoundation;
84
+ return { foundation, colors: themeColors };
85
+ }
86
+
87
+ /**
88
+ * Build the Foundation layer for one variant. Pure function, deterministic.
89
+ */
90
+ export function buildFoundationForVariant(
91
+ variant: Variant,
92
+ spacingInput: SpacingInput = defaultInputSpacing,
93
+ ): FoundationLayer {
94
+ return buildVariantThemes(variant, spacingInput).foundation;
80
95
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Token metadata registry — the SSOT for DTCG `$description` / `$deprecated`
3
+ * annotations (Format Module 2025.10 §"Description" + §"Deprecation").
4
+ *
5
+ * **Why a side-table (not inline on each builder):** descriptions and
6
+ * deprecations are editorial metadata, not derived from the foundation math.
7
+ * Keeping them in one keyed map lets the IR stay a pure projection of the
8
+ * resolved theme while `decorateMeta` (see `ir.ts`) stitches the prose on in a
9
+ * single post-pass — smaller diff than threading copy through every builder.
10
+ *
11
+ * **Keying:** each key is a token's dotted IR path **after** kebab-case
12
+ * normalization (`kebabPath`), i.e. exactly the path the JSON renderer emits in
13
+ * a `{...}` alias. So `bg.primary`, `border.radius.md`, `space.medium`. A dev
14
+ * assertion (`assertTokenMetaResolves`) fails the build/tests on any stale key
15
+ * that no longer maps to an IR node, so this map can never silently rot.
16
+ *
17
+ * **Deprecation values:** `true` is a bare DTCG deprecation flag; a string is a
18
+ * human-readable reason (DTCG allows either). Prefer the string form — it
19
+ * surfaces the migration path to consumers reading the JSON.
20
+ *
21
+ * @see ir.ts (`decorateMeta` post-pass + `assertTokenMetaResolves`)
22
+ * @see renderers/json.ts (`projectToken` emits `$description` / `$deprecated`)
23
+ * @see https://www.designtokens.org/TR/drafts/format/ §Description, §Deprecation
24
+ */
25
+
26
+ export interface TokenMetaEntry {
27
+ /** DTCG `$description` — human-readable intent of the token. */
28
+ readonly description?: string;
29
+ /** DTCG `$deprecated` — `true` (bare flag) or a string migration reason. */
30
+ readonly deprecated?: boolean | string;
31
+ }
32
+
33
+ /**
34
+ * Editorial metadata keyed by kebab-normalized dotted IR path.
35
+ *
36
+ * Starter set covers the well-known foundation roles + a couple of scale
37
+ * anchors; extend as tokens gain documented intent. Every key MUST resolve to
38
+ * a live IR node (enforced by `assertTokenMetaResolves`).
39
+ */
40
+ export const TOKEN_META: Readonly<Record<string, TokenMetaEntry>> = {
41
+ // ── Foundation backgrounds (semantic surface roles) ──────────────────────
42
+ 'bg.primary': { description: 'Primary action surface — the brand’s dominant call-to-action background.' },
43
+ 'bg.secondary': { description: 'Secondary action surface — supporting actions paired with the primary.' },
44
+ 'bg.danger': { description: 'Destructive / error surface — irreversible or failing actions.' },
45
+ 'bg.success': { description: 'Positive confirmation surface — completed or healthy states.' },
46
+ 'bg.info': {
47
+ description: 'Informational surface — neutral, non-blocking notices.',
48
+ deprecated: 'Use bg.information once the foundation role is renamed (tracked for S6).',
49
+ },
50
+
51
+ // ── Foundation text-on roles ─────────────────────────────────────────────
52
+ 'text.on-primary': { description: 'Text/icon colour with guaranteed contrast over bg.primary.' },
53
+ 'text.on-danger': { description: 'Text/icon colour with guaranteed contrast over bg.danger.' },
54
+
55
+ // ── Spacing primitives (density-aware dimension scale) ────────────────────
56
+ 'space.medium': { description: 'Base spacing step (1rem at normal density) — the layout rhythm unit.' },
57
+ 'space.large': { description: 'Comfortable spacing step — section padding and card gutters.' },
58
+
59
+ // ── Spacing foundation aliases ───────────────────────────────────────────
60
+ 'spacing.md': { description: 'Medium foundation spacing alias — references the space.medium primitive.' },
61
+ 'spacing.lg': { description: 'Large foundation spacing alias — references the space.large primitive.' },
62
+
63
+ // ── Border scale ─────────────────────────────────────────────────────────
64
+ 'border.radius.md': { description: 'Default card corner radius.' },
65
+ 'border.width.thin': { description: 'Hairline border — dividers and default input outlines.' },
66
+ 'border.width.regular': { description: 'Standard emphasis border — focused/active controls.' },
67
+ };
68
+
69
+ /**
70
+ * Dev assertion: every {@link TOKEN_META} key must map to a live IR node.
71
+ *
72
+ * `resolvedPaths` is the set of kebab-normalized dotted paths the IR currently
73
+ * emits (built by `decorateMeta`'s walk). Throws listing every stale key so a
74
+ * rename in the foundation can never leave dangling documentation behind.
75
+ *
76
+ * @throws if any `TOKEN_META` key is absent from `resolvedPaths`.
77
+ */
78
+ export function assertTokenMetaResolves(resolvedPaths: ReadonlySet<string>): void {
79
+ const stale = Object.keys(TOKEN_META).filter((key) => !resolvedPaths.has(key));
80
+ if (stale.length > 0) {
81
+ throw new Error(
82
+ `token-meta: ${stale.length} stale TOKEN_META key(s) resolve to no IR node: ${stale.join(', ')}. ` +
83
+ `Update src/token-meta.ts to match the current IR paths.`,
84
+ );
85
+ }
86
+ }