@apollion-dsi/tokens 5.5.0 → 5.7.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/src/ir.ts CHANGED
@@ -265,6 +265,11 @@ function buildColorPrimitives(colors: ColorsThemeInterface): IRGroup {
265
265
  }
266
266
  out.grayscale = recordColorGroup(colors.grayscale);
267
267
  out.neutral = recordColorGroup(colors.neutral);
268
+ // ADR-023 — surface ladder roles (mode-aware grounds / inks / edges / washes).
269
+ out.surface = recordColorGroup(colors.surface as unknown as Record<string, string>);
270
+ out.ink = recordColorGroup(colors.ink as unknown as Record<string, string>);
271
+ out.edge = recordColorGroup(colors.edge as unknown as Record<string, string>);
272
+ out.wash = recordColorGroup(colors.wash as unknown as Record<string, string>);
268
273
  out.baseDark = colorToken(colors.baseDark ?? '#000000');
269
274
  out.baseLight = colorToken(colors.baseLight ?? '#ffffff');
270
275
  out.deepDark = colorToken(colors.deepDark ?? '#000000');
@@ -317,6 +322,11 @@ const BG_REF: Readonly<Record<string, string>> = {
317
322
  danger: 'color.danger.base',
318
323
  warning: 'color.warning.base',
319
324
  info: 'color.information.base',
325
+ canvas: 'color.surface.canvas',
326
+ sunken: 'color.surface.sunken',
327
+ paper: 'color.surface.paper',
328
+ raised: 'color.surface.raised',
329
+ overlay: 'color.surface.overlay',
320
330
  };
321
331
 
