@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/README.md +5 -0
- package/lib/build.cjs +10 -9
- package/lib/build.esm.js +10 -9
- package/lib/builders/colors.d.ts +27 -0
- package/lib/cli.mjs +13 -12
- package/lib/config-loader.cjs +1 -1
- package/lib/config-loader.esm.js +2 -2
- package/lib/config-schema.cjs +1 -1
- package/lib/runtime/v1.cjs +1 -1
- package/lib/runtime/v1.esm.js +1 -1
- package/package.json +1 -1
- package/src/build.ts +28 -1
- package/src/builders/colors.ts +34 -3
- package/src/builders/ladder.ts +116 -0
- package/src/config-schema-runtime.ts +1 -0
- package/src/config-schema.ts +5 -0
- package/src/ir.ts +23 -0
- package/src/renderers/css.ts +48 -0
- package/src/renderers/tailwind.ts +2 -2
- package/src/theme-factory.ts +12 -24
- package/src/token-meta.ts +9 -0
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,
|
package/src/renderers/css.ts
CHANGED
|
@@ -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;
|
package/src/theme-factory.ts
CHANGED
|
@@ -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 (`
|
|
15
|
-
* the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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' ?
|
|
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.' },
|