# @cdx-ui/styles

![coverage](../../.github/badges/coverage-styles.svg)

Design tokens and theme infrastructure for the Forge Design System. Tokens flow from Figma Variables through a Style Dictionary pipeline into CSS custom properties, Tailwind v4 theme variables, and runtime theming artifacts — supporting three presets (Poise, Prestige, Pulse), light/dark modes, cross-platform fonts, and FI white-label overrides.

This package is **platform-neutral**: it ships no `expo-*`, bundler, or runtime platform dependency. Native font _loading_ lives in [`@cdx-ui/native/fonts`](../native/README.md) (`useForgeFonts`); this package owns the `--font-*` token values those fonts resolve against.

## Installation

```bash
pnpm add @cdx-ui/styles
```

## Usage

### CSS imports (Tailwind v4 + Uniwind)

Import `theme.css` and `utilities.css` in your global stylesheet, after Tailwind and Uniwind:

```css
@import 'tailwindcss';
@import 'uniwind';

@import '@cdx-ui/styles/theme.css';
@import '@cdx-ui/styles/utilities.css';
```

`theme.css` declares all token variables — primitives in `@theme static`, mode-dependent semantics in `@variant light/dark`, and platform fonts in `@variant ios/android/web`. `utilities.css` provides composite typography utilities (`heading-xl` through `body-xs`, plus `label-sm` and `label-xs`).

Components then use standard Tailwind utilities backed by theme variables:

```tsx
<View className="bg-surface-background p-4 rounded-lg">
  <Text className="text-content-primary heading-md">Welcome</Text>
  <Text className="text-content-secondary body-md">Get started below.</Text>
</View>
```

Web apps that toggle class-based light/dark modes must also load `web.css` as a separate JavaScript import after their Tailwind CSS entry:

```typescript
import './global.css';
import '@cdx-ui/styles/web.css';
```

Do not `@import` `web.css` inside the Tailwind entry. It supplies unlayered `.light`/`.dark` rules that must follow Tailwind's unlayered `@theme static` output. See the [developer getting-started guide](../../apps/docs/content/docs/getting-started/developers.mdx) for the complete setup.

MUI 6+ CSS-variable consumers can load the generated semantic bridge. Apps using Candescent's
`@cds/theme` load the CDS-prefixed bridge instead:

```typescript
import '@cdx-ui/styles/mui.css';
// or: import '@cdx-ui/styles/cds.css';
```

The bridges are intentionally unlayered so their live Forge aliases win over MUI's generated
palette variables. MUI's Emotion output must therefore be placed in a named CSS layer. The
supported default is `StyledEngineProvider enableCssLayer`:

```tsx
import { StyledEngineProvider } from '@mui/material/styles';

createRoot(root).render(
  <StyledEngineProvider enableCssLayer>
    <App />
  </StyledEngineProvider>,
);
```

Without `enableCssLayer`, Emotion inserts later unlayered declarations with the same specificity
and can restore construction-time palette values. Consumers that cannot use layers must configure
an Emotion insertion point that places all Emotion styles before the bridge stylesheet; merely
importing `mui.css` or `cds.css` is not sufficient.

`toMuiThemeOptions` enables MUI native-color mode and uses
`data-mui-color-scheme` as its explicit color-scheme selector. Bridge scopes must carry both the
Forge appearance class and the target selector on the same element:

```tsx
<div className="light" data-mui-color-scheme="light">{/* plain MUI */}</div>
<div className="dark" data-mui-color-scheme="dark">{/* plain MUI */}</div>
<div className="light cds-light">{/* @cds/theme */}</div>
<div className="dark cds-dark">{/* @cds/theme */}</div>
```

Combining the classes is required for scoped, side-by-side themes: `.dark`/`.light` activates the
matching Forge semantic variables, while the MUI/CDS selector activates the bridge. The bridge also
supplies relative-color channel variables for non-native-color MUI and current `@cds/theme`
consumers, so alpha, outlined, selected, and hover states follow live FI overrides.

### CSS imports (non-Tailwind)

For consumers that do not use Tailwind, `vanilla.css` provides all variables as plain `:root` declarations with `@media (prefers-color-scheme: dark)` and class-based typography:

```css
@import '@cdx-ui/styles/vanilla.css';
```

### Preset JSON

All three preset theme objects are importable as JSON for runtime theming:

```typescript
import poise from '@cdx-ui/styles/presets/poise.json';
import prestige from '@cdx-ui/styles/presets/prestige.json';
import pulse from '@cdx-ui/styles/presets/pulse.json';
```

### Apply an FI theme override

An override stores FI choices rather than generated theme output:

