@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.
- package/README.md +18 -9
- package/lib/build.cjs +6 -6
- package/lib/build.esm.js +6 -6
- package/lib/cli.mjs +8 -8
- package/package.json +4 -2
- package/src/build.ts +61 -25
- package/src/ir.ts +715 -0
- package/src/renderers/css.ts +36 -44
- package/src/renderers/dtcg-project.ts +108 -0
- package/src/renderers/json.ts +29 -69
- package/src/renderers/resolver.ts +129 -0
- package/src/renderers/ts.ts +31 -32
- package/src/set-plan.ts +136 -0
- package/src/theme-factory.ts +25 -10
- package/src/token-meta.ts +86 -0
package/src/renderers/css.ts
CHANGED
|
@@ -1,64 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CSS renderer —
|
|
2
|
+
* CSS renderer — project the token IR to CSS custom properties (the resolved
|
|
3
|
+
* consumer surface).
|
|
3
4
|
*
|
|
4
|
-
* Format: `--apollion-{
|
|
5
|
+
* Format: `--apollion-{group}-{...path}-{token}` per ADR-006 §3.3. References
|
|
6
|
+
* are resolved to literal values and the primitives layer is omitted — CSS
|
|
7
|
+
* stays fully resolved (JSON is the SSOT surface). Nested groups (border/font/
|
|
8
|
+
* shadow) flatten into dashed paths. Color tokens additionally get `@property`
|
|
9
|
+
* declarations (tech-radar R3) wrapped in `@supports`.
|
|
5
10
|
*
|
|
6
|
-
*
|
|
7
|
-
* (color/length) — enables native CSS transitions (View Transitions API
|
|
8
|
-
* friendly). `@property` is browser-supported 84%+ mid-2024; wrapped in
|
|
9
|
-
* `@supports` for graceful degradation.
|
|
11
|
+
* Output is deterministic: groups/tokens iterate in IR insertion order.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
* FoundationLayer object; no timestamps, no run-id.
|
|
13
|
-
*
|
|
14
|
-
* @see ADR-006 §3.3 + §3.10 + tech-radar R3
|
|
15
|
-
* @see PRD-002 §S5
|
|
13
|
+
* @see ../ir.ts (source of truth) · ADR-006 §3.3 + §3.10 · tech-radar R3
|
|
16
14
|
*/
|
|
17
15
|
|
|
18
|
-
import type {
|
|
19
|
-
|
|
20
|
-
import type { Variant } from '../config-schema';
|
|
16
|
+
import type { IRDocument, IRGroup } from '../ir';
|
|
17
|
+
import { isToken, kebab, rawValue } from '../ir';
|
|
21
18
|
|
|
22
19
|
const HEADER_LINE = '/* Generated by apollion-tokens build — DO NOT EDIT. See apollion.config.mjs. */';
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
`/* Variant: brand=${variant.brand} mode=${variant.mode} surface=${variant.surface} dimension=${variant.dimension} */`,
|
|
30
|
-
'',
|
|
31
|
-
':root {',
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
// bg layer
|
|
35
|
-
for (const [key, value] of Object.entries(foundation.bg)) {
|
|
36
|
-
lines.push(` --apollion-bg-${key}: ${value};`);
|
|
21
|
+
function emitVars(group: IRGroup, prefix: string[], lines: string[]): void {
|
|
22
|
+
for (const [key, node] of Object.entries(group)) {
|
|
23
|
+
const path = [...prefix, kebab(key)];
|
|
24
|
+
if (isToken(node)) lines.push(` --apollion-${path.join('-')}: ${rawValue(node.value)};`);
|
|
25
|
+
else emitVars(node, path, lines);
|
|
37
26
|
}
|
|
27
|
+
}
|
|
38
28
|
|
|
39
|
-
|
|
40
|
-
for (const [key,
|
|
41
|
-
const
|
|
42
|
-
|
|
29
|
+
function emitProperties(group: IRGroup, prefix: string[], lines: string[]): void {
|
|
30
|
+
for (const [key, node] of Object.entries(group)) {
|
|
31
|
+
const path = [...prefix, kebab(key)];
|
|
32
|
+
if (!isToken(node)) {
|
|
33
|
+
emitProperties(node, path, lines);
|
|
34
|
+
} else if (node.type === 'color') {
|
|
35
|
+
lines.push(
|
|
36
|
+
` @property --apollion-${path.join('-')} { syntax: '<color>'; inherits: true; initial-value: ${rawValue(node.value)}; }`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
43
39
|
}
|
|
40
|
+
}
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
42
|
+
export function renderCss(ir: IRDocument): string {
|
|
43
|
+
const { brand, mode, surface, dimension } = ir.meta;
|
|
44
|
+
const lines: string[] = [HEADER_LINE, ''];
|
|
49
45
|
|
|
46
|
+
// Variant marker as a comment (NOT a var — keep cascade clean).
|
|
47
|
+
lines.push(`/* Variant: brand=${brand} mode=${mode} surface=${surface} dimension=${dimension} */`, '', ':root {');
|
|
48
|
+
emitVars(ir.groups, [], lines);
|
|
50
49
|
lines.push('}', '');
|
|
51
50
|
|
|
52
|
-
// @property declarations for color tokens — opt-in via @supports
|
|
51
|
+
// @property declarations for color tokens — opt-in via @supports.
|
|
53
52
|
lines.push('@supports (background: paint(squircle)) or (color: oklch(0% 0 0)) {');
|
|
54
|
-
|
|
55
|
-
lines.push(` @property --apollion-bg-${key} { syntax: '<color>'; inherits: true; initial-value: ${value}; }`);
|
|
56
|
-
}
|
|
57
|
-
for (const key of Object.keys(foundation.text)) {
|
|
58
|
-
const kebab = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
59
|
-
const value = (foundation.text as unknown as Record<string, string>)[key];
|
|
60
|
-
lines.push(` @property --apollion-text-${kebab} { syntax: '<color>'; inherits: true; initial-value: ${value}; }`);
|
|
61
|
-
}
|
|
53
|
+
emitProperties(ir.groups, [], lines);
|
|
62
54
|
lines.push('}', '');
|
|
63
55
|
|
|
64
56
|
return lines.join('\n');
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared DTCG projection — the pure IR→DTCG-value mapping reused by the
|
|
3
|
+
* single-document JSON renderer (`json.ts`) and the Resolver set renderers
|
|
4
|
+
* (`resolver.ts`).
|
|
5
|
+
*
|
|
6
|
+
* Factored out so both surfaces emit byte-identical token shapes ($value /
|
|
7
|
+
* $type / $description / $deprecated) and a composed (base + color + spacing)
|
|
8
|
+
* set reproduces exactly what the full per-variant document used to emit.
|
|
9
|
+
*
|
|
10
|
+
* @see json.ts (full single-doc surface) · resolver.ts (sliced sets surface)
|
|
11
|
+
* @see https://www.designtokens.org/TR/drafts/format/ (DTCG 2025.10)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { IRColorValue, IRDimensionValue, IRGroup, IRShadowValue, IRToken, IRValue } from '../ir';
|
|
15
|
+
import { isToken, kebab, kebabPath } from '../ir';
|
|
16
|
+
|
|
17
|
+
export interface DtcgColorValue {
|
|
18
|
+
colorSpace: 'oklch';
|
|
19
|
+
components: number[];
|
|
20
|
+
alpha?: number;
|
|
21
|
+
hex: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DtcgDimensionValue {
|
|
25
|
+
value: number;
|
|
26
|
+
unit: 'px' | 'rem';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DtcgShadowValue {
|
|
30
|
+
color: DtcgColorValue;
|
|
31
|
+
offsetX: DtcgDimensionValue;
|
|
32
|
+
offsetY: DtcgDimensionValue;
|
|
33
|
+
blur: DtcgDimensionValue;
|
|
34
|
+
spread: DtcgDimensionValue;
|
|
35
|
+
inset?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type DtcgTokenValue = DtcgColorValue | DtcgDimensionValue | DtcgShadowValue | string | number;
|
|
39
|
+
|
|
40
|
+
export interface DtcgToken {
|
|
41
|
+
$value: DtcgTokenValue;
|
|
42
|
+
$type: string;
|
|
43
|
+
$description?: string;
|
|
44
|
+
$deprecated?: boolean | string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DtcgGroup {
|
|
48
|
+
[key: string]: DtcgToken | DtcgGroup;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function colorValue(v: IRColorValue): DtcgColorValue {
|
|
52
|
+
return v.alpha !== undefined
|
|
53
|
+
? { colorSpace: 'oklch', components: [...v.components], alpha: v.alpha, hex: v.hex }
|
|
54
|
+
: { colorSpace: 'oklch', components: [...v.components], hex: v.hex };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function dimensionValue(v: IRDimensionValue): DtcgDimensionValue {
|
|
58
|
+
return { value: v.value, unit: v.unit };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function shadowValue(v: IRShadowValue): DtcgShadowValue {
|
|
62
|
+
const s = v.shadow;
|
|
63
|
+
const out: DtcgShadowValue = {
|
|
64
|
+
color: colorValue(s.color),
|
|
65
|
+
offsetX: dimensionValue(s.offsetX),
|
|
66
|
+
offsetY: dimensionValue(s.offsetY),
|
|
67
|
+
blur: dimensionValue(s.blur),
|
|
68
|
+
spread: dimensionValue(s.spread),
|
|
69
|
+
};
|
|
70
|
+
if (s.inset) out.inset = true;
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function projectValue(value: IRValue): DtcgTokenValue {
|
|
75
|
+
switch (value.kind) {
|
|
76
|
+
case 'ref':
|
|
77
|
+
return `{${kebabPath(value.path)}}`;
|
|
78
|
+
case 'color':
|
|
79
|
+
return colorValue(value);
|
|
80
|
+
case 'dimension':
|
|
81
|
+
return dimensionValue(value);
|
|
82
|
+
case 'fontFamily':
|
|
83
|
+
return value.family;
|
|
84
|
+
case 'fontWeight':
|
|
85
|
+
return value.weight;
|
|
86
|
+
case 'number':
|
|
87
|
+
return value.number;
|
|
88
|
+
case 'strokeStyle':
|
|
89
|
+
return value.style;
|
|
90
|
+
case 'shadow':
|
|
91
|
+
return shadowValue(value);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function projectToken(token: IRToken): DtcgToken {
|
|
96
|
+
const out: DtcgToken = { $value: projectValue(token.value), $type: token.type };
|
|
97
|
+
if (token.description !== undefined) out.$description = token.description;
|
|
98
|
+
if (token.deprecated !== undefined) out.$deprecated = token.deprecated;
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function projectGroup(group: IRGroup): DtcgGroup {
|
|
103
|
+
const out: DtcgGroup = {};
|
|
104
|
+
for (const [key, node] of Object.entries(group)) {
|
|
105
|
+
out[kebab(key)] = isToken(node) ? projectToken(node) : projectGroup(node);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
package/src/renderers/json.ts
CHANGED
|
@@ -1,81 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JSON renderer —
|
|
2
|
+
* JSON renderer — project the token IR to DTCG-compliant JSON (the full
|
|
3
|
+
* single-document surface, used by `renderers.test.ts` and as the oracle for
|
|
4
|
+
* resolver composition-equivalence).
|
|
3
5
|
*
|
|
4
|
-
* **Spec:** [Design Tokens
|
|
5
|
-
* (W3C draft track
|
|
6
|
+
* **Spec:** [Design Tokens Format Module 2025.10](https://www.designtokens.org/TR/drafts/format/)
|
|
7
|
+
* (DTCG / W3C Community Group draft track).
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Pure projection of the IR (`ir.ts`) via the shared `dtcg-project` mapping:
|
|
10
|
+
* - `primitives` → sibling top-level groups (`color` / `space` / `border-width`).
|
|
11
|
+
* - colour/dimension → `{ colorSpace, components, hex, alpha? }` / `{ value, unit }`.
|
|
12
|
+
* - references → DTCG aliases, e.g. `"$value": "{color.primary.base}"`.
|
|
13
|
+
* - composites/scales (S4) → `fontFamily`/`fontWeight`/`number`/`strokeStyle`
|
|
14
|
+
* primitives and the `shadow` composite `{ color, offsetX, offsetY, blur, spread, inset? }`.
|
|
11
15
|
*
|
|
12
|
-
*
|
|
13
|
-
* Specify, and any tool consuming the DTCG spec.
|
|
16
|
+
* Non-DTCG values travel in the document `$extensions` (`com.apollion.*`).
|
|
14
17
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
18
|
+
* **Build surface note:** `build.ts` no longer emits this full per-variant
|
|
19
|
+
* document to `dist/json/` — the shipped JSON surface is the Resolver + sets
|
|
20
|
+
* (`resolver.ts`). This renderer remains the in-memory SSOT/oracle.
|
|
21
|
+
*
|
|
22
|
+
* @see ../ir.ts (source of truth) · ./dtcg-project.ts · ./resolver.ts
|
|
23
|
+
* @see tech-radar R1/R2/R3/R4 · ADR-006 §3.10
|
|
17
24
|
*/
|
|
18
25
|
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
import type { Variant } from '../config-schema';
|
|
22
|
-
|
|
23
|
-
interface DtcgToken<T extends string = string> {
|
|
24
|
-
$value: string;
|
|
25
|
-
$type: T;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface DtcgGroup {
|
|
29
|
-
[key: string]: DtcgToken | DtcgGroup;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface DtcgDocument {
|
|
33
|
-
$description: string;
|
|
34
|
-
bg: DtcgGroup;
|
|
35
|
-
text: DtcgGroup;
|
|
36
|
-
spacing: DtcgGroup;
|
|
37
|
-
/** Variant metadata — non-standard extension under DTCG `$extensions` namespace. */
|
|
38
|
-
$extensions: {
|
|
39
|
-
'com.apollion.variant': {
|
|
40
|
-
brand: string;
|
|
41
|
-
mode: string;
|
|
42
|
-
surface: string;
|
|
43
|
-
dimension: string;
|
|
44
|
-
};
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function toColorGroup(layer: Record<string, string>): DtcgGroup {
|
|
49
|
-
const out: DtcgGroup = {};
|
|
50
|
-
for (const [key, value] of Object.entries(layer)) {
|
|
51
|
-
const kebab = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
52
|
-
out[kebab] = { $value: value, $type: 'color' };
|
|
53
|
-
}
|
|
54
|
-
return out;
|
|
55
|
-
}
|
|
26
|
+
import { projectGroup } from './dtcg-project';
|
|
56
27
|
|
|
57
|
-
|
|
58
|
-
const out: DtcgGroup = {};
|
|
59
|
-
for (const [key, value] of Object.entries(layer)) {
|
|
60
|
-
out[key] = { $value: value, $type: 'dimension' };
|
|
61
|
-
}
|
|
62
|
-
return out;
|
|
63
|
-
}
|
|
28
|
+
import type { IRDocument } from '../ir';
|
|
64
29
|
|
|
65
|
-
export function renderJson(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
mode: variant.mode,
|
|
75
|
-
surface: variant.surface,
|
|
76
|
-
dimension: variant.dimension,
|
|
77
|
-
},
|
|
78
|
-
},
|
|
30
|
+
export function renderJson(ir: IRDocument): string {
|
|
31
|
+
// `primitives` is keyed by namespace (`color` / `space` / `borderWidth`);
|
|
32
|
+
// each projects to a sibling top-level DTCG group, so reference paths like
|
|
33
|
+
// `{space.medium}` resolve at the document root.
|
|
34
|
+
const doc = {
|
|
35
|
+
$description: ir.description,
|
|
36
|
+
...projectGroup(ir.primitives),
|
|
37
|
+
...projectGroup(ir.groups),
|
|
38
|
+
$extensions: ir.extensions,
|
|
79
39
|
};
|
|
80
40
|
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
81
41
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolver renderer (R5) — project the IR slices to a DTCG **Resolver** manifest
|
|
3
|
+
* plus the reference-closed set documents it points at.
|
|
4
|
+
*
|
|
5
|
+
* **Spec:** DTCG Resolver draft (Design Tokens Community Group — Resolver /
|
|
6
|
+
* "modes" module on the 2025.10 draft track). A resolver names a set of token
|
|
7
|
+
* files (`sets`) and a set of `modifiers` (enumerated axes with a `default`); a
|
|
8
|
+
* consumer composes the chosen modifier values into a single resolved token
|
|
9
|
+
* tree. This replaces the old N-file per-variant JSON explosion with one
|
|
10
|
+
* manifest + O(brand·mode·surface + dimension + 1) sets.
|
|
11
|
+
*
|
|
12
|
+
* **S5 mode caveat:** `mode` is emitted as a real modifier (values + default)
|
|
13
|
+
* even though palette generation is currently mode-agnostic (dark == light).
|
|
14
|
+
* The topology is therefore correct ahead of S6 wiring real dark-mode logic.
|
|
15
|
+
*
|
|
16
|
+
* Set documents are projected from `IRSlice`s via the shared `dtcg-project`
|
|
17
|
+
* mapping, so a composed (base + color + spacing) tree is byte-equivalent to the
|
|
18
|
+
* old full per-variant document (asserted in `resolver.test.ts`).
|
|
19
|
+
*
|
|
20
|
+
* @see ../ir.ts (slices) · ./dtcg-project.ts · ../set-plan.ts
|
|
21
|
+
* @see https://www.designtokens.org/TR/ (Resolver draft)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { projectGroup } from './dtcg-project';
|
|
25
|
+
|
|
26
|
+
import type { ApollionConfig } from '../config-schema';
|
|
27
|
+
import type { IRSlice } from '../ir';
|
|
28
|
+
import type { SetPlan } from '../set-plan';
|
|
29
|
+
|
|
30
|
+
/** A DTCG modifier: an enumerated axis with a default value. */
|
|
31
|
+
interface ResolverModifier {
|
|
32
|
+
name: string;
|
|
33
|
+
values: string[];
|
|
34
|
+
default: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A DTCG set reference: a named token document at `source`. */
|
|
38
|
+
interface ResolverSet {
|
|
39
|
+
name: string;
|
|
40
|
+
source: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ResolverManifest {
|
|
44
|
+
name: string;
|
|
45
|
+
description: string;
|
|
46
|
+
sets: ResolverSet[];
|
|
47
|
+
modifiers: ResolverModifier[];
|
|
48
|
+
$extensions: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Render one IR slice (base/color/spacing) to a DTCG set JSON document. */
|
|
52
|
+
function renderSet(slice: IRSlice, description: string): string {
|
|
53
|
+
const doc: Record<string, unknown> = {
|
|
54
|
+
$description: description,
|
|
55
|
+
...projectGroup(slice.groups),
|
|
56
|
+
};
|
|
57
|
+
if (Object.keys(slice.extensions).length > 0) doc.$extensions = slice.extensions;
|
|
58
|
+
return `${JSON.stringify(doc, null, 2)}\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Axis-invariant base set (border + font). */
|
|
62
|
+
export function renderBaseSet(slice: IRSlice): string {
|
|
63
|
+
return renderSet(slice, 'Apollion DS — base set (axis-invariant: border + font). DTCG 2025.10.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Colour set for one (brand × mode × surface). */
|
|
67
|
+
export function renderColorSet(slice: IRSlice, brand: string, mode: string, surface: string): string {
|
|
68
|
+
return renderSet(slice, `Apollion DS — colour set. brand=${brand} mode=${mode} surface=${surface}. DTCG 2025.10.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Spacing set for one dimension. */
|
|
72
|
+
export function renderSpacingSet(slice: IRSlice, dimension: string): string {
|
|
73
|
+
return renderSet(slice, `Apollion DS — spacing set. dimension=${dimension}. DTCG 2025.10.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Render the Resolver manifest (`resolver.json`).
|
|
78
|
+
*
|
|
79
|
+
* Modifiers enumerate the four axes (brand/mode/surface/dimension) with the
|
|
80
|
+
* first encountered value as `default`. `sets` lists every concrete set file.
|
|
81
|
+
* The `$extensions['com.apollion.resolver']` block documents the composition
|
|
82
|
+
* rule — which sets to merge for a chosen modifier tuple — since the draft's
|
|
83
|
+
* apply-by-modifier binding is still settling; this keeps the manifest
|
|
84
|
+
* self-describing for our own consumers.
|
|
85
|
+
*/
|
|
86
|
+
export function renderResolver(config: ApollionConfig, plan: SetPlan): string {
|
|
87
|
+
const { modifiers } = plan;
|
|
88
|
+
|
|
89
|
+
const modifierList: ResolverModifier[] = [
|
|
90
|
+
{ name: 'brand', values: [...modifiers.brand], default: modifiers.brand[0] },
|
|
91
|
+
{ name: 'mode', values: [...modifiers.mode], default: modifiers.mode[0] },
|
|
92
|
+
{ name: 'surface', values: [...modifiers.surface], default: modifiers.surface[0] },
|
|
93
|
+
{ name: 'dimension', values: [...modifiers.dimension], default: modifiers.dimension[0] },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const sets: ResolverSet[] = [
|
|
97
|
+
{ name: plan.base.name, source: plan.base.file },
|
|
98
|
+
...plan.spacing.map((s) => ({ name: s.name, source: s.file })),
|
|
99
|
+
...plan.color.map((c) => ({ name: c.name, source: c.file })),
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const manifest: ResolverManifest = {
|
|
103
|
+
name: 'apollion-tokens',
|
|
104
|
+
description:
|
|
105
|
+
'Apollion DS token resolver — DTCG 2025.10. Compose the base set with the ' +
|
|
106
|
+
'colour set for the chosen brand/mode/surface and the spacing set for the ' +
|
|
107
|
+
'chosen dimension. NOTE: mode is a topology placeholder in this release ' +
|
|
108
|
+
'(dark resolves identically to light until S6).',
|
|
109
|
+
sets,
|
|
110
|
+
modifiers: modifierList,
|
|
111
|
+
$extensions: {
|
|
112
|
+
'com.apollion.resolver': {
|
|
113
|
+
// How to compose a final token tree for a chosen modifier tuple.
|
|
114
|
+
composition: {
|
|
115
|
+
always: [plan.base.name],
|
|
116
|
+
byModifier: {
|
|
117
|
+
// colour set name = `<brand>.color.<mode>.<surface>`
|
|
118
|
+
color: '${brand}.color.${mode}.${surface}',
|
|
119
|
+
// spacing set name = `spacing.<dimension>`
|
|
120
|
+
spacing: 'spacing.${dimension}',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
modeIsPlaceholder: true,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
129
|
+
}
|
package/src/renderers/ts.ts
CHANGED
|
@@ -1,46 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TypeScript renderer —
|
|
2
|
+
* TypeScript renderer — project the token IR to an `as const` literal (the
|
|
3
|
+
* resolved consumer surface).
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Consumers importing the generated `.d.ts` get literal types (e.g.
|
|
6
|
+
* `tokens.bg.primary: '#003750'` not `string`) — autocomplete + compile-time
|
|
7
|
+
* invariant checks (tech-radar R4). References resolve to literal values, the
|
|
8
|
+
* primitives layer is omitted, and nested groups (border/font/shadow) nest as
|
|
9
|
+
* nested objects.
|
|
7
10
|
*
|
|
8
|
-
* @see tech-radar R4
|
|
9
|
-
* @see ADR-006 §3.10 + PRD-002 §S5
|
|
11
|
+
* @see ../ir.ts (source of truth) · tech-radar R4 · ADR-006 §3.10
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
|
-
import type {
|
|
13
|
-
|
|
14
|
-
import type { Variant } from '../config-schema';
|
|
14
|
+
import type { IRDocument, IRGroup } from '../ir';
|
|
15
|
+
import { isToken, rawValue } from '../ir';
|
|
15
16
|
|
|
16
17
|
const HEADER_LINE = '// Generated by apollion-tokens build — DO NOT EDIT. See apollion.config.mjs.';
|
|
17
18
|
|
|
18
|
-
function
|
|
19
|
-
const pad = '
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
function emitGroup(group: IRGroup, depth: number, out: string[]): void {
|
|
20
|
+
const pad = ' '.repeat(depth);
|
|
21
|
+
for (const [key, node] of Object.entries(group)) {
|
|
22
|
+
if (isToken(node)) {
|
|
23
|
+
out.push(`${pad}${JSON.stringify(key)}: ${JSON.stringify(rawValue(node.value))},`);
|
|
24
|
+
} else {
|
|
25
|
+
out.push(`${pad}${key}: {`);
|
|
26
|
+
emitGroup(node, depth + 1, out);
|
|
27
|
+
out.push(`${pad}},`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
export function renderTs(
|
|
27
|
-
|
|
32
|
+
export function renderTs(ir: IRDocument): string {
|
|
33
|
+
const { brand, mode, surface, dimension } = ir.meta;
|
|
34
|
+
const out: string[] = [
|
|
28
35
|
HEADER_LINE,
|
|
29
|
-
`// Variant: brand=${
|
|
36
|
+
`// Variant: brand=${brand} mode=${mode} surface=${surface} dimension=${dimension}`,
|
|
30
37
|
'',
|
|
31
38
|
'export const tokens = {',
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
' spacing: {',
|
|
39
|
-
serializeRecord(foundation.spacing as unknown as Record<string, string>, 4),
|
|
40
|
-
' },',
|
|
41
|
-
'} as const;',
|
|
42
|
-
'',
|
|
43
|
-
'export type Tokens = typeof tokens;',
|
|
44
|
-
'',
|
|
45
|
-
].join('\n');
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
emitGroup(ir.groups, 1, out);
|
|
42
|
+
|
|
43
|
+
out.push('} as const;', '', 'export type Tokens = typeof tokens;', '');
|
|
44
|
+
return out.join('\n');
|
|
46
45
|
}
|
package/src/set-plan.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Set topology planner (R5) — the single source of truth for which Resolver
|
|
3
|
+
* sets exist, their file paths, and the modifier enumerations.
|
|
4
|
+
*
|
|
5
|
+
* Both `build.ts` (which writes the set files) and `resolver.ts` (which writes
|
|
6
|
+
* the manifest pointing at them) derive from this plan, so set names can never
|
|
7
|
+
* drift between the two. Derived purely from the expanded variants of a config.
|
|
8
|
+
*
|
|
9
|
+
* **Set topology** (each set is reference-closed, see `ir.ts` slices):
|
|
10
|
+
* - `base` — 1 file, axis-invariant (border + font).
|
|
11
|
+
* - `spacing.<dimension>` — one per dimension.
|
|
12
|
+
* - `<brand>.color.<mode>.<surface>` — one per (brand × mode × surface).
|
|
13
|
+
*
|
|
14
|
+
* @see ir.ts (`buildBaseIR` / `buildColorIR` / `buildSpacingIR`)
|
|
15
|
+
* @see renderers/resolver.ts (manifest) · build.ts (set emission)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Dimension } from '@apollion-dsi/core/themes/dimension';
|
|
19
|
+
|
|
20
|
+
import type { ApollionConfig, Mode, Surface } from './config-schema';
|
|
21
|
+
import { expandVariants } from './config-schema';
|
|
22
|
+
|
|
23
|
+
/** Directory (relative to dist root) holding the emitted set files. */
|
|
24
|
+
export const SETS_DIR = 'json/sets';
|
|
25
|
+
/** Resolver manifest path (relative to dist root). */
|
|
26
|
+
export const RESOLVER_PATH = 'json/resolver.json';
|
|
27
|
+
|
|
28
|
+
/** A colour set keyed by its (brand × mode × surface) coordinate. */
|
|
29
|
+
export interface ColorSetPlan {
|
|
30
|
+
readonly kind: 'color';
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly file: string;
|
|
33
|
+
readonly brand: string;
|
|
34
|
+
readonly mode: Mode;
|
|
35
|
+
readonly surface: Surface;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A spacing set keyed by its dimension. */
|
|
39
|
+
export interface SpacingSetPlan {
|
|
40
|
+
readonly kind: 'spacing';
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly file: string;
|
|
43
|
+
readonly dimension: Dimension;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The axis-invariant base set. */
|
|
47
|
+
export interface BaseSetPlan {
|
|
48
|
+
readonly kind: 'base';
|
|
49
|
+
readonly name: string;
|
|
50
|
+
readonly file: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SetPlan {
|
|
54
|
+
readonly base: BaseSetPlan;
|
|
55
|
+
readonly spacing: readonly SpacingSetPlan[];
|
|
56
|
+
readonly color: readonly ColorSetPlan[];
|
|
57
|
+
/** Distinct modifier values (declaration/encounter order), for the manifest. */
|
|
58
|
+
readonly modifiers: {
|
|
59
|
+
readonly brand: readonly string[];
|
|
60
|
+
readonly mode: readonly Mode[];
|
|
61
|
+
readonly surface: readonly Surface[];
|
|
62
|
+
readonly dimension: readonly Dimension[];
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Set source path relative to the dist root (used by the resolver manifest). */
|
|
67
|
+
function setFile(name: string): string {
|
|
68
|
+
return `${SETS_DIR}/${name}.json`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Stable colour-set name for a coordinate: `<brand>.color.<mode>.<surface>`. */
|
|
72
|
+
export function colorSetName(brand: string, mode: Mode, surface: Surface): string {
|
|
73
|
+
return `${brand}.color.${mode}.${surface}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Stable spacing-set name for a dimension: `spacing.<dimension>`. */
|
|
77
|
+
export function spacingSetName(dimension: Dimension): string {
|
|
78
|
+
return `spacing.${dimension}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Push `value` onto `list` if absent — preserves first-encounter order. */
|
|
82
|
+
function pushUnique<T>(list: T[], value: T): void {
|
|
83
|
+
if (!list.includes(value)) list.push(value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Derive the full set topology from a config. Deterministic: ordering follows
|
|
88
|
+
* `expandVariants` (brands alphabetical → declaration order for the rest), so
|
|
89
|
+
* the emitted manifest + files are byte-stable across runs.
|
|
90
|
+
*/
|
|
91
|
+
export function planSets(config: ApollionConfig): SetPlan {
|
|
92
|
+
const variants = expandVariants(config);
|
|
93
|
+
|
|
94
|
+
const brands: string[] = [];
|
|
95
|
+
const modes: Mode[] = [];
|
|
96
|
+
const surfaces: Surface[] = [];
|
|
97
|
+
const dimensions: Dimension[] = [];
|
|
98
|
+
|
|
99
|
+
const colorSeen = new Set<string>();
|
|
100
|
+
const color: ColorSetPlan[] = [];
|
|
101
|
+
const spacingSeen = new Set<string>();
|
|
102
|
+
const spacing: SpacingSetPlan[] = [];
|
|
103
|
+
|
|
104
|
+
for (const v of variants) {
|
|
105
|
+
pushUnique(brands, v.brand);
|
|
106
|
+
pushUnique(modes, v.mode);
|
|
107
|
+
pushUnique(surfaces, v.surface);
|
|
108
|
+
pushUnique(dimensions, v.dimension);
|
|
109
|
+
|
|
110
|
+
const cName = colorSetName(v.brand, v.mode, v.surface);
|
|
111
|
+
if (!colorSeen.has(cName)) {
|
|
112
|
+
colorSeen.add(cName);
|
|
113
|
+
color.push({
|
|
114
|
+
kind: 'color',
|
|
115
|
+
name: cName,
|
|
116
|
+
file: setFile(cName),
|
|
117
|
+
brand: v.brand,
|
|
118
|
+
mode: v.mode,
|
|
119
|
+
surface: v.surface,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const sName = spacingSetName(v.dimension);
|
|
124
|
+
if (!spacingSeen.has(sName)) {
|
|
125
|
+
spacingSeen.add(sName);
|
|
126
|
+
spacing.push({ kind: 'spacing', name: sName, file: setFile(sName), dimension: v.dimension });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
base: { kind: 'base', name: 'base', file: setFile('base') },
|
|
132
|
+
spacing,
|
|
133
|
+
color,
|
|
134
|
+
modifiers: { brand: brands, mode: modes, surface: surfaces, dimension: dimensions },
|
|
135
|
+
};
|
|
136
|
+
}
|