322
332
  const TEXT_REF: Readonly<Record<string, string>> = {
@@ -327,6 +337,17 @@ const TEXT_REF: Readonly<Record<string, string>> = {
327
337
  onDanger: 'color.baseLight',
328
338
  onWarning: 'color.baseDark',
329
339
  onInfo: 'color.baseLight',
340
+ primary: 'color.ink.primary',
341
+ secondary: 'color.ink.secondary',
342
+ muted: 'color.ink.muted',
343
+ disabled: 'color.ink.disabled',
344
+ };
345
+
346
+ /** Foundation `edge.*` → `color.edge.*` primitive (ADR-023 ink-alpha lines). */
347
+ const EDGE_REF: Readonly<Record<string, string>> = {
348
+ hairline: 'color.edge.hairline',
349
+ border: 'color.edge.border',
350
+ strong: 'color.edge.strong',
330
351
  };
331
352
 
332
353
  /** Navigate the structural palette by IR path (`color.x.y`). */
@@ -691,6 +712,7 @@ export function buildColorIR(foundation: FoundationLayer, colors: ColorsThemeInt
691
712
  color,
692
713
  bg: colorRefGroup(foundation.bg as unknown as Record<string, string>, colors, BG_REF),
693
714
  text: colorRefGroup(foundation.text as unknown as Record<string, string>, colors, TEXT_REF),
715
+ edge: colorRefGroup(foundation.edge as unknown as Record<string, string>, colors, EDGE_REF),
694
716
  tx: buildReadableTextGroup(colors),
695
717
  shadow: buildShadow(colors),
696
718
  },
@@ -749,6 +771,7 @@ export function buildIR(foundation: FoundationLayer, colors: ColorsThemeInterfac
749
771
  groups: {
750
772
  bg: colorRefGroup(foundation.bg as unknown as Record<string, string>, colors, BG_REF),
751
773
  text: colorRefGroup(foundation.text as unknown as Record<string, string>, colors, TEXT_REF),
774
+ edge: colorRefGroup(foundation.edge as unknown as Record<string, string>, colors, EDGE_REF),
752
775
  tx: buildReadableTextGroup(colors),
753
776
  spacing: dimensionRefGroup(foundation.spacing as unknown as Record<string, string>, primitives, SPACE_REF),
754
777
  border: border.group,
@@ -90,3 +90,51 @@ export function renderCss(ir: IRDocument, options: RenderCssOptions = {}): strin
90
90
 
91
91
  return lines.join('\n');
92
92
  }
93
+
94
+ /** Options of {@link renderCssModes}. */
95
+ export interface RenderCssModesOptions extends RenderCssOptions {
96
+ /** `<html>` attribute carrying the resolved mode (same default as the core mode engine). */
97
+ attribute?: string;
98
+ }
99
+
100
+ /**
101
+ * Mode-scoped CSS (APOLLION-DARK-MODE-ENGINE, ADR-024 — `output.cssModes`):
102
+ * ONE file per (brand × surface × dimension) carrying BOTH modes, scoped the
103
+ * way the core mode engine scopes them — `:root`/`[attr="light"]` for light,
104
+ * `[attr="dark"]` for dark, plus a `prefers-color-scheme` fallback for pages
105
+ * that never set the attribute. Consumers without React link this file and
106
+ * flip the mode with the attribute (`getModeScript` / `syncDocumentMode`)
107
+ * — the "CSS-only switch" of the docs, made real.
108
+ */
109
+ export function renderCssModes(light: IRDocument, dark: IRDocument, options: RenderCssModesOptions = {}): string {
110
+ const { attribute = 'data-apollion-mode', rgbCompanions } = options;
111
+ const { brand, surface, dimension } = light.meta;
112
+ const lines: string[] = [HEADER_LINE, ''];
113
+
114
+ const block = (selector: string, ir: IRDocument, scheme: 'light' | 'dark'): void => {
115
+ lines.push(`${selector} {`, ` color-scheme: ${scheme};`);
116
+ emitVars(ir.groups, [], lines);
117
+ if (rgbCompanions) emitRgbVars(ir.groups, [], lines);
118
+ lines.push('}', '');
119
+ };
120
+
121
+ lines.push(
122
+ `/* Variant: brand=${brand} surface=${surface} dimension=${dimension} modes=light+dark (attribute ${attribute}) */`,
123
+ '',
124
+ );
125
+ block(`:root, html[${attribute}="light"]`, light, 'light');
126
+ block(`html[${attribute}="dark"]`, dark, 'dark');
127
+ lines.push('@media (prefers-color-scheme: dark) {');
128
+ lines.push(` html:not([${attribute}]) {`, ' color-scheme: dark;');
129
+ const inner: string[] = [];
130
+ emitVars(dark.groups, [], inner);
131
+ if (rgbCompanions) emitRgbVars(dark.groups, [], inner);
132
+ for (const line of inner) lines.push(` ${line}`);
133
+ lines.push(' }', '}', '');
134
+
135
+ lines.push('@supports (background: paint(squircle)) or (color: oklch(0% 0 0)) {');
136
+ emitProperties(light.groups, [], lines);
137
+ lines.push('}', '');
138
+
139
+ return lines.join('\n');
140
+ }
@@ -91,13 +91,13 @@ interface ColorEntry {
91
91
  }
92
92
 
93
93
  /**
94
- * Colour entries for the preset's `colors` scale: the `bg`/`text`/`tx`
94
+ * Colour entries for the preset's `colors` scale: the `bg`/`text`/`edge`/`tx`
95
95
  * foundation groups, keyed `<group>-<path>` (the CSS var name minus the
96
96
  * `--apollion-` prefix — one mental model across surfaces).
97
97
  */
98
98
  function colorEntries(ir: IRDocument): ColorEntry[] {
99
99
  const out: ColorEntry[] = [];
100
- for (const groupName of ['bg', 'text', 'tx'] as const) {
100
+ for (const groupName of ['bg', 'text', 'edge', 'tx'] as const) {
101
101
  for (const [key, token] of flatTokens(groupAt(ir.groups, groupName), [groupName])) {
102
102
  const color = colorPayload(token);
103
103
  if (!color) continue;
@@ -11,10 +11,11 @@
11
11
  * 5. If surface === 'negative': core `invertForSurface` applied.
12
12
  *
13
13
  * **Mode handling (ADR-019):** `mode === 'dark'` derives the palette through
14
- * the canonical-dark seed swap (`toCanonicalDarkInput`) — the same convention
15
- * the core invariant suite exercises as "canonical dark". Emitted dark equals
16
- * the runtime canonical dark byte-exactly (parity by composition over the
17
- * Strangler Fig builders; see `__tests__/parity.test.ts` canonical-dark rows).
14
+ * the canonical-dark seed swap (`toDarkColors`, imported from the core — the
15
+ * single source of the swap, ADR-022) — the same convention the core invariant
16
+ * suite exercises as "canonical dark". Emitted dark equals the runtime
17
+ * canonical dark byte-exactly (parity by composition over the Strangler Fig
18
+ * builders; see `__tests__/parity.test.ts` canonical-dark rows).
18
19
  * This retires the S5 placeholder (dark == light) and closes the ADR-006 E6/S6
19
20
  * parking.
20
21
  *
@@ -22,6 +23,7 @@
22
23
  * @see PRD-002 §S5 · docs/spikes/2026-08-13-dark-mode-real-parity.md
23
24
  */
24
25
 
26
+ import { toDarkColors } from '@apollion-dsi/core/themes/colors';
25
27
  import type { FoundationLayer } from '@apollion-dsi/core/themes/foundation';
26
28
  import { createFoundation } from '@apollion-dsi/core/themes/foundation';
27
29
  import { createSemantic } from '@apollion-dsi/core/themes/semantic';
@@ -30,6 +32,7 @@ import { defaultInputSpacing } from '@apollion-dsi/core/themes/spacing';
30
32
  import { invertForSurface } from '@apollion-dsi/core/themes/surface';
31
33
 
32
34
  import { mountGreyscaleColors, mountNeutralColors, mountSingleColor } from './builders/colors';
35
+ import { resolveSurfaceLadder } from './builders/ladder';
33
36
  import { createSpacing } from './builders/spacing';
34
37
  import type { Variant } from './config-schema';
35
38
 
@@ -42,33 +45,18 @@ export interface VariantThemes {
42
45
  colors: ColorsThemeInterface;
43
46
  }
44
47
 
45
- /**
46
- * Canonical dark input — the ADR-010 convention the core invariant suite calls
47
- * "canonical dark": swap ONLY the contrast/deep axes (`baseLight↔baseDark`,
48
- * `deepLight↔deepDark`); brand seeds stay fixed. The engine is mode-aware by
49
- * construction (ADR-012 state floors, ADR-018 `onLight`, `isDarkSurface`), so
50
- * every derived stop resolves correctly from the swapped seeds. See ADR-019 and
51
- * `docs/spikes/2026-08-13-dark-mode-real-parity.md`.
52
- */
53
- function toCanonicalDarkInput(input: Variant['colors']): Variant['colors'] {
54
- return {
55
- ...input,
56
- baseDark: input.baseLight,
57
- baseLight: input.baseDark,
58
- deepDark: input.deepLight,
59
- deepLight: input.deepDark,
60
- };
61
- }
62
-
63
48
  function composeColors(input: Variant['colors']) {
49
+ const neutral = mountNeutralColors(input);
64
50
  return {
65
51
  baseDark: input.baseDark,
66
52
  baseLight: input.baseLight,
67
53
  deepDark: input.deepDark,
68
54
  deepLight: input.deepLight,
69
55
  transparent: input.transparent ?? 'transparent',
70
- neutral: mountNeutralColors(input),
56
+ neutral,
71
57
  grayscale: mountGreyscaleColors(input),
58
+ // ADR-023 — surface ladder (mirror of core's createColors composition).
59
+ ...resolveSurfaceLadder({ ...input, neutral }, input.ladder),
72
60
  main: mountSingleColor(input.main!, input),
73
61
  opposite: mountSingleColor(input.opposite!, input),
74
62
  complementary: mountSingleColor(input.complementary!, input),
@@ -91,7 +79,7 @@ export function buildVariantThemes(variant: Variant, spacingInput: SpacingInput
91
79
  // ADR-019: dark stops being a placeholder — the palette derives from the
92
80
  // canonical-dark seed swap. Emitted dark == runtime canonical dark, byte-exact
93
81
  // (parity by composition over the Strangler Fig builders).
94
- const colorsInput = variant.mode === 'dark' ? toCanonicalDarkInput(variant.colors) : variant.colors;
82
+ const colorsInput = variant.mode === 'dark' ? toDarkColors(variant.colors) : variant.colors;
95
83
  const themeColors = composeColors(colorsInput) as unknown as ColorsThemeInterface;
96
84
  const themeSpacing = createSpacing(spacingInput, variant.dimension);
97
85
  const semantic = createSemantic({ colors: themeColors as never, spacing: themeSpacing as never });
package/src/token-meta.ts CHANGED
@@ -48,6 +48,15 @@ export const TOKEN_META: Readonly<Record<string, TokenMetaEntry>> = {
48
48
  deprecated: 'Use bg.information once the foundation role is renamed (tracked for S6).',
49
49
  },
50
50
 
51
+ // ── Surface ladder (ADR-023 — mode-aware grounds, inks, edges) ───────────
52
+ 'bg.canvas': { description: 'Page ground — the theme canvas (never pure black in dark).' },
53
+ 'bg.paper': { description: 'Component ground (cards, inputs, tables) — lighter than the canvas in both modes.' },
54
+ 'bg.overlay': { description: 'Top of the elevation ladder — modal / dialog panels.' },
55
+ 'text.primary': { description: 'Primary neutral ink — AA + APCA Lc ≥ 75 over bg.paper and bg.canvas.' },
56
+ 'text.muted': { description: 'Muted neutral ink — AA + APCA Lc ≥ 45 over bg.paper.' },
57
+ 'edge.border': { description: 'Default control outline — primary ink at 20 % alpha, legible on any ground.' },
58
+ 'edge.hairline': { description: 'Dividers and card outlines — primary ink at 10 % alpha.' },
59
+
51
60
  // ── Foundation text-on roles ─────────────────────────────────────────────
52
61
  'text.on-primary': { description: 'Text/icon colour with guaranteed contrast over bg.primary.' },
53
62
  'text.on-danger': { description: 'Text/icon colour with guaranteed contrast over bg.danger.' },