- `inputs` contains generative choices such as brand colors and display fonts.
- `selections` contains package-owned design decisions such as Action colors or border radius.
- `overrides` contains exceptional direct token choices as `{ ref }` or `{ value }`.

For a payload fetched from storage, pass the untrusted value directly to `applyThemeOverride` at
application startup. It validates the payload, expands it for the current platform, evaluates
package rules, and writes through Uniwind only when the entire payload is valid.

```typescript
import { applyThemeOverride } from '@cdx-ui/styles';

const result = applyThemeOverride(await response.json());

if (!result.applied && result.reason === 'invalid_theme_override') {
  reportThemeDiagnostics(result.diagnostics);
}
```

Invalid payloads produce no writes. A metadata-only override is valid and selects the current
package definition of its `basePreset`; it may return `no_theme_changes` when no runtime variables
need to change.

For previews or non-Uniwind adapters, use the pure expansion API. It returns complete concrete
light/dark state in `resolved`, sparse adapter changes in `writes`, diagnostics, provenance, and
the runtime artifact revision.

```typescript
import { expandThemeOverride, MUI_SEMANTIC_MAPPINGS, toMuiThemeOptions } from '@cdx-ui/styles';

const result = expandThemeOverride(rawOverride, 'web');
if (result.valid) {
  const muiOptions = toMuiThemeOptions({
    expanded: result.expanded,
    mappings: MUI_SEMANTIC_MAPPINGS,
  });
}
```

Use `ThemeOverride` when constructing a trusted, typed payload in application code. Authoring tools
that need to inspect known fields while preserving additive unknown fields can call
`parseThemeOverride` explicitly before editing or reserializing the payload.

```typescript
import type { ThemeOverride } from '@cdx-ui/styles';
import {
  OVERRIDE_SCHEMA_VERSION,
  SUPPORTED_OVERRIDE_SCHEMA_VERSIONS,
  parseThemeOverride,
  overrideCatalogs,
} from '@cdx-ui/styles';
```

`overrideCatalogs` supplies the package-owned input and selection options for constrained authoring
UIs. `OVERRIDE_SCHEMA_VERSION` is the authored contract version, and
`SUPPORTED_OVERRIDE_SCHEMA_VERSIONS` lists payload versions accepted by the current parser. The
published list currently contains only the current version: no legacy migration transformer ships.
Recognizable legacy `light`/`dark` mode buckets remain rejected even if a future transition gate
recognizes their version.

The `@cdx-ui/styles/theming` subpath exposes the same override API with a platform-conditioned
`applyThemeOverride`:

```typescript
import { applyThemeOverride, expandThemeOverride } from '@cdx-ui/styles/theming';
```

Other TypeScript exports include theme object types and constants, preset/font data, palette
generation, override diagnostics and runtime data, and Uniwind/MUI adapter contracts.

**Font loading:**

Fonts are not loaded from this package. Native/Expo apps call `useForgeFonts` from [`@cdx-ui/native/fonts`](../native/README.md); the registered font names match the `--font-*` values this package emits into `theme.css`. See [Getting Started](../../apps/docs/content/docs/getting-started/developers.mdx) for usage.

### Runtime artifacts

The build produces runtime JSON artifacts importable from the package:

```typescript
import runtimeMap from '@cdx-ui/styles/runtime/token-to-css-var.json';
import overrideArtifacts from '@cdx-ui/styles/runtime/override-artifacts.json';
import prestigePatch from '@cdx-ui/styles/runtime/prestige-vs-default.json';
import pulsePatch from '@cdx-ui/styles/runtime/pulse-vs-default.json';
```

- **`runtime/token-to-css-var.json`** — Flat map of roughly 690 token dot-paths to CSS custom property names. Used internally by `applyThemeOverride`; importable for custom apply logic.
- **`runtime/override-artifacts.json`** — Concrete primitive values, target metadata, catalogs,
  recipes, warning/error rules, font mappings, managed targets, and deterministic revision.
- **`runtime/prestige-vs-default.json`** / **`runtime/pulse-vs-default.json`** — Sparse preset patches (diff from build default to target preset). Used internally by `applyThemeOverride` when `basePreset` differs from the build default.

## Token pipeline

The pipeline has two stages — **fetch** and **build** — both run from package scripts.

### Fetching tokens from Figma

