Theme Configuration Guide
Premise
In Aplica DS, every visual decision starts in configuration. Each brand is defined in a .config.mjs file — not in Figma, not in manual JSON. The engine reads this configuration and automatically generates all tokens for every combination of mode, surface, and dimension.
This guide covers how to create and configure brand themes and foundations using the @aplica/aplica-theme-engine package.
Overview: Themes vs Foundations
These two concepts have distinct responsibilities and complement each other:
| Concept | What it defines | Where it lives | What it generates |
|---|---|---|---|
| Theme | Visual identity: colors, typography, gradients | theme-engine/config/<name>.config.mjs | data/brand/<name>/ with decomposed palettes |
| Foundation | Aliases for semantic tokens | theme-engine/config/foundations/<name>.config.mjs | data/foundation/<name>/ with consumption tokens |
A theme can have multiple foundations, but each theme is linked to one foundation in the build. The foundation exposes tokens with simple names (foundation.bg.primary, foundation.spacing.medium) that point to the configured theme's semantic layer.
Theme Config Structure
Brand config files use defineThemeEngineConfig from the package:
// theme-engine/config/my-brand.config.mjs
import { defineThemeEngineConfig } from '@aplica/aplica-theme-engine/config';
export default defineThemeEngineConfig({
// ─────────── REQUIRED ───────────
name: 'my_brand', // Theme ID; defines the folder name in data/brand/
colors: { // All hex colors used by the theme
brand_primary: '#E7398A',
brand_secondary: '#38C2D0',
brand_tertiary: '#8F58BD',
action_primary: '#C40145',
action_secondary: '#1872A6',
action_link: '#FF0F80',
// ... feedback and product (see section below)
},
mapping: { // Maps semantic concepts → color keys
brand: {
first: 'brand_primary',
second: 'brand_secondary',
third: 'brand_tertiary'
},
interface: {
function: {
primary: 'action_primary',
secondary: 'action_secondary',
link: 'action_link'
},
feedback: {
info_default: 'feedback_info',
info_secondary: 'feedback_info_dark',
success_default: 'feedback_success',
// ... warning, danger
}
},
product: {
promo_default: 'product_promo',
promo_secondary: 'product_promo_alt',
// ... cashback, premium
}
},
typography: { // Font families and weights
fontFamilies: {
main: 'Inter',
content: 'Inter',
display: 'Inter',
code: 'Fira Code'
},
weights: {
main: {
light: { normal: 300, italic: 300 },
regular: { normal: 400, italic: 400 },
semibold: { normal: 600, italic: 600 },
bold: { normal: 700, italic: 700 },
black: { normal: 900, italic: 900 }
}
// content, display, code follow the same structure
}
},
// ─────────── OPTIONAL ───────────
options: { /* see options section */ },
gradients: { /* see gradients section */ }
});
Colors and Semantic Mapping
The separation between colors and mapping is deliberate:
colors— a simple dictionary of free names to hex values. The name is internal and can be any descriptive string.mapping— connects those free names to the system's fixed semantic roles.
This separation allows multiple themes to share the same semantic roles with completely different colors, without duplicating the mapping logic.
Feedback Colors
Each feedback type has a default variant (softer, for backgrounds) and a secondary variant (more saturated, for borders and icons):
colors: {
// Feedback — soft for bg, saturated for border/icon
info_blue: '#CBF6ED', // default: soft
info_blue_sat: '#1872A6', // secondary: saturated
ok_green: '#D7F6CB',
ok_green_sat: '#86C46D',
warning_amber: '#FEE6C2',
warning_sat: '#FDB750',
error_red: '#F9C8C8',
error_sat: '#EE5A5A',
},
mapping: {
interface: {
feedback: {
info_default: 'info_blue',
info_secondary: 'info_blue_sat',
success_default: 'ok_green',
success_secondary: 'ok_green_sat',
warning_default: 'warning_amber',
warning_secondary: 'warning_sat',
danger_default: 'error_red',
danger_secondary: 'error_sat'
}
}
}
Product Colors
Same logic: default + secondary per item. The default schema includes promo, cashback, and premium:
colors: {
promo_color: '#6BC200',
promo_alt: '#D2FD9D',
cashback_color: '#FFBB00',
cashback_alt: '#FFF94F',
premium_color: '#B200AF',
premium_alt: '#EBC2DD'
},
mapping: {
product: {
promo_default: 'promo_color',
promo_secondary: 'promo_alt',
cashback_default: 'cashback_color',
cashback_secondary: 'cashback_alt',
premium_default: 'premium_color',
premium_secondary: 'premium_alt'
}
}
Remember the cost: Each product item generates dozens of tokens per layer per theme. Before adding a new item, check whether an existing feedback or brand token can solve the case. See 01-colors.md for the full rationale.
Configuration Options
The options key controls engine behavior for that theme:
Text strategies (txtOnStrategy)
| Value | Behavior |
|---|---|
'high-contrast' (default) | Always black or white — maximum contrast |
'brand-tint' | Uses the palette tone that passes WCAG — preserves brand color |
'custom-tint' | Fixed configured color, with fallback if WCAG fails |
options: {
txtOnStrategy: 'brand-tint',
// Only for 'custom-tint':
txtOnCustomTint: {
light: '#1a1a2e', // text on light surfaces
dark: '#f0f0ff', // text on dark surfaces
fallback: 'high-contrast' // fallback if the color fails WCAG
}
}
Other relevant options
| Property | Default | Description |
|---|---|---|
darkModeChroma | 0.85 | Saturation factor in dark mode (0.7 = softer, 1.0 = same as light) |
accessibilityLevel | 'AA' | Minimum WCAG level: 'AA' (4.5:1) or 'AAA' (7:1) |
acceptAALevelFallback | true | When targeting AAA, accept AA (4.5:1) if AAA cannot be achieved |
includePrimitives | false | Generates _primitive_theme.json with the raw decomposed palette (all 19 levels × all declared colors). Enable when your Figma file uses Figma Variables directly — not via Tokens Studio — and needs access to primitive palette values. Disabled by default since 3.6.3 because most workflows only need the semantic and foundation layers. |
uiTokens | false | Generates _ui.json with component-scoped UI tokens |
borderOffset.palette | 10 | Border distance from surface (scale 10–190) |
borderOffset.neutrals | 1 | Distance steps on the neutrals scale |
txtBaseColorLevel | workspace default | Palette level to start from for txt token lookup for this theme (overrides workspace generation.colorText.txtBaseColorLevel) |
Workspace Config: generation.colorText (since 3.6.0)
The txt token and readable-text generation are controlled at the workspace level in aplica-theme-engine.config.mjs, not per-theme. This ensures all themes share the same text contract shape:
import { defineThemeEngineConfig } from '@aplica/aplica-theme-engine/config';
export default defineThemeEngineConfig({
generation: {
colorText: {
generateTxt: true, // Enable the txt property (default: false for compatibility)
txtBaseColorLevel: 140, // Palette level for txt lookup (starts here, climbs if needed for WCAG)
fallbackBaseColorLevel: 160, // Secondary fallback level
textExposure: ['feedback'], // Which families get foundation.txt.* flat aliases
// Options: 'feedback', 'interfaceFunction', 'product'
}
},
paths: {
configDir: './theme-engine/config',
globalConfigDir: './theme-engine/config/global',
foundationsDir: './theme-engine/config/foundations',
dataDir: './data',
distDir: './dist'
}
});
textExposure values
| Value | Foundation aliases generated |
|---|---|
'feedback' | foundation.txt.info, .success, .warning, .danger (default) |
'interfaceFunction' | foundation.txt.primary, .secondary, .link |
'product' | Per-theme product items: foundation.txt.promo, .cashback, etc. |
Breaking change (3.6.0 → 3.6.1): In 3.6.0, some
txt-related options were per-theme. In 3.6.1 they moved togeneration.colorTextin the workspace config. If upgrading from 3.6.0, movegenerateTxt,txtBaseColorLevel,fallbackBaseColorLevel, andtextExposurefrom individual theme configs to the workspace config.
See 07-txt-token.md for the full txt contract documentation.
Workspace synchronization checklist
Before publishing or deploying multiple themes, verify that all workspace-wide settings are consistent. Some options affect the shared architecture layers (mode/, surface/, semantic/) — a single divergent theme corrupts the output for all consumers.
| Setting | Scope | Rule |
|---|---|---|
options.interaction.legacyStructure | Workspace-wide | Must be identical across all themes. Controls whether shared layers emit solid/ghost groups. |
options.interaction.decomposition.method | Workspace-wide | Must be identical. Mixing system-scale and dilution across themes breaks the semantic layer. |
generation.colorText.* | Workspace-wide | Configured once in aplica-theme-engine.config.mjs, not per theme. |
options.interaction.decomposition.modeResolution | Per-theme | Can differ between themes. Affects only that theme's feedback surface polarity behavior. |
options.baseAdaptation | Per-theme | Can differ between themes. Affects only that theme's normal/default surfaces. |
options.txtOnStrategy | Per-theme | Can differ between themes. |
options.darkModeChroma | Per-theme | Can differ between themes. |
options.accessibilityLevel | Per-theme | Can differ between themes. |
Quick check before deploying:
# Confirm all themes declare the same legacyStructure value
grep -r "legacyStructure" theme-engine/config/
# Confirm all themes declare the same decomposition method
grep -r "method:" theme-engine/config/
# Validate schema alignment across all themes without writing
npm run sync:architecture:test
Interaction Decomposition (since 3.9.0)
By default, interface.function and interface.feedback generate interaction states (normal, action, active, focus) using the engine's palette-level logic. Version 3.9.0 introduces authored control over how those states are derived.
Decomposition method
Configure options.interaction.decomposition.method inside the theme config:
| Value | Behavior |
|---|---|
'system-scale' (default) | Legacy behavior, now explicitly named. Palette levels drive state colors (e.g., active: 120). All existing themes use this. |
'dilution' | New. State colors move toward white or black from the base color without rotating hue. Factors drive intensity (e.g., active: 0.8, action: 1.2). Values above 1.0 invert direction. |
When to use each method:
| Scenario | Method | Target | Why |
|---|---|---|---|
| Existing themes, no visual change needed | system-scale | — | Fully backward-compatible; palette levels are predictable and explicit |
| Buttons that adapt naturally to light/dark context | dilution + target: 'canvas' | — | States dilute toward white on light canvases and black on dark canvases — no manual dark-mode override needed |
| Brand color must stay chromatic in hover/active states | dilution + target: 'anchor' | source: 'palette' | Keeps interaction states within the same hue family rather than drifting to gray |
| Custom accent color for interaction states across all surfaces | dilution + target: 'anchor' | source: 'hex' | Pins the dilution destination to a specific color regardless of theme |
| Interaction states that reference another generated token | dilution + target: 'anchor' | source: 'token' | Links states to a living token — updates automatically when the referenced color changes |
function and feedback need different rules | Any method per group | — | Use options.interaction.groups to configure each independently (since 3.12.0) |
options: {
interaction: {
decomposition: {
method: 'dilution',
// direction: 'high' // 'high' = move darker (default); 'low' = move lighter
},
surfaces: {
solid: {
levels: {
// dilution: factors (1.0 = base color, <1.0 = less diluted, >1.0 = inverts direction)
action: 1.2,
active: 0.8,
focus: 0.3,
}
},
ghost: {
enabled: true,
levels: {
// ghost backgrounds use palette levels, even in dilution mode
action: 40,
active: 60,
focus: 20,
}
}
}
}
}
normal always stays on the authored base color — no override is needed or expected.
Backward compatibility
- Themes with no
interactionconfig generate exactly as before —system-scaleis the implicit default. - Legacy
options.interfaceFunctionPaletteLevelsstill works and is mapped internally tooptions.interaction.surfaces.solid.levels. - Existing token structures for
background,txtOn,border, andtxtremain unchanged.
Dilution targets (since 3.9.0 / 3.13.1)
When using method: 'dilution', the target property controls which destination the states dilute toward:
| Value | Behavior |
|---|---|
'canvas' (default for dilution) | Moves toward the resolved canvas for the active quadrant — lightens on light canvases, darkens on dark canvases. fallback controls behavior when no canvas is resolvable. |
'anchor' (since 3.13.1) | Moves toward a configurable chromatic anchor. States shift in hue toward the anchor rather than toward white/black. |
Canvas-aware dilution (since 3.9.0)
options: {
interaction: {
decomposition: {
method: 'dilution',
target: 'canvas', // default when method is 'dilution'
fallback: 'ambient-neutral', // 'legacy' | 'ambient-neutral'
}
}
}
Anchor-aware dilution (since 3.13.1–3.13.3)
options: {
interaction: {
decomposition: {
method: 'dilution',
target: 'anchor',
anchor: {
source: 'palette', // 'palette' | 'hex' | 'token'
// hex: '#7C3AED', // required when source: 'hex'
// token: 'brand.branding.first.default.background', // required when source: 'token'
canvasAware: true, // anchor lightens on light canvases, darkens on dark canvases
canvasMix: 0.2 // 0.0–1.0 — how strongly the anchor responds to the canvas
}
}
}
}
anchor.source | Description |
|---|---|
'palette' | Current brand palette family — stays within the same color family |
'hex' | Fixed chromatic destination declared in anchor.hex |
'token' | Another generated token scale declared in anchor.token |
anchor.canvasAware + anchor.canvasMix let the anchor retain its authored hue while still lightening on light and darkening on dark canvases. Use target: 'anchor' for brighter or more chromatic secondary states that must remain systemic without manual overrides.
The
target/anchorconfig applies per-theme, per-surface, and per-group — any branch that usesmethod: 'dilution'can independently configure its target.
Per-group configuration (since 3.12.0)
When function and feedback need different decomposition behavior, declare them under options.interaction.groups:
options: {
interaction: {
decomposition: { method: 'system-scale' }, // theme default
groups: {
function: {
decomposition: { method: 'dilution' }, // override for function only
surfaces: {
solid: {
levels: { action: 1.2, active: 0.8, focus: 0.3 }
}
}
},
feedback: {
// inherits theme default (system-scale)
surfaces: {
solid: {
levels: { action: 80, active: 120, focus: 50 }
}
}
}
}
}
}
Resolution order: theme default → surface-level → group-level → group-surface-level (each layer overrides the previous).
groups.{function|feedback}.levelsis not supported. Declare state values ingroups.{function|feedback}.surfaces.solid.levelsor.ghost.levelsinstead.
Mode resolution for feedback (since 3.13.6)
By default, feedback tokens mirror across quadrant polarity: the dark-positive surface is the inverse of light-positive, and dark-negative mirrors light-negative. This is modeResolution: 'quadrant'.
Some products want feedback to follow light/dark semantics only — the same surface color on positive and negative surfaces within the same mode. Set modeResolution: 'mode' to opt in:
options: {
interaction: {
decomposition: {
method: 'dilution', // modeResolution applies to both methods
modeResolution: 'mode', // 'quadrant' (default) | 'mode'
}
}
}
| Value | Surface behavior | Text & border |
|---|---|---|
'quadrant' (default) | Positive and negative surfaces are quadrant-mirrored — a dark-positive surface is the inverse of light-positive | Text and borders follow the same quadrant inversion |
'mode' | Positive and negative surfaces are identical within the same mode — no polarity inversion | Text and borders align with the surface (no inversion) |
When to use 'mode':
- Feedback (error, warning, success) should look identical in both positive and negative surface contexts
- The product's error state is purely informational and must not shift hue or lightness based on surface polarity
When to keep 'quadrant' (default):
- Feedback colors must adapt to background context — a dark-canvas negative surface needs a clearly distinct treatment
- Anchor-aware dilution is configured and the anchor is quadrant-specific
modeResolutioncan also be set per group:options.interaction.groups.feedback.decomposition.modeResolution: 'mode'— this applies only to feedback while function keeps its own resolution strategy.
Workspace consistency rule
All themes in the same workspace must agree on options.interaction.legacyStructure:
options: {
interaction: {
legacyStructure: false, // must be identical across all themes in the workspace
}
}
| Value | Public output shape |
|---|---|
true (default) | Previous public shape — function and feedback groups as before |
false | Explicit solid and ghost groups generated across brand, mode, surface, and semantic |
Mixed workspaces break.
mode,surface, andsemanticare shared layers — if some themes havelegacyStructure: falseand others do not, those shared layers become inconsistent. Set this flag identically in all theme configs.
Quadrant-aware base adaptation (since 3.13.4)
Background — what is a quadrant? Each theme generates four variants determined by two independent axes: light vs dark mode, and positive vs negative surface context. The four combinations are light-positive, light-negative, dark-positive, dark-negative. Together they are called the active quadrant.
By default, interaction normal surfaces and product default surfaces are fixed at the authored base color regardless of the active quadrant. This means a primary button looks identical in light-positive and dark-positive — the designer's authored hex is the stable reference in all four contexts.
Opt in to quadrant adaptation per theme:
options: {
baseAdaptation: true // interaction normal + product default respond to the active quadrant
}
Concrete effect: given a theme with action_primary: '#C40145':
| Quadrant | baseAdaptation: false (default) | baseAdaptation: true |
|---|---|---|
| light-positive | #C40145 | #C40145 (unchanged — light-positive is the baseline) |
| light-negative | #C40145 | slightly lighter shade |
| dark-positive | #C40145 | adapted dark shade |
| dark-negative | #C40145 | darkest adapted shade |
The adaptation adjusts lightness within OKLCh while preserving hue and chroma — the color family stays the same.
Tokens affected: only interface.function.*.normal.background and product.*.default.background families. All other states (action, active, focus) and all txtOn/border/txt values are unaffected.
When to use:
- The primary button is hard to see in
dark-negativebecause its authored color is too close to the canvas - The brand explicitly wants base surfaces to shift with context (expressive brands, high visual dynamism)
When not to use:
- Financial or institutional products where the brand color must remain a fixed, recognizable reference
- When
normalis used as a color-matching anchor in the UI (e.g., the button matches the brand logo)
Per-theme, not workspace-wide. Unlike
legacyStructure,baseAdaptationis independent per theme — themes in the same workspace can mixtrueandfalsefreely without corrupting shared layers.
Overrides
Overrides allow replacing engine-generated values in specific cases:
overrides: {
// Replace grayscale (warm temperature)
grayscale: {
light: {
surface: { '5': '#faf8f5', '10': '#f5f2ee', '140': '#1a1814' },
txtOn: { /* ... */ },
border: { /* ... */ }
},
dark: { /* same structure */ }
},
// Replace neutrals for a specific color
neutrals: {
brand_primary: {
light: { surface: { '5': '#fdf4f9' } },
dark: { surface: { '5': '#1a0d14' } }
}
}
}
Rule: Overrides are the last resort after exhausting the default semantic options. Validate with
theme-engine sync:architecture:testafter applying overrides to catch reference mismatches before a full build.
Dynamic interaction state overrides (since 3.14.5)
overrides.interaction replaces the legacy overrides.interface.function.* flat form. Override colors pass through the full decomposition pipeline — dilution, base adaptation, quadrant mirroring, and accessibility text — instead of being applied as raw patches.
overrides: {
interaction: {
// Group-level: applies to ALL items in the group
function: {
states: {
active: { color: '#0067FF' } // all function items, active state
}
},
// Item-level: applies to one specific item across its states
feedback: {
items: {
warning_default: {
states: {
normal: { color: '#FF9900' }
}
}
}
}
}
}
Targeting hierarchy — most specific wins:
| Target | Key path | Example |
|---|---|---|
| All items, all presets, one state | interaction.{group}.states.{state} | function.states.active |
| All items, one preset, one state | interaction.{group}.states.{state}.{preset} | function.states.active.solid |
| One item, all presets, one state | interaction.{group}.items.{item}.states.{state} | feedback.items.warning_default.states.normal |
| One item, one preset, one state | interaction.{group}.items.{item}.states.{state}.{preset} | feedback.items.warning_default.states.normal.ghost |
Accepted values per state node:
{ color: '#hex' } // background color only
{ color: '#hex', txtOn: '#hex', border: '#hex', txt: '#hex' } // full override
When only color is provided, the engine derives txtOn, border, and txt using the configured txtOnStrategy and accessibilityLevel. Explicit txtOn/border/txt values bypass derivation — use with care.
Accepted state names: normal, action, active, focus, disabled
Function item keys: primary, secondary, link
Feedback item keys: info_default, info_secondary, success_default, success_secondary, warning_default, warning_secondary, danger_default, danger_secondary
Migration: The legacy
overrides.interface.function.*form is still accepted as a compatibility alias foroverrides.interaction.function.states.*. New configs should use theoverrides.interactionpath.
Gradients
Gradients are controlled globally in themes.config.json and can be customized per theme:
// In the theme config — gradients customized per brand color
gradients: {
brand: {
first: {
angle: 135,
stops: [
{ position: 0, colorRef: 'brand.branding.first.lowest.background' },
{ position: 1, colorRef: 'brand.branding.first.default.background' }
]
},
second: { /* ... */ },
third: { /* ... */ }
}
}
When omitted, the engine uses a solid gradient as a stub. To disable gradients for the entire project, configure global.gradients: false in themes.config.json.
Build order with gradients: Gradients only appear in the final output when
data/semantic/default.jsonhas thesemantic.color.gradientsection — created bysync:architecture. Always runnpm run tokens:build(full pipeline) rather thantokens:build:allalone when gradients are enabled.
Register the Theme in the Build
After creating the .config.mjs, register the theme in the central config file:
// theme-engine/config/global/themes.config.json
{
"themes": {
"my_brand": {
"includePrimitives": true,
"foundation": {
"brand": "engine",
"files": [
"default.json",
"styles/typography_styles.json",
"styles/elevation_styles.json"
]
}
}
}
}
Critical rule: The key in themes must be identical to the name defined in the theme config and to the folder name generated in data/brand/.
Generation Pipeline
Full pipeline (recommended)
npm run tokens:build
Runs automatically in the correct order:
ensure:data— ensuresdata/structure existsdimension:generate— generates dimensional scalethemes:generate— decomposes colors and generatesdata/brand/<theme>/sync:architecture— propagates references to mode, surface, semantic, and foundationfoundations:generate— generates Foundation aliasesbuild:all— Style Dictionary →dist/(JSON, CSS, ESM, CJS, TypeScript)
Useful individual commands
| Command | When to use |
|---|---|
npm run tokens:themes | After changing colors or mapping for a theme |
theme-engine themes:single --config=my-brand | Generate only a specific theme |
npm run tokens:sync | After changing the architecture schema |
npm run tokens:foundations | After changing a foundation config |
npm run tokens:build:all | Only the Style Dictionary build (when data/ is already up to date) |
theme-engine validate:data | Verify data/ integrity before building |
Brand bundle splitting for Figma (since 3.14.0)
When figma:generate is run, the engine evaluates the token count in each brand bundle. If the bundle would exceed Figma's per-collection token limit (default 5,000), it automatically splits _brand.json into semantic chunks:
| Chunk file | Contents |
|---|---|
_brand_product.json | Product/career semantic tokens |
_brand_text.json | Text tokens |
_brand_interface.json | Interface function and feedback tokens |
_brand_core.json | Core brand tokens (when further split needed) |
This is fully automatic — no config change is required. The logical token contract and dist/ output are unchanged; splitting only affects the intermediate data/brand/ files that Tokens Studio reads.
To override the token limit (e.g., if your Figma plan has a different collection budget):
APLICA_THEME_ENGINE_BRAND_TOKEN_LIMIT=3000 npm run tokens:figma
Workspaces with large product schemas (many career types, many product colors) are most likely to trigger splitting.
Golden tip — brand token performance. If your brand bundle exceeds approximately 3,000 tokens, Figma collection performance will start to degrade (sluggish variable picker, slow sync). Automatic splitting (above) is the first line of defense. If performance is still a problem after splitting, consider:
- Break large schemas into separate workspaces — e.g., split a multi-brand workspace into per-brand repos.
- Disable the
negativesurface (extreme measure) — outputting onlypositivequadrants halves the brand file size. Setoptions.surfaces: ['positive']in the workspace config. Only recommended when the product does not use negative-surface UI contexts at all.
When to run sync:architecture
The sync propagates references between layers. Run when:
- Changing the architecture schema (adding/removing feedback or product items)
- Adding a new theme and needing updated semantic references
- Gradients are enabled and you ran
themes:generate - After pulling changes that altered the schema structure
Never manually edit
data/mode/*.json,data/surface/*.json,data/semantic/default.json, ordata/foundation/engine/default.json. The sync overwrites any manual edits in those files.
Adding a New Foundation
When the default alias set is insufficient for a specific consumer:
- Create
theme-engine/config/foundations/my-foundation.config.mjs(based onengine.config.mjs) - Define
name,outputPath,structure(sections and items), andreferences(mapping to Semantic) - Run
npm run tokens:foundations - Link to the theme in
themes.config.json:"foundation": { "brand": "my-foundation", ... }
Quick Reference
| Task | Action |
|---|---|
| Create new theme | Add .config.mjs in theme-engine/config/ + register in themes.config.json + npm run tokens:build |
| Change a theme's colors | Edit the theme's .config.mjs + npm run tokens:themes + npm run tokens:build:all |
| Change foundation | Edit .config.mjs in theme-engine/config/foundations/ + npm run tokens:foundations + npm run tokens:build:all |
| Change schema (feedback/product) | Edit schema override in theme-engine/schemas/ + npm run tokens:sync + rebuild |
| Gradients don't appear in output | Run npm run tokens:sync before tokens:build:all |
| Verify without writing | theme-engine sync:architecture:test |
| View current schema | theme-engine sync:architecture:schema |
References
- What is the Theme Engine: 01-what-is-theme-engine.md
- Detailed build pipeline: 04-build-pipeline.md
- Output formats: 05-output-formats.md
- Engineering quick start: 09-engineering/01-quick-start.md
- Workspace structure: 09-engineering/02-workspace-structure.md
- Full theme configuration reference: 09-engineering/03-theme-configuration.md
- CLI reference: 09-engineering/05-cli-reference.md
- Color system: 01-colors.md
- Foundation layer: 05-foundation-layer.md