@apollion-dsi/tokens 7.2.0 → 7.4.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
@@ -36,10 +36,11 @@ import { createFont, defaultInputFont } from '@apollion-dsi/core/themes/font';
36
36
  import type { FoundationLayer } from '@apollion-dsi/core/themes/foundation';
37
37
  import type { SpacingAlias } from '@apollion-dsi/core/themes/spacing';
38
38
  import { defaultInputSpacing } from '@apollion-dsi/core/themes/spacing';
39
+ import { DEFAULT_TEXT_SIZE, type TextSize } from '@apollion-dsi/core/themes/text-size';
39
40
  import { converter, formatHex, parse } from 'culori';
40
41
 
41
42
  import { breakpointEntries } from './builders/breakpoints';
42
- import { containerSizeEntries, layoutEntries } from './builders/layout';
43
+ import { containerSizeEntries, layoutEntries, zIndexEntries } from './builders/layout';
43
44
  import {
44
45
  motionDurationEntries,
45
46
  motionEasingEntries,
@@ -518,14 +519,14 @@ function buildBorder(primitives: IRGroup): { group: IRGroup; extensions: Record<
518
519
  * Font scales: family/size/weight/lineHeight as DTCG types; non-DTCG bits
519
520
  * (letterSpacing/textTransform/fontStyle) go to `$extensions`.
520
521
  *
521
- * **ADR-011:** `size` is now density-aware. Built with the SAME `dimension` the
522
- * variant uses (`createFont`), so a `font.size.*` token emits the resolved `rem`
523
- * value for that density (e.g. `medium = 1.25rem` at normal, `1rem` at compact),
524
- * mirroring `buildSpacePrimitives`. `family/weight/lineHeight` stay axis-invariant
525
- * — they're identical across dimensions, just carried in each font set.
522
+ * **ADR-030:** `size` follows the `textSize` axis (independent of `dimension`),
523
+ * built with core's `createFont`, so a `font.size.*` token emits the resolved
524
+ * `rem` value for that level (e.g. `medium = 1.25rem` at md, `1.5625rem` at xl).
525
+ * `family/weight/lineHeight` stay axis-invariant — identical across levels, just
526
+ * carried in each font set.
526
527
  */
527
- function buildFont(dimension: Dimension): { group: IRGroup; extensions: Record<string, unknown> } {
528
- const resolved = createFont(defaultInputFont, dimension);
528
+ function buildFont(textSize: TextSize): { group: IRGroup; extensions: Record<string, unknown> } {
529
+ const resolved = createFont(defaultInputFont, { textSize });
529
530
  const family: Record<string, IRToken> = {};
530
531
  for (const [key, value] of Object.entries(defaultInputFont.fontFamily)) {
531
532
  const clean = value.replace(/;\s*$/, '').trim();
@@ -656,7 +657,7 @@ function decorateMeta(doc: IRDocument): IRDocument {
656
657
  // - base → border (+ `border-width.*` it references) [axis-invariant]
657
658
  // - color → `color.*` primitives + bg/text (refs) + shadow [brand×mode×surface]
658
659
  // - spacing → `space.*` primitives + spacing (refs) [dimension]
659
- // - font → font scale (size density-aware, ADR-011) [dimension]
660
+ // - font → font scale (size per text-size level, ADR-030) [textSize]
660
661
  // `decorateMeta` runs per-slice to attach prose; the build asserts the UNION of
661
662
  // all emitted paths is meta-complete (a single set is intentionally partial).
662
663
  // ───────────────────────────────────────────────────────────────────────────
@@ -731,6 +732,20 @@ function buildLayoutConstants(): IRGroup {
731
732
  return out;
732
733
  }
733
734
 
735
+ /**
736
+ * Cross-component `z-index` stacking scale (APOLLION-ZINDEX-TOKENS, plan §5)
737
+ * — axis-invariant mirror of `theme.zIndex`. `number` tokens, not
738
+ * `dimension`: `z-index` is unitless by spec, the same shape `lineHeight`
739
+ * uses elsewhere in this file.
740
+ */
741
+ function buildZIndex(): IRGroup {
742
+ const out: Record<string, IRToken> = {};
743
+ for (const [key, value] of zIndexEntries()) {
744
+ out[key] = { type: 'number', value: { kind: 'number', raw: `${value}`, number: value } };
745
+ }
746
+ return out;
747
+ }
748
+
734
749
  /**
735
750
  * Motion scales (ADR-027) — axis-invariant mirror of `theme.motion` via the
736
751
  * Strangler Fig builder. Tier-1 `duration`/`easing` primitives, the
@@ -799,9 +814,9 @@ const BREAKPOINT_EXTENSION = {
799
814
  * primitives, which travel WITH this slice (`border-width` namespace) so the
800
815
  * set is reference-closed. Identical for every brand/mode/surface/dimension.
801
816
  *
802
- * **ADR-011:** font left the base set — its `size` scale is now density-aware,
803
- * so it lives in per-dimension `font.<dimension>` sets ({@link buildFontIR}),
804
- * symmetric with spacing.
817
+ * **ADR-011 / ADR-030:** font is not in the base set — its `size` scale varies
818
+ * on the `textSize` axis, so it lives in per-level `font.<textSize>` sets
819
+ * ({@link buildFontIR}).
805
820
  */
806
821
  export function buildBaseIR(): IRSlice {
807
822
  const borderWidth = buildBorderWidthPrimitives();
@@ -821,6 +836,7 @@ export function buildBaseIR(): IRSlice {
821
836
  breakpoint: buildBreakpoints(),
822
837
  'container-size': buildContainerSizes(),
823
838
  layout: buildLayoutConstants(),
839
+ 'z-index': buildZIndex(),
824
840
  motion: buildMotion(),
825
841
  },
826
842
  seen,
@@ -829,13 +845,13 @@ export function buildBaseIR(): IRSlice {
829
845
  }
830
846
 
831
847
  /**
832
- * Font slice for one dimension: the typographic scale built with that dimension's
833
- * multiplier (ADR-011). Mirrors {@link buildSpacingIR} — font joined the density
834
- * axis, so it is no longer part of the axis-invariant base set. The non-DTCG bits
835
- * (letterSpacing/textTransform/fontStyle) ride along in `$extensions`.
848
+ * Font slice for one text-size level (ADR-030): the typographic scale built with
849
+ * that level's ratio. Independent of `dimension` — density drives spacing only.
850
+ * The non-DTCG bits (letterSpacing/textTransform/fontStyle) ride along in
851
+ * `$extensions`.
836
852
  */
837
- export function buildFontIR(dimension: Dimension): IRSlice {
838
- const font = buildFont(dimension);
853
+ export function buildFontIR(textSize: TextSize): IRSlice {
854
+ const font = buildFont(textSize);
839
855
  const seen = new Set<string>();
840
856
  const groups = decorateSlice({ font: font.group }, seen);
841
857
  return { groups, extensions: { 'com.apollion.font': font.extensions } };
@@ -900,7 +916,9 @@ export function buildIR(foundation: FoundationLayer, colors: ColorsThemeInterfac
900
916
  };
901
917
  const primitives = buildPrimitives(colors, variant.dimension);
902
918
  const border = buildBorder(primitives);
903
- const font = buildFont(variant.dimension);
919
+ // Per-variant documents (CSS/TS) carry the default `default` scale; the other
920
+ // levels ship in `font.<textSize>` sets and `css/font.css` (ADR-030).
921
+ const font = buildFont(DEFAULT_TEXT_SIZE);
904
922
 
905
923
  const extensions: Record<string, unknown> = {
906
924
  'com.apollion.variant': meta,
@@ -925,6 +943,7 @@ export function buildIR(foundation: FoundationLayer, colors: ColorsThemeInterfac
925
943
  breakpoint: buildBreakpoints(),
926
944
  'container-size': buildContainerSizes(),
927
945
  layout: buildLayoutConstants(),
946
+ 'z-index': buildZIndex(),
928
947
  motion: buildMotion(),
929
948
  font: font.group,
930
949
  shadow: buildShadow(colors),
@@ -13,9 +13,11 @@
13
13
  * @see ../ir.ts (source of truth) · ADR-006 §3.3 + §3.10 · tech-radar R3
14
14
  */
15
15
 
16
+ import { DEFAULT_TEXT_SIZE, TEXT_SIZE_ATTRIBUTE } from '@apollion-dsi/core/themes/text-size';
17
+
16
18
  import { colorPayload, isOpaqueColor, rgbTriplet } from './tailwind';
17
19
 
18
- import type { IRDocument, IRGroup } from '../ir';
20
+ import type { IRDocument, IRGroup, IRSlice } from '../ir';
19
21
  import { isToken, kebab, rawValue } from '../ir';
20
22
 
21
23
  const HEADER_LINE = '/* Generated by apollion-tokens build — DO NOT EDIT. See apollion.config.mjs. */';
@@ -138,3 +140,58 @@ export function renderCssModes(light: IRDocument, dark: IRDocument, options: Ren
138
140
 
139
141
  return lines.join('\n');
140
142
  }
143
+
144
+ /** Options of {@link renderCssTextSize}. */
145
+ export interface RenderCssTextSizeOptions {
146
+ /**
147
+ * Attribute carrying the active level, on `<html>` or any subtree root —
148
+ * core's `TEXT_SIZE_ATTRIBUTE`, the one `ApollionProvider` writes.
149
+ *
150
+ * @default 'data-apollion-text-size'
151
+ */
152
+ attribute?: string;
153
+ /**
154
+ * Level emitted on the bare `:root` block.
155
+ *
156
+ * @default 'default'
157
+ */
158
+ defaultLevel?: string;
159
+ }
160
+
161
+ /**
162
+ * `css/font.css` — the text-size axis (ADR-030) as CSS custom properties.
163
+ *
164
+ * Only `--apollion-font-size-*` vary between levels (family/weight/line-height
165
+ * are level-invariant and already live in the per-variant files). The default
166
+ * level sits on `:root`; every level also gets
167
+ * `:root[attr="<level>"], [attr="<level>"]`: the `:root[…]` form (specificity
168
+ * 0,2,0) beats the plain `:root` block of a per-variant file regardless of load
169
+ * order, and the bare `[…]` form rescales a subtree. Switch at runtime by
170
+ * setting the attribute — no rebuild, no reload.
171
+ */
172
+ export function renderCssTextSize(
173
+ levels: ReadonlyArray<readonly [string, IRSlice]>,
174
+ options: RenderCssTextSizeOptions = {},
175
+ ): string {
176
+ const { attribute = TEXT_SIZE_ATTRIBUTE, defaultLevel = DEFAULT_TEXT_SIZE } = options;
177
+ const lines: string[] = [HEADER_LINE, ''];
178
+ lines.push(
179
+ `/* Text size (ADR-030): levels=${levels.map(([level]) => level).join('+')} default=${defaultLevel} (attribute ${attribute}) */`,
180
+ '',
181
+ );
182
+
183
+ const block = (selector: string, slice: IRSlice): void => {
184
+ lines.push(`${selector} {`);
185
+ const font = slice.groups.font as IRGroup;
186
+ emitVars(font.size as IRGroup, ['font', 'size'], lines);
187
+ lines.push('}', '');
188
+ };
189
+
190
+ const fallback = levels.find(([level]) => level === defaultLevel);
191
+ if (fallback) block(':root', fallback[1]);
192
+ for (const [level, slice] of levels) {
193
+ block(`:root[${attribute}="${level}"],\n[${attribute}="${level}"]`, slice);
194
+ }
195
+
196
+ return lines.join('\n');
197
+ }
@@ -7,8 +7,8 @@
7
7
  * files (`sets`) and a set of `modifiers` (enumerated axes with a `default`); a
8
8
  * consumer composes the chosen modifier values into a single resolved token
9
9
  * tree. This replaces the old N-file per-variant JSON explosion with one
10
- * manifest + O(brand·mode·surface + 2·dimension + 1) sets (spacing + font per
11
- * dimension).
10
+ * manifest + O(brand·mode·surface + dimension + textSize + 1) sets (spacing per
11
+ * dimension, font per text-size level — ADR-030).
12
12
  *
13
13
  * **Mode modifier:** `mode` is emitted as a real modifier (values + default),
14
14
  * and since ADR-019 the dark contexts carry real values — palette generation
@@ -23,11 +23,13 @@
23
23
  * @see https://www.designtokens.org/TR/ (Resolver draft)
24
24
  */
25
25
 
26
+ import { DEFAULT_TEXT_SIZE } from '@apollion-dsi/core/themes/text-size';
27
+
26
28
  import { projectGroup } from './dtcg-project';
27
29
 
28
30
  import type { ApollionConfig } from '../config-schema';
29
31
  import type { IRSlice } from '../ir';
30
- import type { SetPlan } from '../set-plan';
32
+ import type { FontSetPlan, SetPlan } from '../set-plan';
31
33
 
32
34
  /** A DTCG modifier: an enumerated axis with a default value. */
33
35
  interface ResolverModifier {
@@ -51,10 +53,15 @@ interface ResolverManifest {
51
53
  }
52
54
 
53
55
  /** Render one IR slice (base/color/spacing) to a DTCG set JSON document. */
54
- function renderSet(slice: IRSlice, description: string): string {
56
+ function renderSet(slice: IRSlice, description: string, deprecated?: string): string {
57
+ const groups = projectGroup(slice.groups) as Record<string, Record<string, unknown>>;
58
+ // DTCG `$deprecated` on each top-level group, so DTCG-only tooling sees it too.
59
+ if (deprecated) {
60
+ for (const key of Object.keys(groups)) groups[key] = { $deprecated: deprecated, ...groups[key] };
61
+ }
55
62
  const doc: Record<string, unknown> = {
56
63
  $description: description,
57
- ...projectGroup(slice.groups),
64
+ ...groups,
58
65
  };
59
66
  if (Object.keys(slice.extensions).length > 0) doc.$extensions = slice.extensions;
60
67
  return `${JSON.stringify(doc, null, 2)}\n`;
@@ -78,16 +85,24 @@ export function renderSpacingSet(slice: IRSlice, dimension: string): string {
78
85
  return renderSet(slice, `Apollion DS — spacing set. dimension=${dimension}. DTCG 2025.10.`);
79
86
  }
80
87
 
81
- /** Font set for one dimension (size density-aware, ADR-011). */
82
- export function renderFontSet(slice: IRSlice, dimension: string): string {
83
- return renderSet(slice, `Apollion DS — font set. dimension=${dimension}. DTCG 2025.10.`);
88
+ /** Font set for one text-size level (ADR-030), or a deprecated `font.<dimension>` alias. */
89
+ export function renderFontSet(slice: IRSlice, set: Pick<FontSetPlan, 'textSize' | 'deprecatedDimension'>): string {
90
+ if (set.deprecatedDimension) {
91
+ return renderSet(
92
+ slice,
93
+ `Apollion DS — font set. DEPRECATED alias of font.${set.textSize} (dimension=${set.deprecatedDimension} no longer scales type, ADR-030; removed at the next major). DTCG 2025.10.`,
94
+ `Alias of font.${set.textSize} — dimension no longer scales type (ADR-030). Use font.<textSize>.`,
95
+ );
96
+ }
97
+ return renderSet(slice, `Apollion DS — font set. textSize=${set.textSize}. DTCG 2025.10.`);
84
98
  }
85
99
 
86
100
  /**
87
101
  * Render the Resolver manifest (`resolver.json`).
88
102
  *
89
- * Modifiers enumerate the four axes (brand/mode/surface/dimension) with the
90
- * first encountered value as `default`. `sets` lists every concrete set file.
103
+ * Modifiers enumerate the five axes (brand/mode/surface/dimension/textSize)
104
+ * with the first encountered value as `default` (`textSize` always defaults to
105
+ * `default`). `sets` lists every concrete set file.
91
106
  * The `$extensions['com.apollion.resolver']` block documents the composition
92
107
  * rule — which sets to merge for a chosen modifier tuple — since the draft's
93
108
  * apply-by-modifier binding is still settling; this keeps the manifest
@@ -101,7 +116,14 @@ export function renderResolver(config: ApollionConfig, plan: SetPlan): string {
101
116
  { name: 'mode', values: [...modifiers.mode], default: modifiers.mode[0] },
102
117
  { name: 'surface', values: [...modifiers.surface], default: modifiers.surface[0] },
103
118
  { name: 'dimension', values: [...modifiers.dimension], default: modifiers.dimension[0] },
119
+ {
120
+ name: 'textSize',
121
+ values: [...modifiers.textSize],
122
+ // `resolveTextSizes` guarantees `default` is present.
123
+ default: DEFAULT_TEXT_SIZE,
124
+ },
104
125
  ];
126
+ const deprecatedFontSets = plan.font.filter((f) => f.deprecatedDimension);
105
127
 
106
128
  const sets: ResolverSet[] = [
107
129
  { name: plan.base.name, source: plan.base.file },
@@ -114,8 +136,8 @@ export function renderResolver(config: ApollionConfig, plan: SetPlan): string {
114
136
  name: 'apollion-tokens',
115
137
  description:
116
138
  'Apollion DS token resolver — DTCG 2025.10. Compose the base set with the ' +
117
- 'colour set for the chosen brand/mode/surface and the spacing + font sets ' +
118
- 'for the chosen dimension. NOTE: mode is a topology placeholder in this ' +
139
+ 'colour set for the chosen brand/mode/surface, the spacing set for the chosen ' +
140
+ 'dimension and the font set for the chosen textSize. NOTE: mode is a topology placeholder in this ' +
119
141
  'release (dark resolves identically to light until S6).',
120
142
  sets,
121
143
  modifiers: modifierList,
@@ -129,10 +151,13 @@ export function renderResolver(config: ApollionConfig, plan: SetPlan): string {
129
151
  color: '${brand}.color.${mode}.${surface}',
130
152
  // spacing set name = `spacing.<dimension>`
131
153
  spacing: 'spacing.${dimension}',
132
- // font set name = `font.<dimension>` (size density-aware, ADR-011)
133
- font: 'font.${dimension}',
154
+ // font set name = `font.<textSize>` (ADR-030 — independent of dimension)
155
+ font: 'font.${textSize}',
134
156
  },
135
157
  },
158
+ // `font.<dimension>` sets still ship as aliases of `font.default` (ADR-030
159
+ // compatibility); removed at the next major.
160
+ deprecatedSets: Object.fromEntries(deprecatedFontSets.map((f) => [f.name, `font.${f.textSize}`])),
136
161
  modeIsPlaceholder: true,
137
162
  },
138
163
  },
package/src/set-plan.ts CHANGED
@@ -9,7 +9,9 @@
9
9
  * **Set topology** (each set is reference-closed, see `ir.ts` slices):
10
10
  * - `base` — 1 file, axis-invariant (border).
11
11
  * - `spacing.<dimension>` — one per dimension.
12
- * - `font.<dimension>` — one per dimension (size density-aware, ADR-011).
12
+ * - `font.<textSize>` — one per text-size level (ADR-030).
13
+ * - `font.<dimension>` — DEPRECATED aliases of `font.default` (same content), kept so
14
+ * pinned set paths still resolve after ADR-030; removed at the next major.
13
15
  * - `<brand>.color.<mode>.<surface>` — one per (brand × mode × surface).
14
16
  *
15
17
  * @see ir.ts (`buildBaseIR` / `buildColorIR` / `buildSpacingIR` / `buildFontIR`)
@@ -17,9 +19,10 @@
17
19
  */
18
20
 
19
21
  import type { Dimension } from '@apollion-dsi/core/themes/dimension';
22
+ import { DEFAULT_TEXT_SIZE, type TextSize } from '@apollion-dsi/core/themes/text-size';
20
23
 
21
24
  import type { ApollionConfig, Mode, Surface } from './config-schema';
22
- import { expandVariants } from './config-schema';
25
+ import { expandVariants, resolveTextSizes } from './config-schema';
23
26
 
24
27
  /** Directory (relative to dist root) holding the emitted set files. */
25
28
  export const SETS_DIR = 'json/sets';
@@ -44,12 +47,17 @@ export interface SpacingSetPlan {
44
47
  readonly dimension: Dimension;
45
48
  }
46
49
 
47
- /** A font set keyed by its dimension (size density-aware, ADR-011). */
50
+ /** A font set keyed by its text-size level (ADR-030). */
48
51
  export interface FontSetPlan {
49
52
  readonly kind: 'font';
50
53
  readonly name: string;
51
54
  readonly file: string;
52
- readonly dimension: Dimension;
55
+ readonly textSize: TextSize;
56
+ /**
57
+ * Set only on the deprecated `font.<dimension>` aliases: the dimension the
58
+ * legacy name carried. Content is `font.default` — dimension no longer scales type.
59
+ */
60
+ readonly deprecatedDimension?: Dimension;
53
61
  }
54
62
 
55
63
  /** The axis-invariant base set. */
@@ -70,6 +78,7 @@ export interface SetPlan {
70
78
  readonly mode: readonly Mode[];
71
79
  readonly surface: readonly Surface[];
72
80
  readonly dimension: readonly Dimension[];
81
+ readonly textSize: readonly TextSize[];
73
82
  };
74
83
  }
75
84
 
@@ -88,8 +97,13 @@ export function spacingSetName(dimension: Dimension): string {
88
97
  return `spacing.${dimension}`;
89
98
  }
90
99
 
91
- /** Stable font-set name for a dimension: `font.<dimension>`. */
92
- export function fontSetName(dimension: Dimension): string {
100
+ /** Stable font-set name for a text-size level: `font.<textSize>`. */
101
+ export function fontSetName(textSize: TextSize): string {
102
+ return `font.${textSize}`;
103
+ }
104
+
105
+ /** @deprecated Legacy per-dimension font-set name (`font.<dimension>`, alias of `font.default`). */
106
+ export function legacyFontSetName(dimension: Dimension): string {
93
107
  return `font.${dimension}`;
94
108
  }
95
109
 
@@ -115,8 +129,11 @@ export function planSets(config: ApollionConfig): SetPlan {
115
129
  const color: ColorSetPlan[] = [];
116
130
  const spacingSeen = new Set<string>();
117
131
  const spacing: SpacingSetPlan[] = [];
118
- const fontSeen = new Set<string>();
119
- const font: FontSetPlan[] = [];
132
+ const textSizes = [...resolveTextSizes(config)];
133
+ const font: FontSetPlan[] = textSizes.map((textSize) => {
134
+ const name = fontSetName(textSize);
135
+ return { kind: 'font', name, file: setFile(name), textSize };
136
+ });
120
137
 
121
138
  for (const v of variants) {
122
139
  pushUnique(brands, v.brand);
@@ -142,12 +159,19 @@ export function planSets(config: ApollionConfig): SetPlan {
142
159
  spacingSeen.add(sName);
143
160
  spacing.push({ kind: 'spacing', name: sName, file: setFile(sName), dimension: v.dimension });
144
161
  }
162
+ }
145
163
 
146
- const fName = fontSetName(v.dimension);
147
- if (!fontSeen.has(fName)) {
148
- fontSeen.add(fName);
149
- font.push({ kind: 'font', name: fName, file: setFile(fName), dimension: v.dimension });
150
- }
164
+ // Deprecated `font.<dimension>` aliases (ADR-030 compatibility, removed at the
165
+ // next major): same content as `font.default`, after the real sets.
166
+ for (const dimension of dimensions) {
167
+ const name = legacyFontSetName(dimension);
168
+ font.push({
169
+ kind: 'font',
170
+ name,
171
+ file: setFile(name),
172
+ textSize: DEFAULT_TEXT_SIZE,
173
+ deprecatedDimension: dimension,
174
+ });
151
175
  }
152
176
 
153
177
  return {
@@ -155,6 +179,6 @@ export function planSets(config: ApollionConfig): SetPlan {
155
179
  spacing,
156
180
  font,
157
181
  color,
158
- modifiers: { brand: brands, mode: modes, surface: surfaces, dimension: dimensions },
182
+ modifiers: { brand: brands, mode: modes, surface: surfaces, dimension: dimensions, textSize: textSizes },
159
183
  };
160
184
  }