Tokens are pulled from Figma Variables via the [REST API](https://www.figma.com/developers/api) (`GET /v1/files/:file_key/variables/local`). This requires an Enterprise plan and a personal access token with `file_variables:read` scope.

```bash
FIGMA_VARIABLES_TOKEN=your_token pnpm tokens:fetch
```

The script reads `figma.config.json` for the file key, preset list, and default FI mode, then assembles one [DTCG-compatible](https://www.designtokens.org/tr/2025.10/format/) theme object JSON per preset:

```
tokens/presets/
  poise.json        # Default build preset
  prestige.json
  pulse.json
  .manifest.json    # SHA-256 checksums (written by fetch)
```

`.manifest.json` records the SHA-256 hash of each preset JSON after fetch for diff detection and downstream tooling.

Each file is assembled from four Figma collection roles: **Primitives** (shared), **FI Primitives** (Candescent mode — brand/accent/base colors), **Semantics ({Preset})** (Light + Dark modes), and **Platform** (Web/iOS/Android fonts). Prestige and Pulse are Figma extended collections: inherited values are read through each extension mode's `parentModeId`, while personality-specific values come from `collection.variableOverrides`. Alias references are preserved as DTCG `"{path.to.token}"` syntax.

Useful fetch options:

```bash
pnpm tokens:fetch -- --dry-run
pnpm tokens:fetch -- --collection "Semantics (Poise)"
```

The automated update workflow is documented in [`docs/internal/distribution.md`](../../docs/internal/distribution.md).

#### Figma MCP alternative

If the [Figma MCP server](https://developers.figma.com/docs/figma-mcp-server/) is configured in Cursor, you can inspect variables interactively. Select a frame/layer and ask the agent for variable names and values. This is useful for ad-hoc inspection but does not replace the fetch script for full pipeline runs.

### Building tokens

Style Dictionary v5 reads the default preset JSON and produces all output artifacts:

```bash
pnpm tokens:build
```

To verify committed output and determinism, run:

```bash
pnpm tokens:check
```

The check builds twice, compares the first build with the committed CSS/runtime artifacts, and
then compares both builds byte-for-byte. It is enforced in PR CI, token-update automation, and
release.

**CSS outputs** (written to `css/`):

| File            | Contents                                                                                            |
| --------------- | --------------------------------------------------------------------------------------------------- |
| `theme.css`     | `@theme static` (primitives + semantic defaults), `@variant light/dark`, `@variant ios/android/web` |
| `mui.css`       | Forge semantic and relative-color channel variables mapped to MUI 6+                                |
| `cds.css`       | Forge semantic and relative-color channel variables mapped to `@cds/theme`                          |
| `utilities.css` | `@utility` blocks for composite typography (headings, body-xl through body-xs, label-sm, label-xs)  |
| `web.css`       | Unlayered `.light` / `.dark` overrides for class-based web mode switching                           |
| `vanilla.css`   | Non-Tailwind fallback — `:root` variables + `@media (prefers-color-scheme: dark)` + classes         |

**Runtime artifacts** (generated by `tokens:build`):

| Artifact             | File                               | Purpose                                                   |
| -------------------- | ---------------------------------- | --------------------------------------------------------- |
| **Runtime map**      | `runtime/token-to-css-var.json`    | Theme path → CSS custom property for adapter writes       |
| **Override program** | `runtime/override-artifacts.json`  | Primitive/type/catalog/recipe/rule/font data and revision |
| **Preset patches**   | `runtime/{preset}-vs-default.json` | Sparse diff from build default → each non-default preset  |

### Validating font tokens

Font registration names in `useForgeFonts` (owned by `@cdx-ui/native`) must exactly match the `--font-*` values emitted into `theme.css` — a mismatch causes silent fallback to platform default fonts with no error. The validation script catches this drift as a build/CI failure:

```bash
pnpm fonts:validate
```

It parses every `--font-*` declaration from `css/theme.css` (across `@theme static`, `@variant light/dark`, and `@variant ios/android/web`), reads the font map keys from `../native/src/fonts/useForgeFonts.ts` source, and verifies a matching key exists for every non-system value. System fonts (`SF Pro`, `SF Mono`, `Roboto`, `Roboto Mono`) are read from `figma.config.json` and excluded — they are platform built-ins resolved natively by React Native. `var()` aliases are skipped because they resolve to other variables that are themselves validated.

On success the script prints `Font validation: N CSS values checked, N matched, 0 mismatches.` and exits 0. On failure it lists each mismatch with its variable name, value, and CSS section, then exits non-zero. The script is wired into `.github/workflows/ci.yml` as a pre-build step so font-token drift fast-fails before the heavier build/lint/test stages.

## Theme architecture

### Presets

Three complete preset files ship — **Poise** (default), **Prestige**, and **Pulse**. Poise is compiled into CSS. Prestige and Pulse inherit Poise's semantic surface through their Figma extended collections and produce deterministic non-empty runtime patches for their reviewed radius and display-font personalities. All presets pass metadata-only and package-owned recipe round-trip expansion.

### Modes and platforms

Light/dark mode switching is handled by Uniwind `@variant light/dark` blocks in `theme.css`. Platform-specific fonts (SF Pro on iOS, Roboto on Android, Inter on web) use `@variant ios/android/web` blocks. Both are activated automatically by Uniwind at runtime.

### FI overrides

FI white-labelling persists explicit `inputs`, named recipe `selections`, and target-centric direct
`overrides`. The Theme API remains pass-through. The package parser rejects malformed known intent,
the pure expander resolves every reference concretely, and Uniwind/MUI consume the same expansion.
See [Override Structure](../../docs/internal/token-architecture/14-override-structure.md).

## Package structure

```
styles/
├── css/
│   ├── theme.css              # Auto-generated — all token variable declarations
│   ├── mui.css                # Auto-generated — Forge → MUI CSS-variable bridge
│   ├── cds.css                # Auto-generated — Forge → CDS CSS-variable bridge
│   ├── utilities.css          # Auto-generated — composite typography @utility blocks
│   ├── web.css                # Auto-generated — unlayered class-based mode overrides
│   └── vanilla.css            # Auto-generated — non-Tailwind fallback
├── runtime/
│   ├── token-to-css-var.json       # Build-generated — token path → CSS variable name map
│   ├── override-artifacts.json      # Build-generated — target runtime program and revision
│   ├── prestige-vs-default.json    # Build-generated — sparse preset patch (Poise → Prestige)
│   └── pulse-vs-default.json       # Build-generated — sparse preset patch (Poise → Pulse)
├── scripts/
│   ├── figma-fetch-variables.mjs   # Figma Variables REST API fetch
│   ├── build-tokens.mjs            # Style Dictionary build + artifact generation
│   ├── check-token-artifacts.mjs    # Rebuild + byte-determinism gate
│   ├── postbuild.mjs                # Strict ESM declaration/JSON import normalization
│   ├── check-package-conformance.mjs # Packed ESM/CJS/browser/Metro/export smoke
│   └── validate-font-tokens.mjs    # CI check: --font-* values match @cdx-ui/native useForgeFonts keys
├── src/
│   ├── public.ts              # Platform-neutral public exports shared by condition entries
│   ├── index.ts               # Root barrel: public + default/CJS applyThemeOverride
│   ├── index.browser.ts       # Root barrel: public + browser applyThemeOverride
│   ├── index.native.ts        # Root barrel: public + native applyThemeOverride
│   ├── theming.ts             # Alias of index.ts (`./theming` subpath)
│   ├── theming.browser.ts     # Alias of index.browser.ts
│   ├── theming.native.ts      # Alias of index.native.ts
│   ├── constants.ts           # Shared preset, mode, platform, schema, and protocol constants
│   ├── palette.ts             # Color scale generation (Leonardo contrast algorithm)
│   ├── utils/                 # Private common object/JSON and color helpers
│   ├── adapters/              # Uniwind startup/covering and MUI adapters
│   ├── applyThemeOverrideRuntime.ts # Target parse/expand/apply orchestration
│   ├── applyThemeOverride.ts  # Default/CJS optional-peer adapter
│   ├── override/              # Parser, expansion, catalogs, recipes, rules, and contracts
│   └── types.ts               # DTCG and public theme/override types
├── tokens/
│   └── presets/
│       ├── .manifest.json     # SHA-256 checksums (written by fetch)
│       ├── poise.json         # Poise theme object (build default)
│       ├── prestige.json      # Prestige theme object
│       └── pulse.json         # Pulse theme object
├── figma.config.json          # File key, preset list, default preset/FI mode
├── sd.config.ts               # Style Dictionary v5 configuration
├── package.json
├── tsconfig.json
└── tsconfig.build.json
```

`css/` and `runtime/` files are auto-generated — do not edit directly.

## Building

```bash
pnpm --filter @cdx-ui/styles build
```

TypeScript sources are compiled by `react-native-builder-bob` to `lib/` with CommonJS, strict ESM,
and declaration targets. The build packs the package and exercises ESM, CJS, browser/no-`require`,
Metro source-condition, JSON/CSS, optional-peer, workspace-range, and private-path conformance.

## Further reading

- [Token Architecture](../../docs/internal/token-architecture/README.md) — theme/pipeline decisions, CSS architecture, and target override contract

## License

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)

MIT © 2026 Digital First Holdings LLC. See [LICENSE](./LICENSE) for details.
