Skip to content

Latest commit

 

History

History
904 lines (731 loc) · 67.8 KB

File metadata and controls

904 lines (731 loc) · 67.8 KB

Multi-Platform Token Export — Design Document

Status: Proposal (not yet implemented). Recommends Approach E (Hybrid). Audience: CEM maintainers, design-system reviewers, downstream platform teams. Decision status: MVP decisions are resolved; see §12 (Decision log).


1. Context & goals

CEM ships a generated token manifest from canonical markdown specs in packages/cem-theme/src/lib/tokens/*.md. The exact count is build output, not design-document truth; dist/lib/tokens/generated-token-coverage.md and manifest-utils.mjs are authoritative. The CSS pipeline is mature and validated. The next strategic step is exposing the same tokens to non-CSS consumers:

  • Figma designers — canvas designs must use canonical CEM token values, not hand-typed hex codes or guesses. Every color swatch, type scale, spacing step, and corner radius in Figma should be a CEM token.
  • Application developers on iOS, Android, Compose, SwiftUI, and other non-web runtimes — native code should consume the same vocabulary as the web. Visual parity across platforms requires a single source of truth.

Constraints (non-negotiable)

  1. Markdown specs remain the single source of truth. Every other format is derived, mirroring how dist/lib/css/*.css is derived today. No format may author tokens that don't exist in markdown.
  2. The existing CSS pipeline must keep working. The CSS generators capture CEM-specific features (color-mix() recipes, .cem-theme-* overrides, @media (forced-colors: active) fallbacks, @container queries, data-cem-{coupling,shape,spacing} attribute selectors) that no off-the-shelf transform reproduces.
  3. Tier discipline propagates. Required and recommended tokens emit by default; optional, adapter-only, and deprecated tokens emit behind explicit flags — same contract as the CSS pipeline.

Implementation state

The repo has design notes for token export, but no implemented Style Dictionary config, DTCG build target, Figma import files, or native platform outputs. Treat this as the first implementation architecture, not a migration plan. Any prototype artifacts must either become generated outputs under dist/ or be removed before release.


2. Architectural principles

┌─────────────────┐
│  *.md (truth)   │
└────────┬────────┘
         │ build:docs (compile-markdown.mjs)
         ▼
┌─────────────────┐         ┌──────────────────────────┐
│  *.xhtml        │────────▶│  CSS pipeline (existing) │──▶ dist/lib/css/*.css
│  (h6 + table)   │         └──────────────────────────┘
└────────┬────────┘
         │ export-tokens.mjs (NEW — extract + classify)
         ▼
┌─────────────────┐
│  intermediate   │
│     JSON        │
└────────┬────────┘
         │ export-tokens.mjs (NEW — resolve through generated CSS when needed)
         ▼
┌─────────────────┐         ┌──────────────────────────┐
│  resolved JSON  │────────▶│  DTCG emission           │──▶ dist/lib/tokens/cem.tokens.json + figma/
│  (per-theme)    │         └──────────┬───────────────┘     + cem.tokens.report.{md,json}
└─────────────────┘                    │
                                       ▼
                          ┌─────────────────────────────┐
                          │  Style Dictionary (Phase D) │──▶ dist/lib/token-platforms/{ios,android,js,scss,json}/
                          └─────────────────────────────┘
                                       │
                                       ▼
                          ┌─────────────────────────────┐
                          │  Native Figma library       │──▶ Figma Variables (read-only)
                          │  (Phase E)                  │
                          └─────────────────────────────┘

Principles:

  • md → XHTML → DTCG JSON → consumers is the new spine. CSS continues from XHTML directly (existing path unchanged).
  • DTCG-compatible JSON is the canonical machine-readable export format. It is based on the Design Tokens Community Group format used by native Figma variable import and Style Dictionary, but CEM metadata stays namespaced under $extensions.cem.
  • Tier-aware emission matches the CSS pipeline; the same tier column in source tables drives all outputs.
  • Token classification. Every token belongs to one of three buckets: cross-platform / web-only / voice-audio (see §5).

3. Approach comparison

Approach Source format Cross-platform fan-out Figma path Pros Cons
A. Style Dictionary on DTCG JSON DTCG JSON (derived from XHTML) Style Dictionary DTCG-aware transforms Native Figma import reads the same JSON Mature ecosystem; standards-aligned; one source feeds many targets Adds Style Dictionary dep; needs custom transforms for color-mix() recipes; multi-dim modes need design
B. Per-platform generators XHTML directly (mirror CSS pipeline) Hand-rolled cem-ios.html, cem-android.html, cem-figma.html generators Custom Figma plugin or static JSON Stays in established pattern; full control over output shape Reinvents Style Dictionary's transform library; massive boilerplate per format; per-platform color/font conversion is error-prone
C. DTCG JSON only DTCG JSON Consumer-owned (apps transform themselves) DTCG-aware tools consume directly Lightest weight; pure-standards; future-proof App teams want native code, not "transform yourself"; high adoption friction
D. Native Figma two-way sync DTCG JSON in repo Same as A Figma syncs both ways through a governed connector Designers can author in Figma, push to repo Two-way sync risks designers overwriting md source-of-truth; needs strict governance and review gates
E. Hybrid (recommended) XHTML → DTCG JSON → {Style Dictionary, Figma import} Style Dictionary fans out to iOS/Android/JS Figma imports or pulls DTCG JSON in a read-only workflow Best of A + C: web stays as-is, native gets Style Dictionary, Figma consumes standards Most components to integrate; needs build-time color resolution

Why Approach E

  • The existing CSS path already works and captures features Style Dictionary's CSS transform doesn't reproduce (color-mix(), theme-mode classes, forced-colors fallbacks, container queries). Replacing it would lose capability.
  • Approach B duplicates Style Dictionary. Writing per-platform generators by hand means re-implementing color conversion, unit scaling (rem→pt/dp), and platform-idiomatic emission for every target — work the SD ecosystem has already done and battle-tested.
  • Approach C punts adoption. App teams asking for tokens want consumable artifacts (Swift constants, Android XML, TS modules), not raw JSON. Friction kills adoption.
  • Approach D's two-way sync is a governance landmine. If a designer in Figma renames a token or changes a value, a write-back connector could push it to Git; the canonical md spec is bypassed. Until CEM has a governance process for designer-authored changes, sync stays one-way (read-only from md).
  • Approach E preserves all three sources of value: the CSS pipeline keeps producing CSS the way it does today, the DTCG JSON layer becomes the canonical machine-readable export, and Style Dictionary handles platform fan-out without us reinventing it.

4. Reference: M3 design tokens

Material Design 3 design tokens guide defines a three-tier model:

  • Reference tokens — primitives (e.g., md.ref.palette.primary40)
  • System tokens — semantic roles (e.g., md.sys.color.primary)
  • Component tokens — component-specific (e.g., md.comp.button.container.color)

CEM's existing layering already maps cleanly:

M3 tier CEM equivalent Examples
Reference Branded colors (D0), thickness scale, size scale, dimension scale, breakpoint widths --cem-color-blue-xl, --cem-typography-thickness-bold, --cem-dim-large
System Emotional palette (D0), action intent tokens, semantic shape/stroke/layering endpoints --cem-palette-trust, --cem-action-primary-hover-background, --cem-bend-control
Component Controls geometry (D2c), action border-radius, role typography --cem-control-height, --cem-action-border-radius, --cem-typography-button-font-size

packages/cem-theme/src/lib/tokens/cem-m3-parity.md already documents the role-by-role mapping. The DTCG export will (optionally, behind a flag) emit an M3-shaped alias group so consumers expecting M3 names get a drop-in adapter.

Important framing: M3, Angular Material, MUI, and other external systems are adapter layers, not replacements for CEM tokens. CEM markdown specs remain canonical; CEM CSS custom properties remain the stable web-facing API. Adapter sets (M3-shaped, etc.) are derived outputs, gated behind opt-in flags, and never become the source of truth for any CEM token.


5. Token classification

Every CEM token belongs to one of three buckets. This drives what gets exported where:

Bucket Examples DTCG JSON iOS/Android SCSS/JS Figma Notes
Cross-platform visual Branded colors, resolved palette/action colors, spacing, typography sizes/weights, timing, shape radii, controls geometry ✓ (resolved) Recipes resolve per theme when inputs are portable
Web-only forced-colors fallbacks, @container rules, unresolved system colors, ring composite recipes, CSS selectors and media-query behavior CSS only (existing pipeline)
Voice/audio data --cem-voice-*-{speech-rate,speech-pitch,speech-volume,ssml-emphasis,ink-thickness,icon-stroke-multiplier} ✓ (separate file) v2 / TTS adapters Separate cem.voice.tokens.json; not visual

Concrete translation gotchas

Token Value Translation strategy
--cem-color-visitedtext-30-black color-mix(in srgb, VisitedText 30%, black) System color VisitedText resolves only on web/native theme. Other platforms require an explicit mapping or skip report.
--cem-bend-round calc(var(--cem-shape-height, var(--cem-control-height)) / 2) Resolve to numeric px/rem at build time; mobile cannot evaluate nested calc() at token load.
--cem-palette-trust light-dark(var(--cem-color-blue-l), var(--cem-color-blue-d)) Map to two values per theme: light= and dark=. iOS asset catalogs and Android values-night/ consume them directly.
--cem-typography-data-font-variant-numeric tabular-nums lining-nums OpenType feature settings; Android Roboto may not support lining-nums. Document as recommended, not required, on Android.
--cem-voice-loud-speech-rate 0.94 Not a visual property; routes only to TTS adapter (AVSpeechSynthesizer / Android TextToSpeech). Excluded from visual exports.

5.1 Portability classification (per-value)

Bucket tagging (above) is per-token. Portability is a finer-grained per-value tag that drives how each token value is emitted to each target. Every emitted token carries a portability field in $extensions.cem:

Portability Meaning Emission strategy
literal Directly portable scalar value #ecf0ff, 4px, 200ms, 400 — emit as-is to every target
alias References another token; can be expressed as a DTCG reference DTCG-aware targets get "$value": "{cem.color.blue.l}"; non-aware targets get the resolved primitive
mode Different concrete values per theme mode Emit as DTCG modes (Figma) or per-resource-qualifier files (Android values-night/, iOS asset catalogs)
css-expression Requires CSS runtime evaluation that cannot be pre-resolved per theme (e.g., currentColor, @container math) Skip on non-web targets; document in report; CSS keeps the expression
platform-note Semantically useful but not directly representable on the target (e.g., system colors, SSML emphasis, complex font stacks) Skip on incompatible targets; emit metadata-only entry to the report

Rule: the exporter classifies every token at extraction time and propagates the classification through every output. Reports list every css-expression and platform-note skipped per target so consumers can see what the target cannot represent — no silent loss.

This taxonomy is orthogonal to the bucket: a "cross-platform visual" token may still be css-expression on non-web (e.g., --cem-bend-round is calc(...) resolvable per theme; on iOS it becomes literal after resolution; on a target that only consumes raw DTCG without resolution it stays css-expression).


6. Mode strategy

CEM has multiple orthogonal mode dimensions:

Mode dimension Values Cardinality
Theme light, dark, contrast-light, contrast-dark, native 5
Spacing density dense, normal, sparse 3
Coupling forgiving, balanced, compact 3
Shape style sharp, smooth, round 3

Cartesian product = 135 tuples — impractical to express as a single DTCG mode axis.

Strategy:

  1. Theme is the only exported mode axis. MVP mode values are emitted for light, dark, contrast-light, contrast-dark, and native. native values are Chromium-computed browser-reference values for CSS system colors, not guaranteed iOS/Android system equivalents. This maps to:
    • Figma import mode files, where every imported mode must contain the same token names and types
    • iOS asset catalogs (Any Appearance, Dark, and later high-contrast variants)
    • Android resource qualifiers (values/, values-night/, and later high-contrast qualifiers if supported)
  2. Spacing/coupling/shape are parallel groups. Each appears as its own token group with explicit prefix (e.g., spacing.dense.layout-stack-gap, spacing.normal.layout-stack-gap, spacing.sparse.layout-stack-gap). Consumers select at app level (toggle a CSS attribute on web; pick a Compose theme on Android; etc.).
  3. Forced-colors and reduced-motion are platform-native conventions. Not exported as tokens. iOS adapters read UITraitCollection.accessibilityContrast / UIAccessibility.isReduceMotionEnabled; Android reads Configuration.uiMode / Settings.Global.TRANSITION_ANIMATION_SCALE. Adapter docs (Phase F) describe the mapping per platform.

Counts: 9 parallel groups (3 + 3 + 3) instead of 135 tuples; the theme axis is the only true import/export mode dimension.

Mode-completeness rule. A token must have a concrete value for every mode it claims, or be excluded from all mode files for that target. Partial mode coverage (token X has a light value but no dark value) is a fail-hard validation error — Figma collections and iOS asset catalogs both reject mismatched mode sets. Tokens with incomplete mode coverage are emitted only into the canonical DTCG JSON with a platform-note portability tag and listed in the per-target report.


7. Recipe vs resolved-value split

CEM's action state tokens use recipes such as color-mix(in srgb, var(--cem-palette-trust) 70%, var(--cem-palette-trust-x)) for hover state. Two superpowers:

  1. Theme switching is automatic. If --cem-palette-trust changes per theme, hover follows.
  2. Adapter customization. A product adapter can override the base palette and the hover state recipe follows.

Non-web platforms can't evaluate color-mix() at runtime. Two paths:

  • Resolve at build time → lose recipe semantics. Each theme produces its own concrete RGBA. Theme switching becomes per-theme asset bundling (iOS asset catalogs, Android values-night/). Adapter customization at the recipe level is gone, but adapters can still override at the primitive (palette) level and rebuild.
  • Export the recipe in a portable form. DTCG composite tokens or string-encoded math, requiring custom consumer logic. Native platforms get nothing useful.

Decision (per portability tier):

  • literal values pass through unchanged.
  • alias values are preserved as DTCG references ("$value": "{cem.color.blue.l}") for consumers that understand them (Figma Variables, Style Dictionary, DTCG-aware tooling). Non-aware consumers get the resolved primitive via Style Dictionary's reference resolver. Aliases preserved over resolution whenever the target supports references — this keeps the semantic graph intact in design tools.
  • mode values resolve to per-theme primitives at build time and emit per Figma/iOS/Android mode conventions.
  • css-expression recipes resolve at build time to per-theme RGBA only if the recipe's inputs are themselves resolvable (e.g., color-mix(in srgb, var(--cem-palette-trust) 70%, var(--cem-palette-trust-x)) — both inputs are alias-resolvable). Otherwise the recipe is reported as CSS-only and skipped on non-web targets.
  • platform-note values never resolve; they're skipped with a report entry.

Web keeps recipes verbatim (existing behavior, no change). The CSS pipeline never sees the DTCG JSON layer.

Implementation: packages/cem-theme/scripts/export-tokens.mjs uses Playwright for computed-value capture when a target needs resolved values. It should serve a minimal fixture over HTTP that loads the generated dist/lib/css/*.css files, applies .cem-theme-* classes and data-cem-* attributes to <html>, then reads getComputedStyle(document.documentElement).getPropertyValue('--cem-...') for every token. This follows the repo's HTTP-served custom-element constraints and lets the browser resolve light-dark(), color-mix(), calc(), and var() chains from the same CSS output shipped to web consumers.

Do not add a separate color-math dependency for the first implementation. If Playwright/browser capture fails or proves insufficient, an explicit fallback such as culori may be added to resolve color-mix(in srgb, ...) per CSS Color Module Level 5 §2.1. That fallback must be treated as a separate implementation path and validated against browser-computed values.


8. Implementation phases

Phase A — Token-data extraction (foundational)

Add derive*Tokens() companions to derive*Manifest() in packages/cem-theme/scripts/manifest-utils.mjs. Where the existing manifest functions return { name, tier, categoryId }, the new ones return { name, valueRaw, tier, description, mode, category, sourceTable }.

Add packages/cem-theme/scripts/export-tokens.mjs as the orchestration script. Its first stage iterates compiled dist/lib/tokens/*.xhtml, calls each derivation, builds an in-memory intermediate model, and always writes dist/lib/tokens/cem.tokens.intermediate.json. This intermediate output is a debug artifact: useful for reviews, CI artifact inspection, and exporter debugging, but not a public package contract.

Output shape (excerpt):

{
  "version": "0.0.7",
  "generatedAt": "2026-04-29T10:00:00Z",
  "specs": {
    "cem-colors": {
      "branded": [
        { "name": "--cem-color-blue-xl", "valueRaw": "#ecf0ff", "tier": "required",
          "category": "branded", "sourceTable": "cem-color-hue-variant" }
      ],
      "palette": [...],
      "action": [...],
      "zebra": [...]
    },
    "cem-shape": {...},
    ...
  }
}

cem.tokens.intermediate.json structure:

Field Meaning
$extensions.cem.generated Generated provenance: package version, timestamp, command, source specs, and debug marker.
specs Map of source spec names such as cem-colors, cem-shape, and cem-timing.
specs[*].tokens or grouped arrays Extracted token rows before browser resolution. Each token includes source table id and row data.
token name Original CSS custom property name.
token valueRaw Raw value from the source table or derived table formula before CSS/browser resolution.
token row Source table cells retained for debugging column extraction and derivation bugs.

Consumers must not import cem.tokens.intermediate.json; its shape may change whenever extractor internals change.

Phase B — Theme-aware value resolution

The same export-tokens.mjs script resolves values that non-web targets cannot evaluate. Because resolution needs the generated CSS custom properties, build:tokens depends on build:css, not just build:docs.

For computed capture:

  1. Launch headless Chromium with Playwright.
  2. Serve a minimal local HTML fixture over HTTP. The fixture links or injects the generated dist/lib/css/*.css files.
  3. For each supported theme class (light, dark, contrast-light, contrast-dark, native):
    • Set the relevant .cem-theme-* class on <html>.
    • Set data-cem-spacing, data-cem-coupling, and data-cem-shape only for the parallel-group tokens being resolved.
    • Read getComputedStyle(document.documentElement).getPropertyValue(token.name).
    • Capture the resolved RGBA, numeric, dimension, or string value.
    • Label native values as Chromium-computed browser-reference values in metadata and reports.
  4. Keep both valueRaw and valueByMode in memory, and always write dist/lib/tokens/cem.tokens.resolved.json as a debug artifact.

Output shape (excerpt):

{
    "--cem-palette-trust": {
        "valueRaw": "light-dark(var(--cem-color-blue-l), var(--cem-color-blue-d))",
        "valueByTheme": {
            "light": "rgb(33, 87, 178)",
            "dark": "rgb(122, 158, 220)",
            "contrast-light": "rgb(0, 0, 102)",
            "contrast-dark": "rgb(180, 200, 240)",
            "native": "rgb(0, 95, 184)"
        }
    }
}

cem.tokens.resolved.json structure:

Field Meaning
$extensions.cem.generated Generated provenance with a debug marker.
token key or token record Original CSS custom property identity.
valueRaw Raw recipe or scalar value before resolution.
valueByMode Browser-computed values for light, dark, contrast-light, contrast-dark, and native.
resolution Metadata for how the value was captured, including theme class, data-cem-* attributes, and Chromium/native note.
issues Per-token resolution warnings such as empty computed value, unsupported system color, or unresolved expression.

Consumers must not import cem.tokens.resolved.json; use cem.tokens.json, Figma files, or platform outputs instead.

Spacing/coupling/shape mode resolution happens analogously by setting data-cem-spacing, data-cem-coupling, data-cem-shape attributes on <html> and re-reading the affected tokens. Each becomes a parallel token group in the DTCG output.

Phase C — DTCG JSON emission

The final stage of export-tokens.mjs transforms the extracted and resolved model to the DTCG-compatible format.

  • Tier-aware filtering (default: required + recommended; --with-optional, --with-adapter, --with-deprecated flags add tiers).
  • Theme mode values in $extensions.cem.modes, plus separate Figma mode files where needed.
  • Spacing/coupling/shape as parallel groups.
  • Web-only categories filtered out (forced-colors fallbacks, ring recipes, system color references that don't resolve outside web).
  • Aliases preserved ("$value": "{cem.color.blue.l}") when source token references another token; resolution happens only when the consumer can't follow references.
  • Every emitted token carries $extensions.cem metadata pointing back to its markdown source — this is the traceability contract that lets reviewers verify a derived value matches the spec.

$extensions.cem shape (every token):

{
    "cem": {
        "palette": {
            "comfort": {
                "$type": "color",
                "$value": "{cem.color.blue.l}",
                "$description": "Primary comfort surface color.",
                "$extensions": {
                    "cem": {
                        "cssName": "--cem-palette-comfort",
                        "spec": "cem-colors",
                        "sourceTable": "cem-palette-emotion-shift",
                        "tier": "required",
                        "category": "d0-palette",
                        "rawValue": "light-dark(var(--cem-color-blue-l), var(--cem-color-blue-d))",
                        "portability": "alias",
                        "modes": {
                            "light": "rgb(33, 87, 178)",
                            "dark": "rgb(122, 158, 220)"
                        }
                    }
                }
            }
        }
    }
}

$extensions.cem fields:

Field Purpose
cssName Original CSS custom property name (--cem-...) — preserved across all derived outputs
spec Source spec name (e.g., cem-colors) for back-link to markdown
sourceTable h6 id of the source table in the spec
tier required / recommended / optional / adapter / deprecated
category Internal categorization (e.g., d0-palette, d2c-controls) for filtering and grouping
rawValue Original CSS value verbatim (recipe with var()/color-mix()/calc() intact)
portability literal / alias / mode / css-expression / platform-note (see §5.1)
modes Resolved per-theme values when portability is mode or alias with theme variance

Outputs:

  • dist/lib/tokens/cem.tokens.intermediate.json — debug-only extraction artifact; not a public contract.
  • dist/lib/tokens/cem.tokens.resolved.json — debug-only browser-resolution artifact; not a public contract.
  • dist/lib/tokens/cem.tokens.json — canonical DTCG JSON, visual tokens (cross-platform bucket).
  • dist/lib/tokens/cem.voice.tokens.json — voice/audio bucket, separate file (for TTS adapters).
  • dist/lib/tokens/cem.tokens.report.md — human-readable report listing every token, its portability, and what was skipped per target and why. First-class output, not optional.
  • dist/lib/tokens/cem.tokens.report.json — machine-readable equivalent for CI assertions.

Generated provenance. Text/code outputs include generated-file headers. JSON outputs cannot use comments, so they carry provenance in top-level $extensions.cem.generated metadata: package version, generation timestamp, source spec list, and build command. No generated artifact may ship without provenance.

Phase D — Style Dictionary fan-out

Add packages/cem-theme/style-dictionary.config.mjs and an Nx target build:token-platforms that depends on build:tokens. Keep build:css independent so the existing web output can ship even if native exports remain experimental.

Custom transforms required:

  • cem/size/layout-to-pt (iOS): spacing, shape, control, and layout dimensions → points.
  • cem/size/type-to-pt (iOS): typography sizes → points; no automatic dynamic type scaling in v1.
  • cem/size/layout-to-dp (Android): spacing, shape, control, and layout dimensions → dp.
  • cem/size/type-to-sp (Android): typography sizes and text line-height → sp.
  • cem/number/unitless: font weights, opacity, z-index/layering, and other numeric values stay unitless.
  • cem/category/web-only-filter: drop web-only categories before emission.
  • cem/mode/expand-themes: expand $extensions.cem.modes to per-platform conventions (asset catalog hints for iOS; values-night/ for Android).

Outputs:

dist/lib/token-platforms/
├── ios/
│   ├── CEMTokens.swift                    (struct CEMTokens { static let colorPaletteTrust = UIColor(...) })
│   ├── CEMTokens.xcassets-hints.json      (asset-catalog scaffolding)
│   └── ios-report.md                      (skipped tokens + reasons)
├── android/
│   ├── values/cem-tokens.xml              (light theme)
│   ├── values-night/cem-tokens.xml        (dark theme)
│   ├── values-night-hcc/cem-tokens.xml    (contrast-dark)
│   ├── compose/CEMTokens.kt               (val cemColorPaletteTrust = Color(...))
│   └── android-report.md                  (skipped tokens + reasons)
├── js/
│   ├── cem-tokens.ts                      (typed exports + metadata)
│   └── js-report.md
├── json/
│   ├── cem-tokens.json                    (resolved-per-theme, flat)
│   └── json-report.md
└── scss/
    ├── cem-tokens.scss                    ($cem-color-palette-trust: ...;)
    └── scss-report.md

Per-platform report contract. Every platform output ships a *-report.md documenting:

  1. Token coverage: how many of the manifest's required/recommended tokens emitted to this target.
  2. Skipped tokens with reason: css-expression unresolvable on iOS, system color VisitedText has no native equivalent on Android, voice token excluded from visual export, etc.
  3. Platform-specific transformation choices applied (e.g., layout dimension → dp, typography size → sp, color resolved via Playwright in light theme).
  4. Validation summary: pass/fail counts for each fail-hard rule (see §11).

TypeScript metadata contract. The JS/TS output is for tooling, docs, autocomplete, and tests. It is not a replacement for CSS runtime styling in browsers. At minimum it should expose:

export type CemTokenName = '--cem-palette-comfort' | '--cem-action-primary-default-background';

export interface CemTokenMeta {
    name: CemTokenName;
    type: 'color' | 'dimension' | 'number' | 'duration' | 'fontFamily' | 'string';
    tier: 'required' | 'recommended' | 'optional' | 'adapter' | 'deprecated';
    spec: string;
    sourceTable: string;
    portability: 'literal' | 'alias' | 'mode' | 'css-expression' | 'platform-note';
}

Phase E — Figma integration

The MVP path is native Figma Variables in the CEM UI Kit. All Figma workflows are read-only from the generated artifacts; write-back is deferred until governance is designed.

  1. Native Figma library variables (MVP).

    • Refresh one CEM Tokens collection in the CEM UI Kit from generated token JSON.
    • Keep five modes in the collection: Light, Dark, Contrast Light, Contrast Dark, and Native.
    • Tokens appear as Figma Variables organized by slash-delimited groups.
    • Write-back stays disabled until a governance process exists for converting designer edits into markdown spec changes.
  2. Manual Figma Variables import (developer fallback).

    • Designers or design-ops drag-drop generated DTCG JSON files into the Variables panel if scripted refresh is not available.
    • Lower operational risk than sync; no write-back path.
    • Keep this documented as the fallback if file permissions, automation, or mode behavior blocks adoption.
  3. Figma REST API sync (post-v1 only).

    • Do not include in MVP or default local builds.
    • Start as a manual script before considering CI.
    • Require explicit file id configuration, scoped write token, dry-run/report mode, generated diff artifacts, and rollback instructions.
    • CI integration, if added later, must run only from protected branch/release workflows with required approval.

Figma import contract

Figma import has stricter requirements than canonical DTCG JSON. Generate separate Figma files and validate them before native library refresh or direct manual import:

  • Import one file per mode (cem-light.tokens.json, cem-dark.tokens.json, cem-contrast-light.tokens.json, cem-contrast-dark.tokens.json, cem-native.tokens.json). Figma creates or updates variables only when a token is present in every imported mode file and has the same $type in each file.
  • Emit only supported value shapes: sRGB/HSL colors, dimension values in px, duration values in s, single-string fontFamily, numbers, booleans encoded through com.figma.type, and strings.
  • Normalize names exactly as Figma does: nested DTCG groups become slash-separated names. Duplicate normalized names must fail before import because Figma ignores duplicates after the first one.
  • Keep aliases inside the same collection where possible. Cross-collection aliases require Figma-specific com.figma.aliasData metadata and should be deferred until the one-collection flow is proven.
  • Exclude unsupported or incomplete-mode tokens from all Figma mode files and list them in cem-figma-report.md.

examples/figma/README.md will document the native Figma library MVP path plus direct-import and split-collection prompts.

Developer prompts for Figma variants

Use these prompts when intentionally changing the Figma workflow. They preserve the same source-of-truth rule: markdown specs remain canonical, and Figma remains read-only.

Prompt: refresh native Figma Variables

Update the CEM token export Figma workflow to refresh native Figma Variables in the CEM UI Kit.
Keep one CEM Tokens collection. Use the generated files in dist/lib/tokens/figma/ as the only Figma input:
cem-light.tokens.json, cem-dark.tokens.json, cem-contrast-light.tokens.json, cem-contrast-dark.tokens.json,
and cem-native.tokens.json. Preserve read-only governance: Figma changes must become markdown spec edits, not
write-backs. Update docs/todo.md, packages/cem-theme/docs/token-export.md, and examples/figma/README.md.

Prompt: split Figma output by token dimension

Update the CEM token export Figma workflow to split the single CEM collection into dimension-specific collections
only if Figma collection limits or designer navigation justify it. Proposed collections: CEM Color, CEM Dimension,
CEM Typography, CEM Motion, and CEM Platform Notes. Keep markdown specs as source of truth, keep Figma read-only,
and document cross-collection alias handling. If aliases cannot be preserved safely across collections, duplicate
only resolved values and list the loss of alias semantics in cem-figma-report.md. Update docs/todo.md,
packages/cem-theme/docs/token-export.md, and examples/figma/README.md.

Phase F — Adapter examples

Reference applications proving end-to-end consumption:

  • examples/ios/CEMTokensExample/ — minimal SwiftUI app with a button + card using only CEM tokens.
  • examples/android/cem-tokens-example/ — minimal Compose app, parallel.
  • examples/web/import-tokens.ts — TypeScript consumption sample with type-safe imports.
  • examples/figma/CEMTokens.fig — sample Figma file (or screenshots) showing token application.

Phase G — Documentation cross-links

  • Update packages/cem-theme/docs/docs-generation.md to reference the export pipeline.
  • Update packages/cem-theme/src/lib/tokens/index.md with a "Platform consumption" section.
  • Add a "Token export contract" section to CLAUDE.md parallel to the existing "Token manifest contract" section.
  • Cross-reference from cem-m3-parity.md to the M3 alias adapter set.

9. Files to add

Path layout consolidates exports under dist/lib/tokens/ (canonical + Figma-shaped) and dist/lib/token-platforms/ (Style Dictionary outputs). This mirrors the existing dist/lib/css/ convention and keeps related artifacts co-located.

packages/cem-theme/
├── docs/
│   └── token-export.md                          (this file)
├── scripts/
│   ├── manifest-utils.mjs                       (extended: tokensFromTableWithValues)
│   ├── derive-tokens.mjs                        (new: full derive*Tokens() helpers)
│   ├── export-tokens.mjs                        (new: XHTML + CSS → DTCG JSON + Figma + reports)
│   ├── build-token-platforms.mjs                (new: Style Dictionary driver)
│   └── validate-platforms.mjs                   (new: post-emit coverage + name validation)
├── style-dictionary.config.mjs                  (new)
└── dist/lib/                                    (generated)
    ├── tokens/
    │   ├── cem-colors.xhtml                     (existing)
    │   ├── ...other spec xhtml...               (existing)
    │   ├── cem.tokens.json                      (new: canonical DTCG)
    │   ├── cem.voice.tokens.json                (new: voice/TTS metadata, v2 adapters)
    │   ├── cem.tokens.report.md                 (new: human-readable portability report)
    │   ├── cem.tokens.report.json               (new: CI-readable equivalent)
    │   ├── cem.tokens.intermediate.json         (new: debug-only extraction artifact)
    │   ├── cem.tokens.resolved.json             (new: debug-only browser-resolution artifact)
    │   └── figma/
    │       ├── cem-light.tokens.json            (Figma mode file)
    │       ├── cem-dark.tokens.json             (Figma mode file)
    │       ├── cem-contrast-light.tokens.json   (Figma mode file)
    │       ├── cem-contrast-dark.tokens.json    (Figma mode file)
    │       ├── cem-native.tokens.json           (Figma mode file; Chromium native colors)
    │       └── cem-figma-report.md              (skipped tokens + Figma-specific notes)
    └── token-platforms/                         (Style Dictionary outputs)
        ├── ios/
        ├── android/
        ├── js/
        ├── json/
        └── scss/

examples/
├── ios/CEMTokensExample/
├── android/cem-tokens-example/
├── figma/README.md
└── web/import-tokens.ts

Rationale for path consolidation:

  • dist/lib/tokens/ already holds the compiled XHTML; adding canonical JSON, reports, and Figma files keeps all token artifacts in one tree.
  • dist/lib/token-platforms/ (new) clearly signals "Style Dictionary derived" outputs distinct from CSS or XHTML.
  • Single export-tokens.mjs script handles extraction, classification, CSS-backed resolution, DTCG emission, Figma split, and reports; build-token-platforms.mjs is the Style Dictionary driver. Two scripts, two Nx targets.

10. Reused infrastructure

The pipeline extends — does not replace — what already exists:

Existing utility Path New use
extractTable(), tokensFromTable() packages/cem-theme/scripts/manifest-utils.mjs Add tokensFromTableWithValues() capturing value cells, not just names + tier
Validator pattern packages/cem-theme/scripts/validate-manifest.mjs Mirror as validate-platforms.mjs for non-CSS outputs
Playwright capture/debug scripts tools/scripts/capture-xpath-text.mjs, debug-cem.mjs Model for CSS-backed computed-value capture in export-tokens.mjs
Markdown → XHTML compilation tools/scripts/compile-markdown.mjs Unchanged; export pipeline reads its output
Nx target wiring packages/cem-theme/project.json Add build:tokens and build:token-platforms targets parallel to build:css

Nx target shape

{
    "build:tokens": {
        "dependsOn": ["build:css"],
        "executor": "nx:run-commands",
        "options": {
            "command": "node scripts/export-tokens.mjs",
            "cwd": "packages/cem-theme"
        },
        "outputs": [
            "{projectRoot}/dist/lib/tokens/cem.tokens.json",
            "{projectRoot}/dist/lib/tokens/cem.voice.tokens.json",
            "{projectRoot}/dist/lib/tokens/cem.tokens.report.md",
            "{projectRoot}/dist/lib/tokens/cem.tokens.report.json",
            "{projectRoot}/dist/lib/tokens/cem.tokens.intermediate.json",
            "{projectRoot}/dist/lib/tokens/cem.tokens.resolved.json",
            "{projectRoot}/dist/lib/tokens/figma"
        ]
    },
    "build:token-platforms": {
        "dependsOn": ["build:tokens"],
        "executor": "nx:run-commands",
        "options": {
            "command": "node scripts/build-token-platforms.mjs",
            "cwd": "packages/cem-theme"
        },
        "outputs": ["{projectRoot}/dist/lib/token-platforms"]
    }
}

build:css does not depend on build:tokens — the CSS pipeline ships independently. build:tokens depends on build:css because resolved non-web values are captured from generated CSS. build can add build:tokens after the canonical JSON/report outputs are stable, and add build:token-platforms only after native outputs leave the experimental phase.


11. Verification plan

11.1 Validation contract — fail hard

The exporter must exit non-zero and fail the build on any of these:

  • A token marked required or recommended in any spec's manifest is missing from cem.tokens.json.
  • Two tokens collide on canonical DTCG path (e.g., both --cem-foo-bar and --cem-foo--bar normalize to cem.foo.bar).
  • Two tokens collide on Figma slash-normalized name within one collection (e.g., --cem-action-primary-default-background and --cem-action-primary-default--background both normalize to action/primary/default/background).
  • Same token name has different $type across mode files (e.g., color in light, dimension in dark).
  • cem.tokens.json fails W3C DTCG schema validation.
  • A platform output references a token that should be filtered out for that platform (e.g., voice token in iOS visual export).
  • A generated output is missing the standard generated provenance header or JSON metadata.
  • Mode-completeness violation: a token has a value for one mode but not all modes it claims (see §6).

11.2 Validation contract — warn and report

The exporter logs a warning and includes the token in the per-target report (but does not fail):

  • optional-tier token has no portable value for a target — listed as "skipped optional".
  • css-expression portability that cannot resolve outside CSS — listed with the original recipe.
  • System color value (Highlight, CanvasText, etc.) that has no native equivalent — listed under platform-note.
  • deprecated tokens excluded from a target — listed with deprecation note.
  • adapter-tier tokens excluded from default outputs — listed under "opt-in via --with-adapter".

11.3 Per-phase verification

Phase Verification
A export-tokens.mjs produces JSON with every manifest-derived token name eligible for export; manifest validator confirms parity with CSS output.
B 5-token spot check per theme matches manual getComputedStyle() capture from tools/scripts/debug-cem.mjs.
C DTCG JSON validates against W3C DTCG schema (@design-tokens/parser or tokens-json-validator). Figma Variables imports without error. Report file lists every skipped token with reason.
D Generated Swift compiles with Xcode 15+; Kotlin with Gradle 8+; TypeScript passes tsc --noEmit; SCSS compiles with dart-sass. Each per-platform report shows zero fail-hard violations.
E Native Figma library workflow loads the generated JSON into one CEM Tokens collection without write-back; direct file import remains documented as a fallback.
F Sample iOS and Android apps render a button + card using only CEM tokens; visual parity with web reference (manual screenshot diff).
G yarn nx affected -t lint test build typecheck green; verify:phase13 green.

11.4 End-to-end smoke (after Phases A–G land)

  1. Change a value in cem-colors.md (e.g., --cem-color-blue-xl: #ecf0ff#e0e8ff).
  2. Run yarn build.
  3. Verify the new value flows through:
    • dist/lib/css/cem-colors.css (existing CSS pipeline)
    • dist/lib/tokens/cem.tokens.json
    • dist/lib/token-platforms/ios/CEMTokens.swift
    • dist/lib/token-platforms/android/values/cem-tokens.xml
    • dist/lib/token-platforms/js/cem-tokens.ts
  4. Import or refresh the Figma test collection; verify the new value appears in the Variables panel.
  5. Diff the per-target report files against the previous build — only the changed token should appear in the diff, with no spurious changes to other tokens' portability or mode classifications.

12. Decision log

All decisions needed before MVP extraction/emission are settled. Items marked "deferred" are intentionally post-MVP or require implementation evidence before final commitment.

# Decision Status Decision Rationale
1 Generated output paths Settled Use one MVP token artifact tree under dist/lib/tokens/, including canonical JSON, TS metadata, reports, Figma files, and debug artifacts. Keeps token artifacts co-located with compiled token XHTML and avoids making canonical JSON look like a platform-specific output.
2 Build dependency shape Settled build:tokens depends on build:css; build:css remains independent. Token export resolves non-web values from the exact generated CSS shipped to web consumers.
3 DTCG validation strategy Settled Add a DTCG validator dependency in the MVP path. External validation gives confidence before Figma and platform exporters consume generated JSON.
4 Theme mode export Settled Export light, dark, contrast-light, contrast-dark, and native; native uses Chromium-computed browser-reference system colors. Represents the full CEM theme mode surface while making clear that native colors are browser-reference values, not iOS/Android system equivalents.
5 Figma MVP workflow Settled Native Figma Variables in one CEM Tokens collection; keep direct file import and split-collection prompts for developers. Gives designers the preferred workflow while preserving read-only governance and fallback paths.
6 Debug artifacts Settled Always emit cem.tokens.intermediate.json and cem.tokens.resolved.json under dist/lib/tokens/; mark as debug-only/non-contract. Useful for CI review and exporter debugging without making these files public package APIs.
7 Style Dictionary version Settled Use latest compatible major at implementation time, pinned by lockfile; prefer v5+ if compatible, otherwise v4+. Avoids stale doc-pinned versions while still requiring reproducible installs.
8 Native unit mapping Settled Android layout/spacing/shape/control → dp; Android typography → sp; iOS dimensions and typography → points; numeric values stay unitless. Global px→dp creates accessibility issues; Android typography must scale with system font size while layout dimensions should not.
9 Color-resolution fallback Settled Use browser capture only unless it fails or proves insufficient; add culori only as a validated fallback. Browser capture validates against shipped CSS behavior; a JS color library adds a second resolution engine.
10 Figma REST API sync Deferred Defer to post-v1; start with a manual script before CI. Future CI requires explicit file id config, scoped write token, dry-run/report artifacts, and approval. API sync requires file-id config, write permissions, CI secrets, and rollback planning; it is unnecessary for proving generated artifacts.
11 Voice token export scope Deferred Emit metadata placeholder in v1; defer full TTS adapter support to v2. No current consumer apps with TTS adapters; metadata-only keeps the taxonomy visible without overbuilding.
12 Adapter and deprecated tier export Settled Gate behind --with-adapter and --with-deprecated. Matches the existing CSS pipeline contract.
13 DTCG mode encoding Settled Use canonical $extensions.cem.modes; Figma gets one generated file per mode. Multi-mode DTCG support is tool-specific, so CEM keeps mode data namespaced and emits Figma-friendly files.
14 Output stability commitment Deferred Stabilize after Phase D ships once. Token names in CSS are stable, but DTCG/platform output names need one full release cycle before promising compatibility.
15 color-mix() resolution policy Settled Resolve at export time per theme; preserve alias-form DTCG for DTCG-aware consumers. Native platforms get concrete RGBA while DTCG-aware tools retain semantic references.
16 Native Figma library support tier Settled MVP bridge, not source of truth. The CEM UI Kit is the preferred designer workflow, but markdown remains canonical and direct import remains fallback.
17 Figma collections Settled One CEM collection with groups by dimension; split only if collection limits or navigation friction arise. One collection simplifies references; split-collection prompts document the future variant.
18 iOS dynamic type Settled Do not auto-scale in v1. CEM typography sizes are nominal; product apps decide whether to apply dynamic type.
19 Android color modes Settled Resource qualifiers (values-night/) for v1; Compose color schemes optional. Resource qualifiers are platform-native and work without app-level Compose wiring.
20 Package export map for generated JSON/TS Deferred Expose generated JSON/TS via package.json exports after generated paths stabilize. Consumers should not depend on deep paths, but exports should wait until generated paths are proven.

13. Risks and mitigations

Risk Impact Mitigation
Style Dictionary's color-mix() support is incomplete Cross-platform colors may emit as raw color-mix(...) strings that don't compile Resolve recipes before Style Dictionary sees them; Style Dictionary receives concrete values plus CEM metadata
Figma Variables import interprets DTCG modes differently Designers see different mode behavior than expected Validate the CEM UI Kit native variables first because it is the MVP path; keep direct file import as fallback and compare mode behavior in the report
Multi-mode dimensionality explosion DTCG output becomes huge and hard to navigate Theme as the only import/export mode axis; spacing/coupling/shape as parallel groups (9 vs 135)
Voice tokens leaking into visual exports Designers see speech-rate as a "color" or "size" in Figma Hard-filter via category prefix; separate cem.voice.tokens.json; Style Dictionary's cem/category/web-only-filter excludes voice from visual platform outputs
Native platforms can't represent all theme modes iOS and Android may not have a clean "contrast-light" or "native" equivalent Canonical JSON/Figma include all theme modes; platform-native exports may map or exclude modes per target report. Native mode is labeled Chromium-reference.
DTCG draft format changes before W3C ratification Output JSON shape may need migration Pin DTCG schema version in cem.tokens.json $schema; add migration script if the spec revision requires a new wire shape

14. Out of scope (explicit non-goals for v1)

  • Two-way Figma → md sync. Governance for designer-authored changes is not designed yet. Any GitHub write-back capability stays disabled.
  • CI auto-regeneration on token change. Build-on-merge hooks are a future enhancement; v1 ships with manual yarn build.
  • Migration of existing component libraries (e.g., @epa-wg/cem-components) to consume new exports. Each adapter team owns the migration on their own timeline.
  • Locale-aware token variants. CEM voice tokens are language-agnostic; per-locale TTS configuration is a product-level concern, not a token-level one.
  • Custom Figma plugin for CEM. The native Figma library workflow covers the Figma path. A CEM-specific plugin is an option only if the native workflow proves insufficient — not in v1.
  • Dynamic theme synthesis. Themes are statically resolved at build time; runtime theme generation (e.g., from a brand color picker) is a separate feature.

15. Recommended MVP

The full Phase A–G arc is the destination. The MVP is the smallest slice that delivers measurable value:

  1. Canonical DTCG JSONdist/lib/tokens/cem.tokens.json with $extensions.cem traceability and per-theme resolved modes (Phases A + B + C, scoped to required + recommended tiers).
  2. Portability reportcem.tokens.report.md listing every token, its portability classification, and what the canonical export resolved vs. preserved as alias vs. skipped.
  3. Figma file exports — one generated file per theme mode for native Figma library refresh, with direct Figma Variables file import documented as a fallback. Cover only literal-portability and alias-portability tokens. native uses Chromium-computed browser-reference colors. css-expression and platform-note tokens listed in cem-figma-report.md.
  4. TypeScript metadatacem-tokens.ts typed token names + metadata for consumer tooling, autocomplete in IDEs, and docs generation. Not a runtime CSS replacement.
  5. Defer native — Android XML/Compose and iOS Swift exports stay behind a separate build:token-platforms target until unit-mapping policy (decisions §9, §14, §15) and mode coverage are validated.

Why this slice:

  • Designers and developers get usable artifacts on day one (DTCG JSON for tooling, Figma files for design).
  • The hardest cross-platform questions (px→dp, dynamic type, asset catalogs) are deferred until evidence accrues.
  • Native exports are the most expensive to maintain incorrectly — premature shipping creates support load without commensurate adoption.
  • Reports surface every gap so the next milestone can prioritize what's missing.

After the MVP ships and stabilizes, native platform exports follow under the experimental stability label.


16. Stability and versioning

Generated artifacts have different maturity. The doc commits to explicit stability labels so consumers know what they can rely on:

Tier Includes Stability commitment
Stable Markdown specs (packages/cem-theme/src/lib/tokens/*.md), CSS custom properties (dist/lib/css/*.css), required token names Backwards compatible; breaking changes only with major version bump and migration notes
Beta Canonical DTCG JSON (cem.tokens.json), TypeScript metadata, portability reports Schema may evolve in minor releases; renames discouraged; consumers should pin minor versions
Experimental Figma exports, Android XML/Compose, iOS Swift, asset-catalog hints, direct Figma REST API sync May change shape, naming, or structure freely between minor releases; consumers regenerate per release

Breaking changes (require major version bump):

  • Removing or renaming a token in the Stable tier.
  • Changing a token's $type in canonical DTCG JSON.
  • Changing Figma slash-normalized names after a release publishes.
  • Changing px→dp / px→pt / px→sp default mapping policy per category.
  • Moving a token from required to optional in a way that removes it from default outputs.
  • Removing a portability classification value (literal/alias/mode/css-expression/platform-note).

Non-breaking changes (minor bump):

  • Adding new tokens, specs, or modes.
  • Adding new platform outputs (e.g., a new dist/lib/token-platforms/dart/ for Flutter).
  • Adding fields to $extensions.cem metadata.
  • Tightening portability classification (e.g., a token previously css-expression becomes mode once resolution is implemented for it).

Generated provenance carries the version and source-spec timestamp so a downstream consumer can detect when their cached copy diverges from the upstream package version.


References